Phase 13: Tools & Protocols

拆卸和控制流量:从字节到图表

拆除列表是关于哪些字节是指令的索赔,这个课程构建了检查索的解码器和图表.

Type: Build

Languages: Python, C, x86-64 assembly

Prerequisites: Phase 13 · 32 (ELF from scratch)

Time: ~150 minutes

学习目标

  • 根据英特尔SDM卷2章2的规则,将本书固定中的x86-64指令子集解码.
  • 证明解码器是否可以使用llvm-objdump -d在每一个建筑物的指示上.
  • 运行线性扫描和递归下降在相同的字节上,并解释每个字节是错误的地方.
  • 切割指令成基本块,并用键字边形构建控制流图.
  • 通过库珀,哈维和肯尼迪的反复算法计算直接主导者,
  • 通过转移的输入,从其表上点的目标中恢复间接跳跃.

问题

你编译了ledger.c在-O0问一个代理人什么grade经纪人打电话给拆卸器,阅读了列表,并回答说,jmpq *%rax没有任何已知的目标.return 0现在,我们要去.

你打开了同一个物体.llvm-objdump -d它们在列表中一直存在. 列表中没有什么说明执行如何达到它们,所以工具把它们视为死码.

列表只能回答一个问题:哪个字节从哪个命令中运行.它不说明哪个命令运行,在哪个顺序或从哪个地方.

电动机6.5.0没有自主拆卸器. 为了进行深入分析,它的 README 要求你带来Ghidra,Hopper或IDA. 对于离线检查, rea还可以调用一个安装的 GNU 文件或Rizin. 它的呼叫图在未解决的目标上停止:"呼叫/呼叫只包含已解决的函数.... 无目标计算流量仍然是未知的. 这一课程建立了这些事实的部分,

指示格式每个字段都有一个规则

图2-1显示每个指令的字节顺序. 首先是可选的预设,然后是1到3个opcode字节,然后是可选的ModRM字节和可选的SIB字节. 随后,一个移动为0,1,2或4字节,然后一个直接的0,1,2或4字节. 整个指令最多为15字节 (SDM 2.3.11). 简单的数据表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表表

每个字段决定了下一个字段意味着什么.因此,一个解码器是一个小决策链,每个决策背后的手册中有一个句子.

预सर्ग分为四组

SDM 2.1.1列出了遗留前置.F0锁和重复前置F2其他F3组2拥有6个段的重点 (2E是%cs集团3是66组4是67现在,我们需要一个解决方案.

编译器发射更多. 排列填充在ledger-x86_64-O0.o在抵消0xf2读书66 66 66 66 66 2e 0f 1f 84 00 00 00 00 00五份复印件66在一排.llvm-objdump打印成nopw %cs:(%rax,%rax)解码器收集了看到的每一个字节前置,然后问了哪些字节有意义.

66对于SSE指令,它是必须的前,选择了opcode,所以66 0f ef c0是pxor %xmm0, %xmm0没有16位的任何东西.F3 0f 6f是movdqu解码器首先查找强制前和第二个字节字节.只有当该对没有输入时才会66意思是16位的操作.

REX 添加了4个位,必须坐下 opcode 旁边

SDM 2.2.1 定义REX为从 个字节中一个字节40为了4F具有形状的0100WRXB现在,我们要去.W选择一个64位操作数. R扩大ModRM监管领域. X扩大SIB指数领域. B扩展ModRM r/m字段,SIB基段或 opcode byte中的注册表 (表2~4). REX必须在 opcode或 escape byte之前出现,而忽略一个 REX 后续由一个遗留前置.

在48 89 e5字节48是 REX 配合W设置. 操作码89是MOV r/m, r其他e5选择%rsp进入%rbp删除REX,然后相同的字节是movl %esp, %ebp现在,我们要去.

随着 REX 的出现,选择编码 4 到 7 个字节.%spl现在%bpl现在%sil其他%dil没有%ah现在%ch现在%dh其他%bh解码器可以保存两个表格.

两张地图中有双码

常见的情况是一个字节的操作代码.0F通过两个字节的地图,0F 38或0F 3A某些opcode使用ModRMreg字段作为另外三个opcode位,写成/digit在手册中.83 /7是cmp具有8位即时的,83 /5是sub其他FF /4是间接的jmp现在,我们要去.

解码器在code/main.py列表包括四个固定物体中的光码以及几个邻居.mov现在lea现在movslq现在movzbl现在imul轮,test其他idiv继续说道push现在pop现在call现在ret十六个条件跳跃,多字节nop其他任何opcode都会增加DecodeError猜测的解码器比停止的更糟糕.

摩德瑞姆和SIB选择操作数

SDM 2.1.3 分为ModRMmod子,子,子,子reg其他地方r/m其他国家mod = 11意思是 r/m 是一个注册表. mod = 00现在01其他10没有移动,8位移动或32位移动的平均内存. r/m = 100意思是一个SIB字节后面.SIB分为 scale其他地方index其他地方base平均水平为100没有指数登记.

在表2-2中,两个特殊行在64位模式下. mod = 00随着r/m = 101没有(%rbp)对于下一个指令,它是32位的移动 (SDM 2.2.1.6).mod = 00具有SIB基础101没有基数注册的32位移动.

接下来8d 44 08 fa其他grade在-O0电脑的代码8D是lea们的们44是01 000 100位:一个8位的移动,reg %eax之后,一个SIB.08是00 001 000: 规模1,指数%rcx基础%rax移动字节fa AT&T 的文字是leal -0x6(%rax,%rcx), %eax计算的score / 10 - 6切换指数

转移是链接编辑写的位置

48 8d 0d 00 00 00 00是leaq (%rip), %rcx转移式对象中,它们是位置持有者. 输入中.rela.text在抵消0x69,,,R_X86_64_PC32.rodata-0x4根据 psABI (表 4.9) 的定义R_X86_64_PC32作为S + A - P:符号地址加上添加式减去字段地址.

处理器将该字段添加到下一个指令的地址,所以最终目标是S + A + (end - P)为了这个lea字段是最后四个字节,所以end - P目标是.rodata + (-4) + 4 =.rodata + 0x0电灯中的每一个RIP相对操作数都包含这个 -4在其添加中,所以.rodata+0x10在-O1 lea平均.rodata+0x14现在,我们要去.

解码器记录每个指令内移动场的偏移. resolve_reference()转换表中的运行函数是转换函数.leaq (%rip), %rcx在"在桌子上.rodata+0x0"我知道.

直接的最大位数为32位,除了一个例外

SDM 2.2.1.5 表示,即时保持在64位模式中最多32位,并且具有扩展信号.MOV r64, imm64(B8+r随着REX.W), AT&T的语法称之为movabsq解码器读取8位即时的字符号为算术 (83 c1 fa是addl $-0x6, %ecx) 和未签名的转班计数和混动选项 (pshufd $0xee这分别与什么相匹配?llvm-objdump他们的指纹.

靠近的树枝,push其他pop默认为64位操作系统,并忽略66(SDM 2.2.1.7) 这就是为什么解码器给它们命名的原因.pushq现在popq现在callq其他retq没有读到一个大小的预写.

通过手动解码一条指令

在你信任代码之前,解码48 69 c0 67 66 66 66其他grade在纸上.

  1. 48现在40为了4F现在,我们要做什么?W = 1演唱器尺寸是64位.
  2. 69是IMUL r, r/m, imm32现在,我们要去.
  3. c0是ModRM11 000 000: 登记表,法令%rax子%rax现在,我们要去.
  4. 接下来的四个字节是直接的,小的:0x66666667现在,我们要去.
  5. 现在,我们要做什么?imulq $0x66666667, %rax, %rax现在,我们要去.

这就是7个字节,llvm-objdump常数是对方的,使得分为10乘以乘法,sarq $0x22在第34课中,你会再次遇到这对,score / 10现在,我们要去.

解码器在Decoder.decode()现在_State.modrm()其他_State.finish()按照顺序阅读,并将每个分支与其文件串中命名的 SDM 部分匹配.

线性扫描

施瓦茨,德布雷和安德鲁斯在"拆除可执行代码复制" (WCRE 2002) 节3.1中描述了线性扫描. 开始于代码节目的第一个字节,解码一个命令,继续在字节之后,并在结束时停止. objdump现在,我们在做什么?llvm-objdump现在,我们要去.

pythondef linear_sweep(code: bytes, start: int = 0, end: int | None = None) -> SweepResult:
    position = start
    while position < end:
        try:
            instruction = decode(code[:end], position)
        except DecodeError:
            undecodable.append((position, code[position]))
            position += 1
            continue
        instructions.append(instruction)
        position = instruction.end

方法有一个失败模式,论文的图1显示. 数据的字节被解码为指令,然后解码器会错过实际指令的边界. 在x86上,解码器通常在几字节内重新同步,这使得错误很难看到. 检测只发生在不有效的操作码上, 这篇论文的结论是, 这些算法"默默失败".

扫描也是这个课程的预言.llvm-objdump -d在每一个命令,每一个命令,llvm-objdump captured 文字生活在code/fixtures/*.objdump.txt测试分析每个行以地址,字节和文字, 之后放弃评论#它们包含86,56,84和11个指令,总共237,并且解码器匹配它们.

复发性下降

同样论文的3.2节称第二种方法为复习穿越.从已知入口点开始,然后解码到控制转移.对于直接跳跃,继续到目标.对于条件分支,继续到目标和下一个指示,并且对于呼叫,继续之后,因为呼叫回来.为了回来,停止.保持一组访问的地址,以便步行结束.

pythonif instruction.flow == "branch":
    worklist.append(instruction.target)
    address = instruction.end
elif instruction.flow == "indirect-jump":
    table = resolver(instruction, instructions) if resolver else None
    if table is None:
        unresolved.append(instruction)
        break

失败模式是相反的.间接跳跃没有字节目标.行走停留在那里,每一个只有通过跳跃可达的块都不知名.论文的图2显示第二种危险:一个目标集来自转移指向指令中部.main.py记录这个案例作为冲突,并阻止了这一路径,所以重叠是可见的,而不是默默解码两次.

安德里斯和其他人测量了981个编译器生成的二进制器上的9个拆卸器 (USENIX Security 2016).直线数据和重叠代码在实际编译器输出中比文献所假设的更少,硬原始是函数边界恢复.请记住这两项发现.下一节的陷在编译器输出中很少.间接跳跃在grade常见的.

片是错误的,下坡是正确的

code/fixtures/sweep-trap.s函数体内有4字节表的24字节手写组件.movzbl (%rcx,%rax), %eax跳过了它.

asmlookup:
        movslq  %edi, %rax
        leaq    .Ltable(%rip), %rcx
        movzbl  (%rcx,%rax), %eax
        jmp     .Ldone
.Ltable:
        .byte   0x31, 0xc0, 0x90, 0x48
.Ldone:
        addl    $1, %eax
        retq

用它建造clang -target x86_64-unknown-linux-gnu -c sweep-trap.s图表的四个字节被选为可行的指示.31 c0是xorl %eax, %eax现在,我们要去.90是nop现在,我们要去.48是一个REX前,所以扫描将它与真实的三个字节相结合addl $1, %eax其他信息addq $0x1, %rax捕获者llvm-objdump输出显示出:

text       e: eb 04                        	jmp	0x14 <lookup+0x14>
      10: 31 c0                        	xorl	%eax, %eax
      12: 90                           	nop
      13: 48 83 c0 01                  	addq	$0x1, %rax
      17: c3                           	retq

任何字节都被解码,每一个字符都是真实的,retq现在,这个名单仍然是错误的,0xe目标0x14列表中没有任何指令从0x14命令是运行在0x14是一个32位的addl列表显示64位addq图表的四个字节显示为代码.

代的回归lookup符号解码了六个指示, 接下来跳到0x14报告addl $0x1, %eax它没有要求任何4个表字节和8个填充字节.plus_one现在,我们要去.trap_report()在main.py打印了比较:

json"linearSweep": {
  "instructions": 9,
  "atJumpTarget": {"starts": "0x13", "text": "addq\t$0x1, %rax", "startsAtTarget": false},
  "claimedDataBytes": 4,
  "claimedPaddingBytes": 8
},
"recursiveDescent": {
  "instructions": 6,
  "atJumpTarget": "addl\t$0x1, %eax",
  "claimedDataBytes": 0,
  "claimedPaddingBytes": 0
}

测试test_sweep_and_objdump_share_the_same_mistake证实这一点llvm-objdump打印addq $0x1, %rax在0x13预言器是线性扫描,继承其盲点.与预言器的协议检查解码器,下降检查命令运行.

基本块

艾伦在"控制流量分析" (1970) 中定义了基本块.它是一个指令序列,只输入在第一条指示时,只留在最后.雅克丹和其他人重新在"No More Gotos" (NDSS 2015) 的II.A.2节中解译,标题节点作为函数输入.

构建是通过两个通过扫描解码的指示,按地址进行排序.第一通过收集领导人.领导者是进入,分支或跳跃目标,桌面目标或任何控制转移后的指示.第二通过在每个领导者和地址序列中的每个空隙中启动一个新的区块.

pythonfor instruction in self.instructions:
    if instruction.address in leaders or current is None or current.end != instruction.address:
        current = Block(instruction.address, [])
        self.blocks[instruction.address] = current
    current.instructions.append(instruction)

边从每个块的最后指示. 返回没有继任者. 一个跳跃有一个,标记jump条件分支有两个,标记taken其他fallthrough单个单个指令在一个区块的最后传递到下一个区块,当下一个指令是分支目标时,这种指令发生.table区块是记录在unresolved现在,我们要去.

另一方面,我们还能看到两个事实.grade在-O0它们的区块是从陷中出现的重叠案例.

控制流量图

total在-O1是31字节和13个指令:

text      40: 85 f6                        	testl	%esi, %esi
      42: 7e 18                        	jle	0x5c <total+0x1c>
      44: 89 f1                        	movl	%esi, %ecx
      46: 31 d2                        	xorl	%edx, %edx
      48: 31 c0                        	xorl	%eax, %eax
      4a: 66 0f 1f 44 00 00            	nopw	(%rax,%rax)
      50: 03 04 97                     	addl	(%rdi,%rdx,4), %eax
      53: 48 ff c2                     	incq	%rdx
      56: 48 39 d1                     	cmpq	%rdx, %rcx
      59: 75 f5                        	jne	0x50 <total+0x10>
      5b: c3                           	retq
      5c: 31 c0                        	xorl	%eax, %eax
      5e: c3                           	retq

领导人是0x40其他地方0x44(在jle), 0x50其他国家jne), 0x5b(在jne),以及0x5c其他国家jle五个街区,五个边缘:

FromToKindWhy
0x400x44fallthroughjle not taken, count is positive
0x400x5ctakencount is zero or negative, return 0
0x440x50fallthroughthe nopw is alignment inside the block, execution passes through it
0x500x50takenjne 0x50, the loop
0x500x5bfallthroughloop exit, return the sum

六字节nopw在0x4a圈的方向是的0x50它们可以访问,所以它们属于区块.0x44单字节nop在0x5f在最后一次之后retq扫描功能就会造成第六个块,unreachable()列出了它.

在-O2并且在一个函数中,它具有两个循环的 140 字节.0x80通过时添加8个值paddd的残留量在0xc0 CFG 有两个后边,0x80 -> 0x80其他0xc0 -> 0xc0测试套房检查了两者.

具有反复算法的统治者

一个街区d控制一个区块b每个路由从入口到b通过d直接统治者b是主导者b它们最接近b概念可以追溯到普罗塞 (1959) 到艾伦 (1970),if其他while没有错.

库珀,哈维和肯尼迪写了"简单,快速的统治算法" (2001). 他们认为,数据流式,以紧的表示运行在反向后序,超过了伦加瓦尔-塔尔扬 (1979) 在大约一千个节点下的图表. 他们的论文说,它的目的是"不提出新的算法". 它表明,简单的算法可以在实践中取得胜利, 函数CFG几乎从来没有一千个节点.

pythondef intersect(first: int, second: int) -> int:
    while first != second:
        while number[first] > number[second]:
            first = idom[first]
        while number[second] > number[first]:
            second = idom[second]
    return first

changed = True
while changed:
    changed = False
    for start in order[1:]:
        processed = [p for p in self.predecessors(start) if p in idom]
        candidate = processed[0]
        for other in processed[1:]:
            candidate = intersect(other, candidate)
        if idom.get(start) != candidate:
            idom[start] = candidate
            changed = True

number按每块的反向后排位置,输入为 0. intersect报纸写出了与后排数的比较,<这代码使用反顺序后号码,所以比较是>其他一切都遵循报纸的伪码.

为了total在-O1结果是idom(0x44) = 0x40现在idom(0x50) = 0x44现在idom(0x5b) = 0x50其他idom(0x5c) = 0x40边缘0x50 -> 0x50后边是循环的标志,因为其目标主导其源头.back_edges()检测结果test_dominators_on_a_hand_built_diamond_with_a_loop运行相同的代码在16字节的序列上,intersect他看到两个前任.

跳板表在 -O0级

在-O0编译了switch在grade对于位置独立的跳表语法:

text      5d: 83 e8 04                     	subl	$0x4, %eax
      60: 77 38                        	ja	0x9a <grade+0x6a>
      62: 48 8b 45 f0                  	movq	-0x10(%rbp), %rax
      66: 48 8d 0d 00 00 00 00         	leaq	(%rip), %rcx
		0000000000000069:  R_X86_64_PC32	.rodata-0x4
      6d: 48 63 04 81                  	movslq	(%rcx,%rax,4), %rax
      71: 48 01 c8                     	addq	%rcx, %rax
      74: ff e0                        	jmpq	*%rax

阅读它作为四个事实.subl $0x4接着是ja是一个限制检查:超过4的指数进入默认情况下0x9a现在,我们可以看到一个图表,leaq随着其迁移,将表地址放入%rcx之前的部分显示了该地址是.rodata+0x0现在,我们要去.movslq addq增加表基,因此每个输入是表到其盒子区块的距离.

在对象文件中,每个表字节为零.目标只存在于.rela.rodata其他:

textRELOCATION RECORDS FOR [.rodata]:
OFFSET           TYPE                     VALUE
0000000000000000 R_X86_64_PC32            .text+0x91
0000000000000004 R_X86_64_PC32            .text+0x8c
0000000000000008 R_X86_64_PC32            .text+0x87
000000000000000c R_X86_64_PC32            .text+0x82
0000000000000010 R_X86_64_PC32            .text+0x86

申请S + A - P进入k在抵消4k存储值为.text + A - (.rodata + 4k)代码还添加了.rodata + 0目标是:.text + A - 4k输入0是0x91 - 0 = 0x91输入1是0x8c - 4 = 0x88进口2是0x87 - 8 = 0x7f条目3和4是0x82 - 12 = 0x76其他0x86 - 16 = 0x76根据第6例,第7例,第8例,第9例和第10例,score / 10按一行,包括9和10的共享案例.

recover_table()现在,我们可以做这个算法.jump_table_resolver()合了语的反向jmpq它找到了addq那些人是"movslq子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子leaq创造了基础,和边界.ja随着下降,将四个不同的目标推进,并继续. 链接的执行式表中有最终值,没有转移. 同样的函数然后读取签署的32位输入,并添加表地址,evidence记录了目标的路径.

关于grade在-O0根据python3 code/main.py cfg code/fixtures/ledger-x86_64-O0.o grade其他:

MethodInstructionsBlocksEdgesBytesWhat is wrong
linear sweep3497118 code + 10 padding5 unreachable blocks, no edge into the four case blocks, padding decoded as nopw
recursive descent253282 code36 code bytes never reached, jmpq *%rax at 0x74 unresolved
descent with the table33811118 codenothing

随着桌子的统治者树完成了.idom每个案例的区块是0x62起的路线.idom其他0xa1是一个入口0x30由于从 0x9a达到0xa1没有过来.0x62现在,我们要去.

优化器对 -O1的评分做了什么

你会看到grade字节说出一些更具体的东西.-O1其他-O2没有间接跳进grade任何:

text      2c: 83 f9 04                     	cmpl	$0x4, %ecx
      2f: 77 0c                        	ja	0x3d <grade+0x2d>
      31: 89 c8                        	movl	%ecx, %eax
      33: 48 8d 0d 00 00 00 00         	leaq	(%rip), %rcx
		0000000000000036:  R_X86_64_PC32	.rodata+0x10
      3a: 8b 04 81                     	movl	(%rcx,%rax,4), %eax
      3d: c3                           	retq

移动补充0x10加上通常的4将桌子放在.rodata+0x14负载很平坦movl现在,我们可以看到01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00 04 00 00 00编译器将五个案例块取代为五个返回值的表. 有一个迁移,一个表,和一个边界检查.控制流量是三个区块和三个边缘,一个条件分支. find_value_tables()报告为value-table包含条目[1, 2, 3, 4, 4]其他evidence: "bytes"现在,我们要去.

grade_label在-O2们的们,labels列五个字符串指针成为一个五个32位的偏移表.rodata+0x0每个条目都包含一个R_X86_64_PC32函数将表地址添加到它加载的输入中. recover_table()读出五个搬迁和退款抵消0, 2, 4, 6, 8进入.rodata.str1.1它们可以保持F现在D现在C现在B其他A复制跳转表的算法还原了相对数据表,因为这两个都是S + A - P在已知的抵消中.

两个构建中的教训是命名字节显示的内容.一个跳转表改变控制流图.一个值表改变负载.称两个"开关"隐藏了教训34必须重建的差异.

雷亚和吉德拉给你什么

rea 6.5.0 与Ghidra 12.1.4提供商返回函数的列表文本procedure_assembly像一个弦一样.inspect_native_instruction返回一个指令的字节,长度,解码文本,操作符号,输入的引用和流量,返回是从p代码中分类的. xrefs返回从Ghidra的排序的唯一源地址ReferenceManager.getReferencesTo现在,我们要去.analyze_function携带CFG和body对于该函数.procedure_info报告指出, "空白和共享尾巴不应被视为拥有字节".

实际分析的分析标签是"Ghidra的复制性下降"以及其分析器,instruction_flow_evidence现在function_references其他jump_table_evidence结果因此命名了它产生的规则.它不能做的就是展示其工作.当Ghidra不找到表时,流量是"未知的",并且对调用者,调用者和CFG缺乏案例块.

你现在有工作了.python3 code/main.py cfg在同一对象上,比较区块开始,边缘计算,以及恢复表与提供商返回的.如果他们同意,你有两个独立的衍生.如果它们不同,你有一个特定的地址,看看,这是什么课35记录为未知的.

编码器作为程序的说明.Capstone 6.0.0-Alpha10 (2026-07-21) 固定的离界限在CVE-2026-55893和CVE-2026-55894下读取和写入,以及5.0.9 (2026-05-28) 后载的CVE修正.一个编码器读取未写的字节.15字节限制,切割检查在_Cursor.byte()拒绝猜测一个未知的保护代码main.py由于它不是为什么建造的.

建立它

运行演示:

bashcd phases/13-tools-and-protocols/33-disassembly-and-control-flow
python3 code/main.py

输出是一个JSON报告.对于每个固定对象,它给出了预言协议.对于每个函数,它给出了指令,区块和边缘计算在三个方法下.它还列出无法达到的区块,未解决的间接跳跃,后边缘,直接主导者和任何表.它结束了陷比较.

预期的形状:

json{
  "fixtures": {
    "ledger-x86_64-O0.o": {
      "oracle": "86/86 instructions agree with llvm-objdump",
      "functions": {
        "grade": {
          "range": "0x30-0xa6",
          "paddingBytes": 10,
          "linearSweep": {"instructions": 34, "blocks": 9, "edges": 7, "unreachableBlocks": ["0x76", "0x7f", "0x88", "0x91", "0xa6"], "...": "..."},
          "recursiveDescent": {"instructions": 25, "blocks": 3, "edges": 2, "unresolvedIndirectJumps": ["0x74"], "...": "..."},
          "recursiveDescentWithTables": {"instructions": 33, "blocks": 8, "edges": 11, "...": "..."},
          "tables": [{"kind": "jump-table", "table": ".rodata+0x0", "count": 5, "entries": ["0x91", "0x88", "0x7f", "0x76", "0x76"], "evidence": "relocation", "...": "..."}]
        }
      }
    }
  },
  "sweepTrap": {"...": "..."}
}

阅读code/main.py在此顺序:

  1. ElfObject阅读部分标题,符号,.rela.*报名,以及symbolize()给路上的地址.llvm-objdump没有.
  2. Decoder.decode()读取前置和REX,然后在一字节或两字节的光码地图上发送.
  3. _State.modrm()读取ModRM,SIB和移动,并记录移动场的偏移.
  4. resolve_reference()适用S + A - P转移到一个流离失所或即时的场地.
  5. linear_sweep()其他recursive_descent()两种方法,每一种都记录了其故障.
  6. recover_table()其他jump_table_resolver()通过其移动或字节来阅读表.
  7. ControlFlowGraph构建领导者,块和打字边缘,dominators()它们是反复算法.
  8. analyze_function()运行每个方法在一个函数上compare_with_oracle()检查解码器.

另外还有两个命令.python3 code/main.py disasm FILE.o印出每一个具有转移目标的指令后的列表. python3 code/main.py cfg FILE.o FUNCTION打印一个函数的分析.

用它

在新物体上使用这三个顺序的方法.

  1. 首先,运行神经比较.llvm-objdump在一个简单的命令上有一个错误,每一个后来的结构都继承了它.
  2. 运行从每个函数符号中递归下降,然后读取unresolved每个进口都是一个间接的跳跃或呼叫,
  3. 对于每一次未解决的跳跃,请查找表格语和指向它的移动.如果语法不匹配,请在跳跃前阅读字节,并写下你所看到的.这笔记是第35课中的第一个未知.
  4. 扫描和下降字节的比较字节.只有扫描要求的字节是填充或数据.没有要求的字节是不可到达的或属于你没有找到的函数.
  5. 通过恢复的表格,从下降中构建CFG,然后阅读后边.每个边都是一个循环,课34将变成while或for现在,我们要去.

随着这些物体的保存,当你转移到新的编译版本时,重新捕获它们,因为改变列表是改变的固定.

互动实验室

实验室A:在纸上解码,然后检查

关于code开放字符串:

bashcd phases/13-tools-and-protocols/33-disassembly-and-control-flow/code
python3 -q

运行:

pythonfrom main import *
decode(bytes.fromhex("48 8d 0d 00 00 00 00"), 0).text
decode(bytes.fromhex("8d 44 08 fa"), 0).text
decode(bytes.fromhex("66 66 66 66 66 2e 0f 1f 84 00 00 00 00 00"), 0).length

在每次电话之前,写下你预期的内容.48试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试试decode(bytes.fromhex("0f 0a"), 0)阅读DecodeError现在,我们要去.

实验室B:搬迁在一个地方

pythonelf = load_fixture("ledger-x86_64-O0.o")
code = elf.section(".text").data
lea = decode(code, 0x66)
lea.displacement_offset
resolve_reference(elf, lea)

参考文献中说.rodata+0x0改变地址为0x116在grade_label阅读参考文献.data然后解码了callq在0x10e现在的场地带着R_X86_64_PLT32 grade-0x4解决目标是0x30现在,grade现在,我们要去.

实验室C:观察两个方法不同意

pythontrap = load_fixture("sweep-trap.o")
text = trap.section(".text").data
[i.text for i in linear_sweep(text, 0, 0x20).instructions]
[i.text for i in recursive_descent(text, [0], 0x20).sorted_instructions()]

计算每个列表中的指令,然后找到从0x13然后将最后一个表字节换成sweep-trap.s其他0x48为了0xe8现在扫描吞了addl其他retq进入一个callq函数似乎没有返回.

实验室D:手动控制者

pythonanalysis = analyze_function(load_fixture("ledger-x86_64-O1.o"), "total")
analysis.cfg.edges
analysis.cfg.reverse_postorder()
analysis.cfg.dominators()
analysis.cfg.back_edges()

在看结果之前,绘制五个块在纸上,计算统治者.然后运行相同的四行.grade在-O0解释为什么idom(0xa1)是0x30没有0x62现在,我们要去.

实践实验室

增加间接调用地点,扩大分析.

要求:

  • 写一个C函数,通过一个从小数组中选出的函数指针调用.-O0其他-O1它们的物体与它们的物体相结合.llvm-objdump给我一个短信.
  • 让解码器与两个对象的 Oracle 一致.只需要在设备中的指令时添加光码.
  • 延长时间recursive_descent()因此,间接调用记录了调用地点.calls随着None作为目标,并继续追逐它.
  • 写一个解析器,通过其移动读取函数指针阵列并报告可能的调用.
  • 添加调用设置到函数报告中,如 indirectCalls根据每个目标的证据.
  • 写一个测试,当解决器返回非函数符号的目标时,它会失败.

成功意味着python3 code/main.py cfg给每一个可能的电话,以及证明的移动,

教训艺术品

这一课包括outputs/skill-disassembly-and-control-flow.md通过它将对象和函数名称转化为检查列表,区块,打字边缘,主导器和任何未解决的转移.它需要预言比较,拆除方法,字节会计,每个恢复表的迁移证据以及未知的情况的声明.

检查

运行演示和确定性套件:

bashcd phases/13-tools-and-protocols/33-disassembly-and-control-flow
python3 code/main.py
python3 code/tests/test_main.py

验证应证明:

  • 解码器同意llvm-objdump -d在四个固定物体的所有237条指示上
  • REX.W选择64位操作符,REX.R和REX.B扩展注册表的字段
  • 模特RM和SIB将AT&T内存操作系统,包括规模,索引和签署的移动表现
  • 记录一个与RIP相对的移动,其在指示内的场地偏移
  • 复制的遗产前置和一个段落过失解码,像 Oracle
  • 一项强制性66预写选择SSE的选号码,然后将32位的登记器留下
  • 八位即时的符号为算术扩展,而非为转换数量
  • 命令超过15字节,命令缩短,以及未知的opcode升级DecodeError
  • R_X86_64_PC32在一个lea通过会议,R_X86_64_PLT32在一个call解决了这个问题
  • 扫描将陷表解码,没有指令对跳跃目标
  • 降落者从来没有要求桌子或填充,并且读addl $0x1, %eax在目标
  • llvm-objdump犯了像搜索陷一样的错误
  • 命令中输入的内容被记录为冲突
  • 没有解决的间接跳跃阻止了下降,并列出
  • total在-O1五个块,五个字符边,一个后边,以及预期的直接主导者
  • 一个手工造型的钻石,带有循环,给了统治者定义的算法
  • total在-O2有两个循环
  • grade在-O0通过出一个5个进口跳板.rela.rodata具有目标0x91, 0x88, 0x7f, 0x76, 0x76
  • 没有桌子grade在-O0有三块块和一个未解决的跳跃
  • grade在-O1其他-O2是一个值表[1, 2, 3, 4, 4]没有间接跳跃
  • grade_label在-O2通过五次移动读取相对串表
  • 互动图像包含分析产生的相同数据

生产失败模式

FailureWhat the weak tool reportsWhat the analysis must prove
Data inside .textA plausible listing with every byte decodedThe descent claims no data byte and the jump target starts an instruction
Padding after the last returnA nopw instruction at the end of the functionThe bytes are unreachable from the entry
Indirect jump through a table"jmp rax" and four dead blocksTable address from the lea relocation, entry count from the bounds check, targets from .rela.rodata
Switch turned into a value table"a jump table with a relocation"A movl load from .rodata+0x14, three blocks, no indirect transfer
Call with a zero displacementcallq 0x113, the next instructionR_X86_64_PLT32 grade-0x4 resolves to 0x30
Decoder guesses an unknown opcodeA mnemonic that looks rightDecodeError with the address, and the sweep records the byte
Instruction longer than 15 bytesA very long "instruction"The cursor raises before reading byte 16
Branch into the middle of an instructionTwo overlapping listingsA conflict record with both addresses
Loop reported as straight-line codeNo while in the pseudocodeA back edge whose target dominates its source
Oracle captured from a different compilerTests pass on stale textThe fixture README names the exact compiler and command

运营规则

根据预言器进行解码,从已知输入中进行步行控制流动,并将每个字节作为索赔,填充,数据或无法达到的帐户. 从一个移动或可以指向的表中恢复每个间接目标.

进一步阅读

This free lesson is part of the AI Engineering from Scratch curriculum. Read the full explanation, run the lesson code, and verify the result in the interactive reader or from the repository source.

Browse the complete course catalog or open this lesson on GitHub.