angr学习
本文最后更新于142 天前,其中的信息可能已经过时,如有错误请发送邮件到2624241828@qq.com

环境配置

依赖环境

$ sudo apt install python-dev libffi-dev build-essential virtualenvwrapper

隔离环境

$ mkvirtualenv angr
$ sudo pip install angr

或者看这个

模糊测试

通过向目标系统提供非预期的输入并监视异常结果来发现软件漏洞。
在模糊测试中,用随机坏数据(也称做 fuzz)攻击一个程序,然后等着观察哪里遭到了破坏。模糊测试的技巧在于,它是不符合逻辑的:自动模糊测试不去猜测哪个数据会导致破坏(就像人工测试员那样),而是将尽可能多的杂乱数据投入程序中。
模糊测试的实现是一个非常简单的过程:

  1. 准备一份插入程序中的正确的文件。
  2. 用随机数据替换该文件的某些部分。
  3. 用程序打开文件。
  4. 观察破坏了什么。
    由模糊测试导致的许多故障都是内存分配错误及缓冲器溢出的结果
# 用随机数据替换文件部分的类import java . io . * ;

import java.security.SecureRandom;

import java.util.Random;

public class Fuzzer {

private Random random = new SecureRandom();

private int count = 1;

public File fuzz(File in, int start, int length) throws IOException

{

byte[] data = new byte[(int) in.length()];

DataInputStream din = new DataInputStream(new FileInputStream(in));

din.readFully(data);

fuzz(data, start, length);

String name = "fuzz_" + count + "_" + in.getName();

File fout = new File(name);

FileOutputStream out = new FileOutputStream(fout);

out.write(data);

out.close();

din.close();

count++;

return fout;

}

// Modifies byte array in place

public void fuzz(byte[] in, int start, int length) {

byte[] fuzz = new byte[length];

random.nextBytes(fuzz);

System.arraycopy(fuzz, 0, in, start, fuzz.length);

}

}
  • 有内存限制,可能因为内存溢出而崩溃,工业级fuzzer通常会使用流式处理或内存映射文件
  • 有可能改变魔数
  • 生成噪音,内存覆盖,挺基础的脚本

符号执行

符号技术弥合了静态和动态分析之间的差距,并提供了一种解决方案来应对模糊测试的有限语义洞察力。动态符号执行是符号执行的一个子集,是一种动态技术,因为它在模拟环境中执行程序。
然而,这种执行发生在符号变量的抽象域中。当这些系统模拟应用程序时,它们在整个程序执行过程中跟踪寄存器和内存的状态以及对这些变量的约束。每当到达条件分支时,执行分支并遵循两条路径,将分支条件保存为对采用分支的路径的约束,并将分支条件的逆作为对未采用分支的路径的约束。

libVEX

VEX IR是一种更加接近于compiler使用的中间语言/中间表示,它是不依赖于特定体系架构的。

Code Blocks

Code Blocks是vex处理代码的一个单元,使用IRSB结构体表示

/* Code blocks, which in proper compiler terminology are superblocks
   (single entry, multiple exit code sequences) contain:  【与Intel Pin中的概念trace是相似的】
 
   - A table giving a type for each temp (the "type environment")
   - An expandable array of statements
   - An expression of type 32 or 64 bits, depending on the
     guest's word size, indicating the next destination if the block
     executes all the way to the end, without a side exit
   - An indication of any special actions (JumpKind) needed
     for this final jump.
    
   "IRSB" stands for "IR Super Block".
*/
typedef
   struct {
      IRTypeEnv* tyenv;
      IRStmt**   stmts;
      Int        stmts_size;
      Int        stmts_used;
      IRExpr*    next;
      IRJumpKind jumpkind;
   }
   IRSB;
Each IRSB contains three things:
  - a type environment, which indicates the type of each temporary
    value present in the IRSB
  - a list of statements, which represent code
  - a jump that exits from the end the IRSB

Statements and Expressions

Statements (type 'IRStmt') represent operations with side-effects,
   eg.  guest register writes, stores, and assignments to temporaries.
   Expressions (type 'IRExpr') represent operations without
   side-effects, eg. arithmetic operations, loads, constants.
   Expressions can contain sub-expressions, forming expression trees,
   eg. (3 + (4 * load(addr1)).

Statements: IRStmt:代表着有side-effect的操作
Expressions: IRExpr:代表着没有side-effect的操作

Storage of guest state

guest state,其实就是代表目标机器寄存器的一片连续的缓存。在这片缓存上可以进行Put/Get操作。
Put/Get操作需要提供两个参数:在代表guest state的缓存中的offset和代表操作数长度的type

angr

符号执行,自动化求解输入,像一个虚拟机,会读取每一条指令在虚拟机中模拟该命令的行为
angr 使用 z3 作为约束求解器,有点像是它在收集数学公式然后把它丢给smt求解器

属性,概念

加载二进制文件生成project(或者对可执行文件进行分析和模拟)

>>> import angr
>>> proj = angr.Project('/bin/true')

project基本属性

cpu架构,文件名,入口地址

>>> import monkeyhex # 数值结果将以十六进制显示
>>> proj.arch
<Arch AMD64 (LE)>
>>> proj.entry
0x401670
>>> proj.filename
'/bin/true'

archinfo.Arch类(及其子类)是一个包含特定CPU架构所有必要信息的“数据库”。它定义了该架构如何处理数据、指令和内存,使得分析工具(如angr)能够理解二进制代码,而无需硬编码每种CPU的细节。

  • arch是archinfo.Arch对象的一个实例,表示编译程序的体系结构,通常关注,arch.bits,arch.bytes,arch.name, arch.memory_endness
  • entry是入口地址
  • filename是二进制文件的绝对路径。

加载器

CLE模块处理从二进制文件到虚拟地址空间的表示,CLE的结果被称为加载器,可以通过.loader属性来调用,可以用来查看与程序一起加载的共享库和查询加载的地址空间

>>> proj.loader
<Loaded true, maps [0x400000:0x5004000]>

>>> proj.loader.shared_objects # may look a little different for you!
{'ld-linux-x86-64.so.2': <ELF Object ld-2.24.so, maps [0x2000000:0x2227167]>,
 'libc.so.6': <ELF Object libc-2.24.so, maps [0x1000000:0x13c699f]>}

>>> proj.loader.min_addr
0x400000
>>> proj.loader.max_addr
0x5004000

>>> proj.loader.main_object  # 主模块
<ELF Object true, maps [0x400000:0x60721f]>

>>> proj.loader.main_object.execstack  # 是否有可执行堆栈
False
>>> proj.loader.main_object.pic  # 是否位置无关
True

Factory

blocks

project.factory.block()用来提取给定地址代码的基本块
angr以基本块为单位进行代码分析
可以提取

>>> block = proj.factory.block(proj.entry) # 提取程序入口的一段代码
<Block for 0x401670, 42 bytes>

>>> block.pp()                          # pretty-print打印反汇编
0x401670:       xor     ebp, ebp
0x401672:       mov     r9, rdx
0x401675:       pop     rsi
0x401676:       mov     rdx, rsp
0x401679:       and     rsp, 0xfffffffffffffff0
0x40167d:       push    rax
0x40167e:       push    rsp
0x40167f:       lea     r8, [rip + 0x2e2a]
0x401686:       lea     rcx, [rip + 0x2db3]
0x40168d:       lea     rdi, [rip - 0xd4]
0x401694:       call    qword ptr [rip + 0x205866]

>>> block.instructions                  # 有多少指令?
0xb
>>> block.instruction_addrs             # 每条指令的地址?
[0x401670, 0x401672, 0x401675, 0x401676, 0x401679, 0x40167d, 0x40167e, 0x40167f, 0x401686, 0x40168d, 0x401694]

或者代码块的其他表示形式

>>> block.capstone                       # capstone disassembly
<CapstoneBlock for 0x401670>
>>> block.vex                            # VEX IRSB (这是一个python内部地址,而不是程序地址)
<pyvex.block.IRSB at 0x7706330>

states

project对象只表示程序的一个初始镜像(理解为快照或者存档),angr分析时,是通过表示simulated program state的特殊对象simstate进行工作的

>>> state = proj.factory.entry_state()
<SimState @ 0x401670>

simstate包括程序的内存,寄存器,文件系统数据,约束条件,可以当作在保存一个动态的状态
使用state.regs和state.mem来访问state的寄存器和内存

>>> state.regs.rip        # 获取当前指令指针
<BV64 0x401670>
>>> state.regs.rax
<BV64 0x1c>      #BV,位向量bitvector。64位,进制的1c 
>>> state.mem[proj.entry].int.resolved  # 将入口点的内存解释为C int
<BV32 0x8949ed31>

这些不是python的int,而是bitvectors(cpu的数),对比就是,寄存器只有64bit,会有整数溢出的情况,可以是确定值,也可以是代数符号,因为angr的行为逻辑在模拟cpu电路,不是在按人类逻辑来

  • python int转bv再回去
>>> bv = state.solver.BVV(0x1234, 32)       # create a 32-bit-wide bitvector with value 0x1234
<BV32 0x1234>                               # BVV stands for bitvector value
>>> state.solver.eval(bv)                # convert to python int
0x1234

bv存储回寄存器和内存,也可以·存储py int,会被转换为适当大小的bv

>>> state.regs.rsi = state.solver.BVV(3, 64)
>>> state.regs.rsi
<BV64 0x3>

>>> state.mem[0x1000].long = 4
>>> state.mem[0x1000].long.resolved
<BV64 0x4>

mem用了一些python的语法糖,使用简单用法

  • 使用array[index]表示指定地址
  • 使用指定内存应解释为<数据类型>,常用值:char,short,int,long,size_t,uint8_t,uint16_t
  • 也可以设置它的值,可以是bitvector或python int
  • 使用.resolved按bv来读取值
  • 使用.concrete按py int来
    读取其他寄存器可能会遇到
>>> state.regs.rdi
<BV64 reg_48_11_64{UNINITIALIZED}>

64位bv,不包含数值,这是符号变量,符号执行的基本

模拟管理器(SimGr)

模拟管理器(Simulation Managers)是angr最重要的控制接口,它允许同时对各组状态的符号执行进行控制,同时应用搜索策略来探索程序的状态空间。states 被整理到stashes里,从而进行各种操作。
state表示一个指定时间的程序运行状态,需要一种方法将它传给next
simulation manager可以包含多个stash状态将不同state放在不同stash里面

  • 放进active桶simgr = proj.factory.simulation_manager(state)
    注:默认的stash是active
  • deadended:跑完结束的
  • found:找到目标如后门
  • pruned: 启用LAZY_SOLVES时,经回溯确认为不可满足而被剪除的state
  • unconstrained: 启用save_unconstrained时,保存指令指针受控或无约束的state
  • unsat: 启用save_unsat时,保存因约束冲突导致不可满足的state
    另外还有一个叫做errore 的列表,它不是一个stash。如果state在执行过程中发生错误,则该state会被包装在一个ErrorRecord对象中,该对象包含state和引发的错误,然后这个对象被插入到errored中
    首先,创建一个simulation manager。构造函数可以传入一个state或一个state的列表。
>>> simgr = proj.factory.simulation_manager(state)
<SimulationManager with 1 active>
>>> simgr.active
[<SimState @ 0x401670>]

执行

>>> simgr.step()

simstate对象执行中是不可变的,可以安全的在多轮执行中使用单个state

>>> simgr.active
[<SimState @ 0x1020300>]
>>> simgr.active[0].regs.rip                 # new and exciting!
<BV64 0x1020300>
>>> state.regs.rip                           # 依然和之前相同
<BV64 0x401670>

以一个有三条路径的程序举例

#include <stdio.h>
#include <stdlib.h>

int main() {
    int num = 0;
    scanf("%d", &num);

    if (num > 50) {
        if (num <= 100) {
            printf("50 < num <= 100\n");
        } else {
            printf("100 < num\n");
            exit(1);
        }
    } else {
        printf("num <= 50\n");
    }
}
// gcc example.c

模拟管理器最基本的功能是将一个stash里所有的states向前推进一个basic block,利用.step()来实现,而.run()方法可以直接执行到程序结束

>>> proj = angr.Project('a.out', auto_load_libs=False)
>>> state = proj.factory.entry_state()
>>> simgr = proj.factory.simgr(state)             # 创建 SimulationManager
>>> simgr
<SimulationManager with 1 active>
>>> simgr.active                                  # active stash
[<SimState @ 0x400640>]

>>> while len(simgr.active) == 1:                 # 一直执行到 active stash 中有不止一个 state
...     simgr.step()
...
<SimulationManager with 1 active>
...
<SimulationManager with 1 active>
<SimulationManager with 2 active>
>>> simgr.active                                  # 有 2 个 active state
[<SimState @ 0x40078f>, <SimState @ 0x400763>]

>>> simgr.step()                                  # 同时推进 2 个 state
<SimulationManager with 3 active>
>>> simgr.active                                  # 得到 3 个 state
[<SimState @ 0x400600>, <SimState @ 0x40076b>, <SimState @ 0x400779>]

>>> simgr.run()                                   # 一直执行到程序结束
<SimulationManager with 3 deadended>
>>> simgr.deadended                               # deadended stash
[<SimState @ 0x1000068>, <SimState @ 0x1000020>, <SimState @ 0x1000068>]

于是我们得到了 3 个deadended状态的state。这一状态表示一个state一直执行到没有后继者了,那么就将它从active stash中移除,放到deadended stash中
可以使用.move(),将filter_func筛选出来的state从from_stash移动到to_stash

>>> simgr.move(from_stash='deadended', to_stash='more_then_50', filter_func=lambda s: '100' in s.posix.dumps(1))
<SimulationManager with 1 deadended, 2 more_then_50>

每个stash都是一个列表,可以用列表的操作来遍历它,同时angr也提供了一些高级的方法,例如在stash名称前面加上one_,表示该stash的第一个state,在名称前加上mp_,将得到一个mulpyplexed版本的stash

>>> for s in simgr.deadended + simgr.more_then_50:
...     print hex(s.addr)
...
0x1000068L
0x1000020L
0x1000068L

>>> simgr.one_more_then_50
<SimState @ 0x1000020>
>>> simgr.mp_more_then_50
MP([<SimState @ 0x1000020>, <SimState @ 0x1000068>])
>>> simgr.mp_more_then_50.posix.dumps(0)
MP(['-2424202024@', '+0000000060\x00'])
模拟管理器所使用的探索技术(exploration techniques)

默认策略是广度优先搜索,但根据目标程序或者需要达到的目的不同,我们可能需要使用不同的探索技术,通过调用simgr.use_technique(tech)来实现,其中tech是一个ExplorationTechnique子类的实例。angr内置的探索技术在angr.exploration_techniques

  • Explorer:该技术实现了.explore()功能,允许在探索时查找或避免某些地址。
  • DFS:深度优先搜索,每次只探索一条路径,其它路径会放到deferred stash中。直到当前路径探索结束,再从deferred中取出最长的一条继续探索。
  • LoopLimiter:限制路径的循环次数,超出限制的路径将被放到discard stash中。
  • LengthLimiter:限制路径的最大长度
  • ManualMergepoint:将程序中的某个地址标记为合并点,将在一定时间范围内到达的所有state合并在一起。
  • Veritesting:试图识别出有用的合并点来解决路径爆炸问题。在创建SimulationManager时通过veritesting=True来开启。
  • Tracer:记录在某个具体输入下的执行路径,结果是执行完最后一个basic block的state,存放在traced stash中。
  • Oppologist:当遇到某个不支持的指令时,它将具体化该指令的所有输入并使用unicorn engine继续执行。
  • Threading:将线程级并行添加到探索过程中。
  • Spiller:当处于active的state过多时,将其中一些转存到磁盘上以保持较低的内存消耗。

分析

angr预先打包了几个内置分析,可以使用来提取一些信息,有这些

>>> proj.analyses.            # Press TAB here in ipython to get an autocomplete-listing of everything:
 proj.analyses.BackwardSlice        proj.analyses.CongruencyCheck      proj.analyses.reload_analyses       
 proj.analyses.BinaryOptimizer      proj.analyses.DDG                  proj.analyses.StaticHooker          
 proj.analyses.BinDiff              proj.analyses.DFG                  proj.analyses.VariableRecovery      
 proj.analyses.BoyScout             proj.analyses.Disassembly          proj.analyses.VariableRecoveryFast  
 proj.analyses.CDG                  proj.analyses.GirlScout            proj.analyses.Veritesting           
 proj.analyses.CFG                  proj.analyses.Identifier           proj.analyses.VFG                   
 proj.analyses.CFGEmulated          proj.analyses.LoopFinder           proj.analyses.VSA_DDG               
 proj.analyses.CFGFast              proj.analyses.Reassembler

CFGfast:静态分析、递归反汇编,不运行代码,记录call和jmp这些,但间接跳转不精准
CFGEmulated:仿真控制流图,动态分析、模拟执行,跑代码,但有点慢
构建和使用快速控制流图(CFGfast)

# 本来,当我们加载这个二进制文件时,它还将所有依赖项加载到同一个虚拟地址空间中
# 大多数分析是不需要的,所以指定auto_load_libs=false
>>> proj = angr.Project('/bin/true', auto_load_libs=False)
>>> cfg = proj.analyses.CFGFast()
<CFGFast Analysis Result at 0x2d85130>

# cfg.graph返回的是一个networkx DiGraph对象
# networkX(python图论算法库)
>>> cfg.graph
<networkx.classes.digraph.DiGraph at 0x2da43a0>
>>> len(cfg.graph.nodes())
951

# 使用cfg.get_any_node获取给定地址的CFGNode
>>> entry_node = cfg.get_any_node(proj.entry)
>>> len(list(cfg.graph.successors(entry_node)))
2

加载二进制文件(CLE and angr Projects)

加载器

重新加载bin/true来了解怎么和加载器进行交互

>>> import angr, monkeyhex
>>> proj = angr.Project('/bin/true')
>>> proj.loader
<Loaded true, maps [0x400000:0x5008000]>

Loaded Objects

CLE加载器(cle.Loader),代表整个加载的二进制对象,加载映射到一个单独的内存空间。 每个二进制对象被能够处理它这种文件类型的加载器后端加载,cle.ELF用来加载ELF二进制文件的。
内存中也有不代表任何加载的二进制文件的对象。比如,提供本地线程存储支持的对象,提供未解析符号支持的扩展对象。
可以用loader.all_objects获取到CLE加载的对象的完整列表,以及几个更有针对性的分类

# 所有加载的对象
>>> proj.loader.all_objects
[<ELF Object fauxware, maps [0x400000:0x60105f]>,
 <ELF Object libc.so.6, maps [0x1000000:0x13c42bf]>,
 <ELF Object ld-linux-x86-64.so.2, maps [0x2000000:0x22241c7]>,
 <ELFTLSObject Object cle##tls, maps [0x3000000:0x300d010]>,
 <KernelObject Object cle##kernel, maps [0x4000000:0x4008000]>,
 <ExternObject Object cle##externs, maps [0x5000000:0x5008000]>

# 这是“主”对象,是你在加载项目时直接指定的对象
>>> proj.loader.main_object
<ELF Object true, maps [0x400000:0x60105f]>

# 这是从共享对象名称到对象的字典映射
>>> proj.loader.shared_objects
{ 'libc.so.6': <ELF Object libc.so.6, maps [0x1000000:0x13c42bf]>
  'ld-linux-x86-64.so.2': <ELF Object ld-linux-x86-64.so.2, maps [0x2000000:0x22241c7]>}

# 这是所有从ELF文件加载的对象
# 如果是windows程序使用all_pe_objects!
>>> proj.loader.all_elf_objects
[<ELF Object true, maps [0x400000:0x60105f]>,
 <ELF Object libc.so.6, maps [0x1000000:0x13c42bf]>,
 <ELF Object ld-linux-x86-64.so.2, maps [0x2000000:0x22241c7]>]

# 这是“扩展对象”,我们用它来为未解析的导入和angr内部提供地址
>>> proj.loader.extern_object
<ExternObject Object cle##externs, maps [0x5000000:0x5008000]>

# 此对象用于为模拟的系统调用提供地址
>>> proj.loader.kernel_object
<KernelObject Object cle##kernel, maps [0x4000000:0x4008000]>

# 最后,你可以获得对给定地址的对象的引用
>>> proj.loader.find_object_containing(0x400000)
<ELF Object true, maps [0x400000:0x60105f]>

符号和重定位

可以在使用CLE时使用符号
符号是把计算机地址和函数命名对应起来的映射关系
从cle中获取符号的最简单方法是使用loader.find_symbol,它接收名称或地址并返回Symbol对象

# 符号查找
>>> malloc = proj.loader.find_symbol('malloc')
>>> malloc
<Symbol "malloc" in libc.so.6 at 0x1054400>

代码中的proj.loader其实就是CLE,负责在angr加载程序时将主程序读入内存,当发现程序需要用到libc.so.6(c语言标准库时),自动将库搬进内存
编译程序的时候,编译器并不知道malloc在哪,所以编译器在代码里留了个空,或者一个占位符,重定位就是程序启动时,将libc.so.6加载进来,确定了malloc的真实地址,回头将程序所有留空的地方填上这个地址
符号最有用的属性是它的名称,所有者和地址,但符号的地址可能是不明确的,symbol对象有三种获取其地址的方式

  • .rebased_addr是它在全局地址空间的地址。这是打印输出显示的内容
  • .linked_addr是相对于二进制的预链接基址的地址。这是例如readelf(1)获取到的地址
  • .relative_addr是它相对于对象基址的地址(即RVA,相对虚拟地址)
>>> malloc.name
'malloc'

>>> malloc.owner_obj
<ELF Object libc.so.6, maps [0x1000000:0x13c42bf]>

>>> malloc.rebased_addr
0x1054400
>>> malloc.linked_addr
0x54400
>>> malloc.relative_addr
0x54400

除了提供调试信息,符号还支持动态链接概念
libc在导出符号提供malloc,主二进制文件依赖于它,如果我们要求cle直接从主对象给我们一个malloc符号,它会告诉我们这是一个导入符号(在当前二进制文件(模块)内部定义,并且标记为允许其他二进制模块访问的符号,就是在说,记住这个东西,之后要查找,指向data段或者text段,angr属性is_export = True。注:符号指向,就是符号表的记录,将名字映射到内存地址。)导入符号没有与之关联地址,提供了用于解析他们的符号的引用,比如.resolvedby,连接import导入和export导出

>>> malloc.is_export
True
>>> malloc.is_import
False

# 在Loader上,方法是find_symbol,因为它执行搜索操作来查找符号。
# 在单个对象上,方法是get_symbol,因为只能有一个具有给定名称的符号。
>>> main_malloc = proj.loader.main_object.get_symbol("malloc")
>>> main_malloc
<Symbol "malloc" in true (import)>
>>> main_malloc.is_export
False
>>> main_malloc.is_import
True
>>> main_malloc.resolvedby
<Symbol "malloc" in libc.so.6 at 0x1054400>

导入和导出之间的链接在内存中的具体注册方式由重定位来处理,重定位表示,当你将导入与导出符号匹配时,在导出地址写入[location],格式为[format].我们可以看到对象(作为relocation实例)的完整重定位列表obj.relocs,或者仅查看从符号表名称到重定位映射obj.imports,导出符号没有对应列表
可以通过以下方式访问重定位对应的导入符号:重定位将写入的地址可以通过可用于Symbol的任何地址标识符访问,你可以通过访问relocation.symbol(存着导入符号)属性来获取这个重定位表对应的导入表符号,并且你还可以通过.owner_obj属性,来获取请求这次重定位的对象。

# 重定位对象没有实现很好的格式化输出(pretty-printing),所以(你看到的)那些地址其实是 Python 内部对象的地址,与我们正在分析的程序毫无关系
>>> proj.loader.shared_objects['libc.so.6'].imports
{u'__libc_enable_secure': <cle.backends.relocations.generic.GenericJumpslotReloc at 0x4221fb0>,
 u'__tls_get_addr': <cle.backends.relocations.generic.GenericJumpslotReloc at 0x425d150>,
 u'_dl_argv': <cle.backends.relocations.generic.GenericJumpslotReloc at 0x4254d90>,
 u'_dl_find_dso_for_object': <cle.backends.relocations.generic.GenericJumpslotReloc at 0x425d130>,
 u'_dl_starting_up': <cle.backends.relocations.generic.GenericJumpslotReloc at 0x42548d0>,
 u'_rtld_global': <cle.backends.relocations.generic.GenericJumpslotReloc at 0x4221e70>,
 u'_rtld_global_ro': <cle.backends.relocations.generic.GenericJumpslotReloc at 0x4254210>}

当某个导入符号无法被解析(例如因为找不到对应的共享库)时,CLE 会自动更新externs对象(loader.extern_obj)让这个对象来顶替,声明由它来导出这个符号,防止程序分析因为缺文件而崩溃

加载选项

如果你在使用angr.Project加载目标,并且希望向Project隐式创建的cle.Loader实例传递选项,你可以直接将关键字参数传递给Project的构造函数,这些参数会被转发CLE。

基础选项

auto_load_libs:用于启用或禁用 CLE 尝试自动解析共享库依赖的功能,默认情况下是开启的
except_missing_libs:与之相对选项,如果设置为true,当二进制文件存在无法解析的共享库依赖时,将会抛出异常
你可以向force_load_libs传递一个字符串列表,列表中列出的任何内容都会在一开始就被视为未解析的共享库依赖。或者,你可以向skip_libs传递一个字符串列表,以此来阻止任何匹配该名称的库被解析为依赖项。此外,你还可以向ld_path传递一个字符串列表(或单个字符串),它将作为共享库的额外搜索路径,并且优先级高于任何默认路径(默认路径包括:加载程序的同级目录、当前工作目录以及你的系统库目录)。

针对二进制文件的选项(Per-Binary Options)

如果你想指定一些仅适用于特定二进制对象的选项,CLE也允许你这样做。参数main_opts和lib_opts通过接受选项字典来实现这一功能。main_opts是一个从选项名到选项值的映射,而lib_opts是一个从库名称到“选项名与值的映射字典”的映射。
可用选项因后端而异,通用选项包括

  • backend:要使用的后端(可以是类或名称)
  • base_addr:要使用的基地址
  • entry_point:要使用的入口点
  • arch:要使用的架构名称
angr.Project(main_opts={'backend': 'ida', 'arch': 'i386'}, lib_opts={'libc.so.6': {'backend': 'elf'}})

后端

CLE 目前拥有用于静态加载ELF、PE、CGC、Mach-O和ELF core dump(核心转储)文件的后端,同时也支持使用IDA加载二进制文件,以及将文件加载到平坦地址空间(flat address space)。在大多数情况下,CLE会自动检测并使用正确的后端,所以除非你在做一些非常奇怪的事情,否则通常不需要指定使用哪个后端。
你可以通过在选项字典中包含一个键来强制 CLE 对某个对象使用特定的后端,某些后端无法自动检测应该使用的架构,因此必须指定arch

后端名称描述需要指定架构?
elf基于 PyELFTools 的 ELF 文件静态加载器
pe基于 PEFile 的 PE 文件静态加载器
mach-oMach-O 文件的静态加载器。不支持动态链接或重基(rebasing)。
cgcCyber Grand Challenge 二进制文件的静态加载器
backedcgc允许指定内存和寄存器支持者(backers)的 CGC 二进制文件静态加载器
elfcoreELF 核心转储(core dumps)的静态加载器
ida启动一个 IDA 实例来解析文件
blob将文件作为平坦镜像(flat image)加载到内存中

符号化函数摘要(Symbolic Function Summaries)

默认情况下,project会尝试使用被称为SimProcedures的符号化摘要来替换对库函数的外部调用(实际上就是模拟库函数对状态产生影响的Python函数)已经实现了很多此类函数作为SimProcedures,内置过程可以在angr.SIM_PROCEDURES字典中找到
字典的第一级是包名,第二级是库函数名称,执行SimProcedure而不是从系统中加载的实际库函数,会让分析变得更加易于处理,代价是可能会有一些不准确性。
当给定的函数没有可用的摘要时:

  1. 如果auto_load_libs为True(这是默认值),那么将执行真正的库函数。这可能是你想要的,也可能不是,取决于具体的函数。例如,libc中的某些函数分析起来极其复杂,很可能会导致尝试执行它们的路径出现状态数量爆炸
  2. 如果auto_load_libs为False,那么外部函数将处于未解析状态,Project会将它们解析为一个通用的“桩(stub)” SimProcedure(ReturnUnconstrained),就是每次被调用时都会返回一个唯一的、无约束的符号值
  3. 如果use_sim_procedures(这是angr.Project的参数,而不是cle.Loader的参数)为False(默认为True),那么只有由extern对象提供的符号会被替换为SimProcedures,并且它们会被替换为ReturnUnconstrained桩,该桩除了返回一个符号值外什么也不做。
  4. 你可以通过angr.Project的参数exclude_sim_procedures_list和exclude_sim_procedures_func来指定某些特定的符号不被SimProcedures替换
    具体的算法逻辑可以查看angr.Project.-register_object的代码

hook

angr用Python摘要替换库代码的机制称为Hooking.在执行模拟时,angr会在每一步检查当前地址是否被Hook了,如果是,则运行Hook代码而不是该地址处的二进制代码。让你实现这一点的API是proj.hook(addr,hook),其中hook是一个SimProcedure实例。你可以通过 .is_hooked、.unhook和.hooked_by来管理项目的Hook。
还有一种替代的Hook地址的API,允许你通过使用proj.hook(addr)作为函数装饰器,来指定你自己的临时编写的函数作为Hook。如果你这样做,你还可以选择指定一个length关键字参数,以便在你的Hook完成后让执行向前跳过若干字节。

>>> stub_func = angr.SIM_PROCEDURES['stubs']['ReturnUnconstrained'] # 这是一个类(CLASS)
>>> proj.hook(0x10000, stub_func())  # 使用该类的一个实例进行 hook

>>> proj.is_hooked(0x10000)            # 这些函数应该是非常直观的
True
>>> proj.unhook(0x10000)
>>> proj.hooked_by(0x10000)
<ReturnUnconstrained>

>>> @proj.hook(0x20000, length=5)
... def my_hook(state):
...     state.regs.rax = 1

>>> proj.is_hooked(0x20000)
True

此外,你可以使用proj.hook_symbol(name,hook),通过提供符号名称作为第一个参数,来Hook该符号所在的地址。这有一个非常重要的用途,就是扩展angr内置库SimProcedures的行为。由于这些库函数只是类,你可以继承它们,重写它们的部分行为,然后将你的子类用于Hook。

求解器引擎

angr是一个符号执行工具,它通过符号表达式来模拟程序的执行,将程序的输出表示成包含这些符号的逻辑或数学表达式,然后利用约束求解器进行求解。
bitvectors是一个比特串,前面看到了bitvectors做的一些具体的数学运算。但bitvectors 不仅可以表示具体的数值,还可以表示虚拟的数值,即符号变量。

>>> x = state.solver.BVS("x", 64)
>>> x
<BV64 x_0_64>
>>> y = state.solver.BVS("y", 64)
>>> y
<BV64 y_1_64>

而符号变量之间的运算同样不会时具体的数值,而是一个AST,所以我们接下来同样使用bitvector来指代AST:

>>> x + 0x10
<BV64 x_0_64 + 0x10>
>>> (x + 0x10) / 2
<BV64 (x_0_64 + 0x10) / 0x2>
>>> x - y
<BV64 x_0_64 - y_1_64>

每个AST都有一个.op和一个.args属性:

>>> tree = (x + 1) / (y + 2)
>>> tree
<BV64 (x_0_64 + 0x1) / (y_1_64 + 0x2)>
>>> tree.op                                       # op 是表示操作符的字符串
'__floordiv__'
>>> tree.args                                     # args 是操作数
(<BV64 x_0_64 + 0x1>, <BV64 y_1_64 + 0x2>)
>>> tree.args[0].op
'__add__'
>>> tree.args[0].args
(<BV64 x_0_64>, <BV64 0x1>)
>>> tree.args[0].args[1].op
'BVV'
>>> tree.args[0].args[1].args
(1L, 64)

知道了符号变量的表示,接下来看符号约束:

>>> x == 1                                        # AST 比较会得到一个符号化的布尔值
<Bool x_0_64 == 0x1>
>>> x + y > 100
<Bool (x_0_64 + y_1_64) > 0x64>

>>> state.solver.BVV(1, 64) > 0                   # 无符号数 1
<Bool True>
>>> state.solver.BVV(-1, 64) > 0                  # 无符号数 0xffffffffffffffff
<Bool True>

正因为布尔值是符号化的,所以在需要做if或者while判断的时候,不要直接使用比较作为条件,而应该使用.is_true和.is_false来进行判断:

>>> yes = state.solver.BVV(1, 64) > 0
>>> yes
<Bool True>
>>> state.solver.is_true(yes)
True
>>> state.solver.is_false(yes)
False

>>> maybe = x == y
>>> maybe
<Bool x_0_64 == y_1_64>
>>> state.solver.is_true(maybe)
False
>>> state.solver.is_false(maybe)
False

为了进行符号求解,首先要将符号化布尔值作为符号变量有效值的断言加入到state中,作为限制条件,当然如果添加了无法满足的限制条件,将无法求解

>>> state.solver.add(x > y)                       # 添加限制条件
[<Bool x_0_64 > y_1_64>]
>>> state.solver.add(y > 2)
[<Bool y_1_64 > 0x2>]
>>> state.solver.add(10 > x)
[<Bool x_0_64 < 0xa>]

>>> state.satisfiable()                           # 可以求解
True
>>> state.solver.eval(x + y)                      # eval 求解得到任意一个符合条件的值
15L
>> state.solver.eval_one(x + y)                   # 求解得到结果,如果有不止一个结果则抛出异常
>>> state.solver.eval_upto(x + y, 5)              # 给出最多 5 个结果
[16L, 13L, 8L, 9L, 17L]
>>> state.solver.eval_atleast(x + y, 5)           # 给出至少 5 个结果,否则抛出异常
[16L, 13L, 8L, 9L, 17L]
>>> state.solver.eval_exact(x + y, 5)             # 有正好 5 个结果,否则抛出异常
>>> state.solver.min(x + y)                       # 给出最小的结果
7L
>>> state.solver.max(x + y)                       # 给出最大的结果
17L

>>> state.solver.eval(x + y, extra_constraints=[x + y < 10, x + y > 5]) # 额外添加临时限制条件
8L
>>> state.solver.eval(x + y, cast_to=str)         # 指定输出格式
'\x00\x00\x00\x00\x00\x00\x00\x08'

>>> state.solver.add(x - y > 10)                  # 添加不可满足的限制条件
[<Bool (x_0_64 - y_1_64) > 0xa>]
>>> state.satisfiable()                           # 无法求解
False

angr使用z3作为约束求解器,而z3支持IEEE754浮点数的理论,所以我们也可以使用浮点数。使用FPV和FPS即可创建浮点数值和浮点符号

>>> state = proj.factory.entry_state()            # 刷新状态
>>> a = state.solver.FPV(3.2, state.solver.fp.FSORT_DOUBLE) # 浮点数值
>>> a
<FP64 FPV(3.2, DOUBLE)>

>>> b = state.solver.FPS('b', state.solver.fp.FSORT_DOUBLE) # 浮点符号
>>> b
<FP64 FPS('FP_b_2_64', DOUBLE)>

>>> a + b
<FP64 fpAdd('RNE', FPV(3.2, DOUBLE), FPS('FP_b_2_64', DOUBLE))>
>>> a + 1.1
<FP64 FPV(4.300000000000001, DOUBLE)>

>>> a + 1.1 > 0
<Bool True>
>>> b + 1.1 > 0
<Bool fpGT(fpAdd('RNE', FPS('FP_b_2_64', DOUBLE), FPV(1.1, DOUBLE)), FPV(0.0, DOUBLE))>

>>> state.solver.add(b + 2 < 0)
[<Bool fpLT(fpAdd('RNE', FPS('FP_b_2_64', DOUBLE), FPV(2.0, DOUBLE)), FPV(0.0, DOUBLE))>]
>>> state.solver.add(b + 2 > -1)
[<Bool fpGT(fpAdd('RNE', FPS('FP_b_2_64', DOUBLE), FPV(2.0, DOUBLE)), FPV(-1.0, DOUBLE))>]
>>> state.solver.eval(b)
-2.4999999999999996

bitvectors和浮点数的转换使用raw_to_bv和raw_to_fp

>>> a.raw_to_bv()
<BV64 0x400999999999999a>
>>> b.raw_to_bv()
<BV64 fpToIEEEBV(FPS('FP_b_2_64', DOUBLE))>

>>> state.solver.BVV(0, 64).raw_to_fp()
<FP64 FPV(0.0, DOUBLE)>
>>> state.solver.BVS('x', 64).raw_to_fp()
<FP64 fpToFP(x_3_64, DOUBLE)>

或者如果我们需要指定宽度的bitvectors,可以使用val_to_bv和val_to_fp

>>> a
<FP64 FPV(3.2, DOUBLE)>
>>> a.val_to_bv(12)
<BV12 0x3>
>>> a.val_to_bv(12).val_to_fp(state.solver.fp.FSORT_FLOAT)
<FP32 FPV(3.0, FLOAT)>

程序状态

state.step()用于模拟执行的一个basic block并返回一个SimSuccessors类型的对象,由于符号执行可能产生多个state,所以该对象的.successors属性是一个列表,包含了所有可能的state。
程序状态state是一个SimState类型的对象,angr.factory.AngrObjectFactory类提供了创建state对象的方法:

  • .blank_state():返回一个几乎没有初始化的 state 对象,当访问未初始化的数据时,将返回一个没有约束条件的符号值。
  • .entry_state():从主对象文件的入口点创建一个state。
  • .full_init_state():与entry_state() 类似,但执行不是从入口点开始,而是从一个特殊的SimProcedure开始,在执行到入口点之前调用必要的初始化函数。
  • .call_state():创建一个准备执行给定函数的state。
  1. 所有方法都可以传入参数addr来指定开始地址
  2. 可以通过args传入参数列表,env传入环境变量。类型可以是字符串,也可以是bv
  3. 通过传入一个符号bv作为argc,可以将argc符号化
  4. 对于.call_state(addr, arg1, arg2, …),addr是希望调用的函数地址,argN是传递给函数的n个参数,如果希望分配一个内存空间并传递指针,则需要使用angr.PointerWrapper(),如果需要指定调用约定,可以传递一个SimCC对象作为cc参数
    创建的state可以很方便的复制和合并
>>> s = proj.factory.blank_state()
>>> s1 = s.copy()                                       # 复制 state
>>> s2 = s.copy()

>>> s1.mem[0x1000].uint32_t = 0x41414141
>>> s2.mem[0x1000].uint32_t = 0x42424242

>>> (s_merged, m, anything_merged) = s1.merge(s2)       # 合并将返回一个元组
>>> s_merged                                            # 表示合并后的 state
<SimState @ 0x405000>
>>> m                                                   # 描述 state flag 的符号变量
[<Bool state_merge_1_14_16 == 0x0>, <Bool state_merge_1_14_16 == 0x1>]
>>> anything_merged                                     # 描述是否全部合并的布尔值
True

>>> aaaa_or_bbbb = s_merged.mem[0x1000].uint32_t        # 此时的值需要根据 state flag 来判断
>>> aaaa_or_bbbb
<uint32_t <BV32 Reverse((if (state_merge_1_14_16 == 0x1) then 0x42424242 else (if (state_merge_1_14_16 == 0x0) then 0x41414141 else 0x0)))> at 0x1000>

使用state.mem可以很方便的操作内存,但如果你想要对内存进行原始的操作时,可以使用state.memory的.load(addr, size)和.store(addr, val)

>>> s = proj.factory.blank_state()
>>> s.memory.store(0x4000, s.solver.BVV(0x0123456789abcdef, 128)) # 默认大端序
>>> s.memory.load(0x4008, 8)                                      # 默认大端序
<BV64 0x123456789abcdef>
>>> s.memory.load(0x4008, 8, endness=angr.archinfo.Endness.LE)    # 小端序
<BV64 0xefcdab8967452301>
>>> s.mem[0x4008].uint64_t.resolved                               # 与 mem 对比
<BV64 0xefcdab8967452301>

>>> s.memory.store(0x4000, s.solver.BVV(0x0123456789abcdef, 128), endness=angr.archinfo.Endness.LE) # 小端序
>>> s.memory.load(0x4000, 8)                                      # 默认大端序
<BV64 0xefcdab8967452301>
>>> s.memory.load(0x4000, 8, endness=angr.archinfo.Endness.LE)    # 小端序
<BV64 0x123456789abcdef>
>>> s.mem[0x4000].uint64_t.resolved                               # 与 mem 对比
<BV64 0x123456789abcdef>

默认情况下store和load都使用大端序们可以通过指定参数endness来使用小端序
通过state.options可以对angr的行为做特定的优化。我们既可以在创建state时将option作为参数传递进去,也可以对已经存在的state进行修改。

>>> s = proj.factory.blank_state(add_options={angr.options.LAZY_SOLVES})    # 启用 options
>>> s = proj.factory.blank_state(remove_options={angr.options.LAZY_SOLVES}) # 禁用 options

>>> s.options.add(angr.options.LAZY_SOLVES)         # 启用 option
>>> s.options.remove(angr.options.LAZY_SOLVES)      # 禁用 option

SimState对象的所有内容(包括memory、registers、mem等)都是以插件的形式存储的,这样做的好处是将代码模块化,如果我们想要在state中存储其他的数据,那么直接实现一个插件就可以了

  • state.globals:实现了一个标准的Python dict的接口,通过它可以在一个state上存储任意的数据。
  • state.history:存储了一个state在执行过程中的路径历史数据,它是一个链表,每个节点表示一个执行,通过像history.parent.parent这样的方式进行遍历。为了得到history中某个具体的值,可以使用迭代器history.NAME,值保存在history.recent_NAME,如果想要快速得到这些值的一个列表,可以查看.hardcopy
  • history.descriptions:对state每次执行的描述的列表。
  • history.bbl_addrs:state每次执行的basic block的地址的列表,每次执行可能多于一个地址,也可能是被hook的SimProcedures的地址。
  • history.jumpkinds:state每次执行时改变控制流的操作的列表。
  • history.guards:state执行中遇到的每个分支的条件的列表。
  • history.events:state执行中遇到的可能有用的事件的列表。
  • history.actions:通常是空的,但如果启用了options.refs,则会记录程序执行时访问的所有内存、寄存器和临时变量。
  • state.callstack:用于记录函数调用堆栈,它是一个链表,可以直接遍历。
  • state.callstack:获得每个调用的 frame。
  • callstack.func_addr:当前正在执行的函数的地址。
  • callstack.call_site_addr:调用当前函数的basic block的地址。
  • callstack.stack_ptr:从当前函数开头开始计算的堆栈指针的值。
  • callstack.ret_addr:当前函数的返回地址。

vex ir

angr使用了VEX作为二进制分析的中间表示。VEX IR是由Valgrind项目开发和使用的中间表示,后来这一部分被分离出去作为libVEX,libVEX用于将机器码转换成VEX IR,在angr项目中,开发了模块PyVEX作为libVEX的Python包装

>>> import pyvex, archinfo
>>> bb = pyvex.IRSB('\xc3', 0x400400, archinfo.ArchAMD64()) # 将一个位于 0x400400 的 AMD64 基本块(\xc3,即ret)转成 VEX
>>> bb.pp()     # 打印 IRSB(Intermediate Representation Super Block)
IRSB {
   t0:Ity_I64 t1:Ity_I64 t2:Ity_I64 t3:Ity_I64

   00 | ------ IMark(0x400400, 1, 0) ------
   01 | t0 = GET:I64(rsp)
   02 | t1 = LDle:I64(t0)
   03 | t2 = Add64(t0,0x0000000000000008)
   04 | PUT(rsp) = t2
   05 | t3 = Sub64(t2,0x0000000000000080)
   06 | ====== AbiHint(0xt3, 128, t1) ======
   NEXT: PUT(rip) = t1; Ijk_Ret
}

>>> bb.statements[3]                # 表达式
<pyvex.stmt.WrTmp object at 0x7f38f1ef84b0>
>>> bb.statements[3].pp()
t2 = Add64(t0,0x0000000000000008)

>>> bb.statements[3].data           # 数据
<pyvex.expr.Binop object at 0x7f38f1ef8460>
>>> bb.statements[3].data.pp()
Add64(t0,0x0000000000000008)

>>> bb.statements[3].data.op        # 操作符
'Iop_Add64'

>>> bb.statements[3].data.args      # 参数
[<pyvex.expr.RdTmp object at 0x7f38f1f77cb0>, <pyvex.expr.Const object at 0x7f38f1f77098>]
>>> bb.statements[3].data.args[0]
<pyvex.expr.RdTmp object at 0x7f38f1f77cb0>
>>> bb.statements[3].data.args[0].pp()
t0

>>> bb.next         # 基本块末尾无条件跳转的目标
<pyvex.expr.RdTmp object at 0x7f38f3cb6f38>
>>> bb.next.pp()
t1

>>> bb.jumpkind     # 无条件跳转的类型
'Ijk_Ret'

扩展工具

Patcherex:二进制文件自动化patch引擎
Driller:用符号执行增强AFL的下一代fuzzer4
Rex:自动化漏洞利用引擎
angrop:rop链自动化生成器

结束

angr会通过总结函数效果的SimProcedures来Hook复杂的库函数
还有一些东西官方文档没翻译中文了,简单写一下在这里,遇到可以看对应文档

内建分析 (Built-in Analyses)

CFG (Control Flow Graph)
它通过静态分析,画出程序里所有的函数和代码块(Basic Blocks)以及它们之间是怎么跳转的。这是做复杂分析的基础。

向后切片 (Backward Slicing)

倒推像,假设你发现程序在某处崩溃了,你只想知道“是哪些代码导致了这次崩溃”,而不想看无关代码。切片技术能帮你从崩溃点往回找,只保留影响该结果的代码路径。

函数标识符 (Function Identifier)

在分析去除了符号表(Stripped)的二进制文件时,我们不知道哪个函数是printf,哪个是strcpy。这个功能通过比对特征(指纹),尝试自动识别出这些标准库函数,方便你阅读。

Gotchas

官方的一个避坑指南,比如内存模型是如何处理未初始化内存的,或者为什么有时候脚本跑不完。

The Whole Pipeline

描述了从你输入一个二进制文件开始,数据是如何在CLE(加载,Arch(架构定义),SimState(状态)、Engine(执行)之间流转的完整生命周期。

Speed Considerations

符号执行非常慢(因为要解方程)。这部分教你如何通过设置(比如跳过复杂的库函数、简化约束求解、使用不同的内存模型)来让分析速度提升10倍甚至100倍。

Intermediate Representation (IR)

中间语言vex,angr 并不直接分析 x86、ARM 或 MIPS 指令。它先把所有架构的指令都翻译成一种叫VEX的通用中间语言。这样angr只需要学会分析VEX,就能支持几乎所有CPU架构。

Working with Data and Conventions

数据与调用约定,不同的系统(Linux/Windows)和架构传参方式不同(有的用寄存器,有的用栈)。这部分教你如何正确地读取函数参数、处理整数/浮点数类型转换。

Claripy

数据抽象层,虽然你已经了解了Solver,但Claripy是angr用来表示“符号”和“具体数值”的通用语言。它把底层的数学求解器(Z3)包装起来,让你能用 Python 语法(如x+y >10)来描述数学约束。

Symbolic Memory Addressing

符号化内存地址,例如,符号执行中,如果代码是mov eax, [ebx],但ebx是个未知数(符号),那eax到底读了哪里的内存?这部分解释了angr处理这种“薛定谔的内存读取”的几种策略。

扩展angr

Programming SimProcedures

编写模拟过程,教你如何用Python写代码来“欺骗”程序。比如程序调用了 check_password(),你不想分析它的汇编,可以直接写一个 Python 函数来模拟它的行为(Hook),告诉程序“密码正确”。

Writing State Plugins

编写状态插件,如果你想在SimState里存一些自己的私货(比如做一个全局计数器,或者记录特殊的覆盖率信息),你需要写一个插件把它挂载到State上。

Extending the Environment Model

扩展环境模型,模拟操作系统层面的行为。比如你想支持一种新的系统调用(Syscall),或者模拟一个特殊的文件系统行为。

Writing Exploration Techniques

编写探索逻辑,教模拟管理器怎么跳转。默认可能是“深度优先”或“广度优先”,你可以写自己的策略,比如“看到循环就跳出”或者“只走包含特定指令的路径”。

Writing Analyses

编写分析算法,教你如何在angr的框架上开发属于你自己的静态分析工具,并将其集成到project.analyses中。

Adding Support for New Architectures

添加新架构支持,如果你遇到一个angr不支持的冷门CPU架构,这部分教你如何编写对应的架构定义和VEX转换逻辑,让angr能分析它。

文章,vex
angr
angr速通
angrctf
angr应用
原理
官方angr中翻

文末附加内容

评论

  1. linye
    Windows Edge
    5 月前
    2026-1-16 17:42:18

    严肃学习

发送评论 编辑评论


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