WASM 逆向工程流程规范 v4.0

WASM 逆向工程流程规范 v4.0

适用范围:Web 风控、签名参数、Token 生成、HLS Token、WebSocket 握手签名、加密参数复刻。
核心原则:先逆 JS 对 WASM 的调用关系,再逆 WASM 本身。

v4.0 整合了普通样本、Rust / Go / Emscripten、混淆样本、控制流平坦化、VM 型 WASM 的完整分析体系。


目录


零、总原则

0.1 分析顺序(不可颠倒)

JS Glue → Import → Export → Memory → WASM

反例(很多人搞反的):

WASM → Memory → Export → JS

0.2 效率原则

原则说明
找输入输出 > 完全理解算法前者半小时,后者可能三天
动态 > 静态能 hook 就不要啃 CFG
Import > ExportImport 暴露外部依赖,比函数列表信息量大
Memory API > 目标函数hook 内存写入比 hook sign() 更通杀
长度猜算法 > 逐行读代码32 hex = MD5,64 hex = SHA256

0.3 时间分配

  • 80% 时间:找入口、输入输出、内存布局、参数预处理
  • 20% 时间:理解算法本身
  • 算法只占 20%,参数预处理占 80%

一、阶段一:识别编译器

1.1 检查调试信息

# name section
wasm-objdump -h app.wasm | grep -i name

# DWARF
wasm-objdump -h app.wasm | grep debug

优先级:

  1. 有 name section → wasm2wat 直接带函数名
  2. 有 DWARF → llvm-dwarfdump 提取源码路径、函数名、变量名
  3. 都没有 → strip 模式,靠指纹判断

1.2 编译器特征指纹

特征来源备注
__wbindgen_*Rust + wasm-bindgenstrip 后仍保留 malloc/free/realloc
_ZN...Rust / C++legacy mangling
_RNv...Rustv0 mangling
_emscripten_*C/C++ + Emscripten同时有 emscripten_resize_heap
golang.wasm + Go.importObjectGoGo 1.21+ 用 go:wasmimport
__new / __pin / __collectAssemblyScript体积小、无 GC 运行时
wasi_snapshot_preview1WASI文件、时钟、随机

1.3 快速指纹命令

# Rust
strings app.wasm | grep -E "panicked at|unwrap|assertion|alloc::|core::|std::|__wbindgen"
strings app.wasm | grep -E "^_R"

# Go
strings app.wasm | grep -E "go:wasmimport|runtime.wasmExit|syscall/js"

# AssemblyScript
strings app.wasm | grep -E "__new|__pin|__unpin|__collect"

二、阶段二:Import 分析(第一优先级)

2.1 导出 Import Section

wasm-objdump -x app.wasm | sed -n '/Import/,/^$/p'

分析时第一件事:先看 Import Section,而不是先看函数。

2.2 按模块名分类

Import 模块典型来源含义
envEmscripten / Rust系统调用、随机数、时间
wasi_snapshot_preview1WASI文件、时钟、随机
wbgwasm-bindgenJS 函数桥接
goGoruntime 桥接
jsAssemblyScriptJS 交互

2.3 关键 import 名

(import "env" "random" ...)        ; 签名依赖随机数
(import "env" "now" ...)           ; 签名依赖时间戳
(import "env" "get_device_id" ...) ; 直接暴露风控来源
(import "env" "log" ...)           ; 定位调用点
(import "env" "abort" ...)         ; 错误处理

2.4 Hook 所有 Import

const origImports = { ...imports.env };
for (const k in origImports) {
    imports.env[k] = (...args) => {
        console.log('import', k, args);
        return origImports[k](...args);
    };
}

产出: 外部依赖清单,直接暴露风控来源。


三、阶段三:JS Glue 分析(第二优先级)

3.1 找 Glue 文件

编译器Glue 文件
Rust wasm-bindgen*_bg.js
Emscripten*.js + *.wasm
Gowasm_exec.js
AssemblyScript*.js + *.wasm

3.2 wasm-bindgen 的三种调用模式

// 模式 A:直接传 (ptr, len)
export function sign(data) {
    const ptr0 = passStringToWasm0(data, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
    const len0 = WASM_VECTOR_LEN;
    return wasm.sign(ptr0, len0);
}

// 模式 B:传对象(struct)
export function verify(obj) {
    const ptr0 = passStringToWasm0(obj.a, ...);
    const ptr1 = passStringToWasm0(obj.b, ...);
    return wasm.verify(ptr0, len0, ptr1, len1);
}

// 模式 C:返回字符串
export function getToken() {
    const ret = wasm.getToken();
    return getStringFromWasm0(ret[0], ret[1]);
}

3.3 关键锚点

passStringToWasm0
getStringFromWasm0
WASM_VECTOR_LEN
__wbindgen_add_to_stack_pointer

3.4 Rust 返回值:经常不是返回值

这是 Rust WASM 逆向最容易踩的坑。

// Rust: pub fn sign(s: String) -> String
// 编译后: (func sign (param $retptr i32) (param $ptr i32) (param $len i32))
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
wasm.sign(retptr, ptr, len);
const r0 = new DataView(wasm.memory.buffer).getUint32(retptr, true);
const r1 = new DataView(wasm.memory.buffer).getUint32(retptr + 4, true);
const result = getStringFromWasm0(r0, r1);
wasm.__wbindgen_add_to_stack_pointer(16);

关键点:

  • retptr 是栈上 16 字节区域(两个 u32:ptr + len)
  • __wbindgen_add_to_stack_pointer(-16) 分配,(+16) 释放
  • 返回 Result<T, E> 时结构为 [tag, ptr, len],tag 0 = Ok,1 = Err

看 Rust 样本时,一定搜 __wbindgen_add_to_stack_pointer,很多输出都藏这里。

3.5 产出

  • 调用约定(参数格式、返回值格式)
  • 输入输出类型
  • 字符串构造方式
  • malloc 实现
  • 300 个 wasm func → 3 个候选

四、阶段四:Export 分析(第三优先级)

4.1 导出 Export Section

wasm-objdump -x app.wasm | sed -n '/Export/,/^$/p'

4.2 Hook 所有 Export

for (const k in wasm) {
    const fn = wasm[k];
    if (typeof fn !== 'function') continue;

    wasm[k] = function(...args) {
        const t0 = performance.now();
        let ret;
        try {
            ret = fn.apply(this, args);
        } catch (e) {
            console.log(`[${k}] threw`, e);
            throw e;
        }
        const dt = performance.now() - t0;

        if (dt > 0.1 || args.length > 2 || typeof ret === 'string') {
            console.log(`[${k}]`, { args, ret, time: dt.toFixed(2) + 'ms' });
        }
        return ret;
    };
}

4.3 筛选目标函数的启发式

特征含义
调用频率低不是工具函数
耗时高有加密运算
参数长输入是待签名数据
返回字符串输出是签名/token
调用栈深内部有复杂逻辑

4.4 Hook instantiate

const origInst = WebAssembly.instantiate;
WebAssembly.instantiate = async function(...args) {
    const r = await origInst.apply(this, args);
    window.__wasm = r.instance ? r.instance.exports : r.exports;
    console.log('WASM exports:', window.__wasm);
    return r;
};

const origStream = WebAssembly.instantiateStreaming;
WebAssembly.instantiateStreaming = async function(...args) {
    const r = await origStream.apply(this, args);
    window.__wasm = r.instance.exports;
    console.log('WASM exports (stream):', window.__wasm);
    return r;
};

五、阶段五:Memory 分析(第四优先级)

5.1 线性内存本质

uint8_t heap[16MB];

5.2 Memory Growth 导致 buffer 失效(关键坑)

// 错误:缓存 buffer
const mem = new Uint8Array(wasm.memory.buffer);
wasm.someFunction();  // 可能触发 memory.grow
mem[0];  // 读的是旧 buffer,全错

// 正确:每次重新获取
function mem() {
    return new Uint8Array(wasm.memory.buffer);
}

5.3 Hook memory.grow

const origGrow = wasm.memory.grow;
wasm.memory.grow = function(pages) {
    const ret = origGrow.call(this, pages);
    console.log('memory.grow', pages, 'new size', wasm.memory.buffer.byteLength);
    window.HEAP8 = new Int8Array(wasm.memory.buffer);
    window.HEAPU8 = new Uint8Array(wasm.memory.buffer);
    window.HEAP32 = new Int32Array(wasm.memory.buffer);
    return ret;
};

5.4 Hook Memory API(通杀技巧)

很多人 Hook sign(),实际上更通杀的是 Uint8Array.prototype.set 或 TextEncoder.prototype.encode。

// 1. Uint8Array.prototype.set —— 谁在写内存
const oldSet = Uint8Array.prototype.set;
Uint8Array.prototype.set = function(...args) {
    if (this.buffer === wasm.memory.buffer) {
        console.log('wasm memory write', args[0], 'at', args[1]);
    }
    return oldSet.apply(this, args);
};

// 2. TextEncoder.prototype.encode —— 字符串转字节
const oldEncode = TextEncoder.prototype.encode;
TextEncoder.prototype.encode = function(str) {
    console.log('encode', str);
    return oldEncode.call(this, str);
};

// 3. DataView.prototype.setUint32 —— 写整数
const oldSetU32 = DataView.prototype.setUint32;
DataView.prototype.setUint32 = function(...args) {
    if (this.buffer === wasm.memory.buffer) {
        console.log('setUint32', args);
    }
    return oldSetU32.apply(this, args);
};

马上能看到:谁在往 wasm memory 写数据、写了什么、长度多少。对高度混淆的站点特别有效。

5.5 Rust 字符串读取

function readRustString(ptr, len) {
    return new TextDecoder().decode(
        new Uint8Array(wasm.memory.buffer, ptr, len)
    );
}

5.6 Memory 修改攻击(字段定位)

const mem = new Uint8Array(wasm.memory.buffer);
const original = mem.slice(ptr, ptr + len);

for (let i = 0; i < len; i++) {
    const backup = mem[ptr + i];
    mem[ptr + i] = backup ^ 0xFF;
    const sig = wasm.sign(ptr, len);
    if (sig !== originalSig) {
        console.log(`byte ${i} participates in signature`);
    }
    mem[ptr + i] = backup;
}

快速定位:参与签名字段 vs 不参与签名字段。


六、阶段六:算法定位

6.1 长度猜算法(先根据长度猜,效率非常高)

输入长度输出长度高概率算法
任意32 hexMD5
任意40 hexSHA1
任意64 hexSHA256
任意128 hexSHA512
任意44 字符Base64(SHA256)
任意24 字符Base64(MD5)
任意与输入相近Base64 / Hex
固定短输出8-16 字符CRC / 自定义 hash
输出带 . 分隔3 段JWT

6.2 识别“假加密”

很多网站 token / sign / secure / encrypt 名字吓人,实际上:

base64(md5(JSON))

甚至:

JSON.stringify() → btoa()

结束。

常见组合:

  • base64(JSON.stringify(obj))
  • hex(md5(str + salt))
  • btoa(unescape(encodeURIComponent(str)))
  • str.split('').reverse().join('')

6.3 常数搜索

xxd app.wasm | grep -i "428a2f98"
wasm2wat app.wasm | grep -i "428a2f98"

SHA256 初始常量:

6a09e667 bb67ae85 3c6ef372 a54ff53a
510e527f 9b05688c 1f83d9ab 5be0cd19

SHA256 轮常量:

428a2f98 71374491 b5c0fbcf e9b5dba5

MD5 常量:

d76aa478 e8c7b756 242070db c1bdceee

AES S-box: 0x63, 0x7c, 0x77, 0x7b... 开头,256 字节表。

CRC32: edb88320。

Rust 常见 hash: sha2、ring、blake3、tiny-keccak。


七、阶段七:静态反汇编(高难度场景)

7.1 WABT vs Binaryen

# 指令级,精确对照
wasm2wat app.wasm > app.wat

# 伪代码,接近 C
wasm-decompile app.wasm -o app.dcmp

策略: 先用 wasm-decompile 扫一遍找可疑函数,再用 wasm2wat 精读关键函数。

7.2 Ghidra

  • 装 WASM 插件
  • 导入时选 wasm:LE:32:default
  • __wbindgen_malloc 是关键锚点
  • call_indirect 对应 table,需手动恢复目标

7.3 call_indirect 分析

不要硬啃,直接看 table。

wasm-objdump -x app.wasm | grep -A20 "Table\|Elem"
(elem (i32.const 1)
    $f12
    $f13
    $f14)

call_indirect 的实际候选函数已经有限。

C++ 虚函数:90% 都是 table dispatch,不是动态生成的。

7.4 Rust 特殊技巧

# 先搜索
__wbindgen_malloc
__wbindgen_free
__wbindgen_realloc

# 然后找
sha256 / md5 / hmac / sign / encrypt / verify

即使符号被裁掉,也通常能通过:

malloc → memcpy → 核心算法 → 返回字符串

定位签名路径。


八、阶段八:动态调试(Chrome DevTools)

8.1 Memory Inspector(隐藏神器)

  1. Sources 面板打开 wasm 文件
  2. 指令级下断点
  3. 断到 local.get 或 i32.load 时,右侧 Scope 显示 memory
  4. 右键 memory → “Inspect Memory”

支持视图: hex、uint8、uint32、float32、ascii、utf8。

分析 JSON 序列化、protobuf、msgpack 非常舒服。

8.2 Frida

Interceptor.attach(wasmExports.sign, {
    onEnter(args) {
        console.log('sign called', args[0], args[1]);
    },
    onLeave(retval) {
        console.log('sign returned', retval);
    }
});

九、阶段九:Browser → Node 复刻

9.1 复刻流程

WASM → 定位 export.sign → 抓输入 → 抓输出 → 分析 memory 格式 → Node 复现

9.2 真正难的不是算法

算法只占 20%,参数预处理占 80%。

优先逆 passStringToWasm0() 附近逻辑,而不是先研究 10000 行加密代码。

9.3 参数预处理清单

// 1. 时间戳格式
Date.now()                    // 毫秒
Math.floor(Date.now()/1000)   // 秒
new Date().toISOString()      // ISO

// 2. 随机数来源
Math.random()                 // 可预测
crypto.getRandomValues()      // 不可预测
performance.now()             // 高精度

// 3. 字符串拼接顺序
const signStr = `${a}&${b}&${c}`;

// 4. 编码方式
encodeURIComponent()
btoa()
JSON.stringify()

9.4 Hook 时间戳和随机数

const origNow = Date.now;
Date.now = function() {
    const v = origNow();
    console.trace('Date.now', v);
    return v;
};

十、Go WASM 特殊流程

10.1 识别

strings app.wasm | grep -E "go:wasmimport|runtime.wasmExit|syscall/js"

典型特征: new Go()、go.run(instance)、wasm_exec.js。

10.2 Rust 经验套 Go 会失效

RustGo
字符串传递ptr + len 非常明显runtime 打包
内存线性内存直接可见自有 GC heap
特征__wbindgen_*runtime / GC / scheduler / stack growth 全打包

10.3 换思路:先看桥接点

syscall/js.valueCall
syscall/js.valueGet
syscall/js.valueSet
syscall/js.valueNew
syscall/js.valueInvoke

Go 大量逻辑通过 syscall/js 访问浏览器对象,比盯着 wasm 指令有价值。


十一、完整决策树

拿到 wasm
  ↓
有 name section? → 是 → wasm2wat 直接看
  ↓ 否
有 DWARF? → 是 → llvm-dwarfdump
  ↓ 否
是 Go WASM? → 是 → 看 syscall/js 桥接
  ↓ 否
Import 有 random/now/device_id? → 是 → 风控依赖这些
  ↓ 否
有 JS Glue? → 是 → 读 glue,定位 export 和调用约定
  ↓ 否
Export 名可读? → 是 → 直接 hook export
  ↓ 否
Hook export + memory → 观察输入输出
  ↓
长度猜算法 → 验证
  ↓
需要深入? → Ghidra + call_indirect 分析

十二、工具优先级

S 级

  • Chrome DevTools(含 Memory Inspector)
  • WABT(wasm2wat)
  • Ghidra + WASM Plugin
  • wasm-tools print

A 级

  • Binaryen(wasm-dis / wasm-decompile)
  • IDA Pro
  • llvm-dwarfdump

B 级

  • Frida
  • Burp Suite
  • mitmproxy
  • radare2

2026 年个人工具优先级(针对签名/风控/Token)

Chrome DevTools
    ↓
JS Glue
    ↓
wasm-tools print
    ↓
Ghidra
    ↓
Frida

而不是:

Ghidra → IDA → 看 3 天 CFG

因为:找到输入输出 > 完全理解算法。


十三、实战口诀

一看 Import        → 外部依赖(随机数、时间、设备指纹)
二看 JS Glue       → 调用约定、输入输出格式
三看 Export        → 入口函数列表
四看 Memory        → 数据布局、buffer 失效
五看 malloc        → 内存分配点、字符串构造
六看返回字符串     → 签名/token 输出
七看算法常数       → SHA/MD5/AES/CRC 指纹
八看 CFG           → call_indirect、复杂控制流

Import、JS Glue、Memory 往往已经能解决 80% 的 Web 签名样本。

真正需要深入到 Ghidra、call_indirect、CFG 恢复,通常是最后 20% 的高难度场景。


十四、一句话总结

不要逆 WASM,先逆 JS 对 WASM 的调用关系。

不要先看算法,先找输入输出和内存布局。

不要从 export 开始追,先从 Import、JS Glue、算法常数反推调用链。

实际项目里,能快速拿下签名和 token 的突破口,90% 都在 JS Glue、Import、memory layout、export 调用链,而不是 WASM 指令本身。


十五、混淆样本处理(Obfuscation Workflow)

15.1 什么时候怀疑存在混淆

JS 侧特征

出现:

_0x31a2
_0x7fd1
_0xa8b3

或者:

eval(...)
new Function(...)
Function(...)

或者:

while(!![])
(function(_0x1234){...})()

通常意味着:JS 已混淆。

WASM 侧特征

Export 极少

正常样本:

malloc
free
sign
verify
encrypt

混淆后:

a
b
c
d

甚至:仅 1~2 个 Export。

Name Section 消失

wasm-objdump -h app.wasm

不存在:

name

同时:

debug

也被删除。

call_indirect 异常多

call_indirect
call_indirect
call_indirect

占比远高于正常 Rust / Emscripten 项目。

可能存在:

  • 控制流平坦化
  • 虚拟机调度
  • 动态派发

超大 Table

(table 2000 funcref)

正常业务代码通常:几十到数百 即可。

大量 table 通常需要重点关注。


15.2 先 Dump 运行时 WASM

原则

很多项目:

网络中的 wasm ≠ 真正执行的 wasm

实际流程:

下载 → 解密 → 重组 → instantiate

因此:先拿运行时 wasm,再做分析。

Hook instantiate

const oldInstantiate = WebAssembly.instantiate;

WebAssembly.instantiate = async function(bytes, ...args) {
    if (bytes instanceof ArrayBuffer) {
        window.__wasmBinary = bytes.slice(0);
    }
    return oldInstantiate.apply(this, arguments);
};

Hook instantiateStreaming

const oldStreaming = WebAssembly.instantiateStreaming;

WebAssembly.instantiateStreaming = async function(resp, ...args) {
    const clone = resp.clone();
    const buf = await clone.arrayBuffer();
    window.__wasmBinary = buf;
    return oldStreaming.apply(this, arguments);
};

15.3 动态解密 WASM 处理

常见结构:

fetch → AES → ArrayBuffer → instantiate

或者:

fetch → xor → inflate → instantiate

重点搜索:

crypto.subtle.decrypt
atob
pako.inflate
xor
decode

Hook:

const oldDecrypt = crypto.subtle.decrypt;

crypto.subtle.decrypt = async function(...args) {
    const ret = await oldDecrypt.apply(this, args);
    console.log(ret);
    return ret;
};

目标:拿到解密后的 wasm,而不是研究解密器本身。


15.4 字符串加密处理

特征

strings app.wasm

几乎看不到可读字符串。

优先 Hook

const oldDecode = TextDecoder.prototype.decode;

TextDecoder.prototype.decode = function(...args) {
    const s = oldDecode.apply(this, args);
    if (s.length > 3) {
        console.log(s);
    }
    return s;
};

价值

直接获得运行时解密字符串,包括:

  • 接口路径
  • 盐值
  • 错误消息
  • 算法名
  • 配置字段

15.5 控制流平坦化(Control Flow Flattening)

典型结构

while (1) {
    switch(state) {
        case 1:
        ...
        break;

        case 2:
        ...
        break;
    }
}

对应 WAT

loop
  block
    br_table
  end
end

判断依据

出现:

  • 一个巨大循环
  • 大量 br_table
  • 状态变量反复修改

即可怀疑。

分析原则

不要立即恢复 CFG。

先记录:

state 变化

真实执行路径。

动态执行路径通常比静态 CFG 恢复效率高很多。


15.6 VM 型混淆

越来越常见。

典型结构

for(;;){
    opcode = code[ip++];
    switch(opcode){
        ...
    }
}

识别特征

  • 巨大函数
  • 巨大 switch
  • 大量 br_table

通常:单函数占整个 wasm 体积 30%~70%。

优先目标

找到 Dispatcher,而不是恢复所有 Handler。

分析顺序

Dispatcher → Opcode 格式 → Handler

15.7 Runtime Table Dump

针对 call_indirect 特别多的样本。

Hook:

const oldGet = WebAssembly.Table.prototype.get;

WebAssembly.Table.prototype.get = function(index) {
    const fn = oldGet.call(this, index);
    console.log("table", index);
    return fn;
};

得到:间接调用目标分布。

有助于恢复:

  • 虚表
  • trait dispatch
  • VM dispatch

15.8 Hook 全部随机源

很多风控系统:

复杂的不是算法
复杂的是随机源

统一 Hook:

Math.random
Date.now
performance.now
crypto.getRandomValues

例如:

const oldRandom = crypto.getRandomValues;

crypto.getRandomValues = function(buf) {
    console.log("random", buf.length);
    return oldRandom.call(this, buf);
};

快速判断:

  • 哪些随机值参与签名
  • 哪些随机值只是噪声

15.9 混淆样本决策树

拿到 wasm
 ↓
存在 name/debug ?
 ↓ 否
检查 Import
 ↓
检查 Glue
 ↓
Hook instantiate
 ↓
Dump Runtime Wasm
 ↓
存在解密流程 ?
 ↓ 是
Hook decrypt
 ↓
获取真实 wasm
 ↓
call_indirect 很多 ?
 ↓ 是
Dump Table
 ↓
恢复 Dispatch
 ↓
巨大 Loop ?
 ↓ 是
怀疑 VM
 ↓
定位 Opcode 解释器
 ↓
动态跟踪执行

十六、混淆场景实战口诀

先看 Import
再看 Glue
然后 Dump Runtime Wasm

能 Hook
别硬删混淆

能 Dump
别硬猜算法

能拿输入输出
别先看 CFG

先解密
后分析

先定位 Dispatcher
后看 Opcode

先跑执行路径
后恢复控制流

十七、最终原则:三类样本统一分析体系

17.1 普通样本

JS Glue → Import → Export → Memory → Algorithm

17.2 混淆样本

JS Glue → Runtime Dump → Memory → Dispatcher → Algorithm

17.3 VM 样本

JS Glue → Runtime Dump → Opcode → Handler → Algorithm

十八、v4.0 完整结构总览

章节内容适用样本
1~10正常样本分析流程普通 WASM
11决策树全部
12工具优先级全部
13实战口诀普通样本
14一句话总结全部
15混淆样本处理混淆 / 平坦化
16混淆场景口诀混淆 / 平坦化
17三类样本统一原则全部
18结构总览—
19一句话总结全部

十九、v4.0 一句话总结

普通样本看 JS Glue,混淆样本先 Dump Runtime Wasm,VM 样本先找 Dispatcher。

能 Hook 别硬删,能 Dump 别硬猜,能拿输入输出别先看 CFG。

先解密后分析,先定位 Dispatcher 后看 Opcode,先跑执行路径后恢复控制流。

这样就把 普通 WASM、Rust / Go / Emscripten、混淆样本、控制流平坦化、VM 型 WASM 全部纳入同一套分析体系了。

No Comments

Send Comment Edit Comment


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