花指令

简介

花指令(junk code)是一种专门用来迷惑反编译器的指令片段,这些指令片段不会影响程序的原有功能,但会使得反汇编器的结果出现偏差,从而使破解者分析失败。比较经典的花指令技巧有利用 jmpcallret 指令改变执行流,从而使得反汇编器解析出与运行时不相符的错误代码。

例题:N1CTF2020 - oflo

逆向分析

惯例拖入 IDA,发现 main() 函数无法被反编译,查看汇编代码发现在 0x400BB1 处存在一个 原地 jmp 使得反汇编出错:

.text:0000000000400B54 ; int __fastcall main(int, char **, char **)
.text:0000000000400B54 main:                                   ; DATA XREF: start+1D↑o
.text:0000000000400B54                                         ; .text:0000000000400C21↓o
.text:0000000000400B54 ; __unwind {
.text:0000000000400B54                 push    rbp
.text:0000000000400B55                 mov     rbp, rsp
.text:0000000000400B58                 sub     rsp, 240h
.text:0000000000400B5F                 mov     rax, fs:28h
.text:0000000000400B68                 mov     [rbp-8], rax
.text:0000000000400B6C                 xor     eax, eax
.text:0000000000400B6E                 lea     rdx, [rbp-210h]
.text:0000000000400B75                 mov     eax, 0
.text:0000000000400B7A                 mov     ecx, 40h ; '@'
.text:0000000000400B7F                 mov     rdi, rdx
.text:0000000000400B82                 rep stosq
.text:0000000000400B85                 mov     qword ptr [rbp-230h], 0
.text:0000000000400B90                 mov     qword ptr [rbp-228h], 0
.text:0000000000400B9B                 mov     qword ptr [rbp-220h], 0
.text:0000000000400BA6                 mov     qword ptr [rbp-218h], 0
.text:0000000000400BB1
.text:0000000000400BB1 loc_400BB1:                             ; CODE XREF: .text:loc_400BB1↑j
.text:0000000000400BB1                 jmp     short near ptr loc_400BB1+1
.text:0000000000400BB3 ; ---------------------------------------------------------------------------
.text:0000000000400BB3                 ror     byte ptr [rax-70h], 90h
.text:0000000000400BB7                 call    loc_400BBF
.text:0000000000400BB7 ; ---------------------------------------------------------------------------
.text:0000000000400BBC                 db 0E8h, 0EBh, 12h
.text:0000000000400BBF ; ---------------------------------------------------------------------------

0x400BB1 处第一个字节改为 0x90nop),继续进行反汇编,接下来来到一个奇怪的调用:

call 指令会将下一条指令的地址(0x400BBC)压入栈上,而在代码片段 0x400BBF 中返回地址会被从栈上弹出,值被加一后又压回栈上并 retn,因此这里实际的执行流从 0x400BBD 开始。因此这里我们可以将 call 指令与代码片段 0x400BBF 都 patch 为 nop

patch 后的逻辑比较简单,就是跳到 0x400BD1,这里会调用 sub_4008B9() 后直接 exit()

此时 main() 函数还是无法 F5 ,我们继续向下看是否还存在花指令,发现在 0x400CB5 处存在一个和前面一样的混淆:

直接 patch 为 nop

获得一个到 0x400CCF 的跳转:

继续向下看,在 0x400D04 又发现一个非常经典的花指令,还是直接将第一个字节 patch 为 nop 即可:

现在看起来就是一个非常正常的函数末尾了:

现在我们回到 main 的开头,p 一下重新建立函数,之后就可以正常 F5 反编译了,main() 逻辑如下:

  • 首先调用 sub_4008B9()

  • 接下来从输入读取 19 字节

  • 调用 mprotect() 修改 main & 0xFFFFC000 处权限为 r | w | x,由于权限控制粒度为内存页,因此这里实际上会修改一整张内存页的权限

  • 修改 sub_400A69() 开头的 10 个字节

  • 调用 sub_400A69() 检查 flag

sub_4008B9() 会调用 fork() 分出父子进程,其中子进程会请求父进程调试执行 /bin/cat /proc/version

父进程以单个系统调用作为步长进行单步调试,这里的 PTRACE_PEEKUSER 会根据提供的偏移值取出子进程对应寄存器的值,偏移值与寄存器间关系参见内核源码 /arch/x86/include/asm/user_64.h 中的 user_regs_struct 结构体.

这里偏移 120 取的是 orig_rax,即**系统调用号,也就是说直到系统调用号为 1(即 write)时父进程才会进入核心逻辑:获取 rsi (偏移 104)与 rdx (偏移 96)并调用 sub_4007D1()

sub_4007D1() 比较简单,主要就是将子进程调用 write() 的输出结果拷贝回父进程:

接下来我们回到 main() 中,由于 sub_400A69() 会在运行时被修改因此我们需要将修改结果直接应用到 IDA 中以获得正确的反汇编结果,其会取 flag 的前 5 字节与代码段进行运算,而 flag 的前 5 字节恒定为 n1ctf,因此我们这样修复 sub_400A69()

sub_400A69() 当中还存在一个伪花指令,这里直接将 0x400AC4jmp 给 patch 为 nop 即可:

0x400B0E 处还是存在一处和前面一样的花指令,继续 patch:

接下来就能正常地反编译 sub_400A69() 了,核心逻辑其实非常简单,需要注意的是在 main() 中传入的 flag 从 n1ctf 往后开始:

求解

由于 /proc/version 的前 14 字节恒定为 "Linux version " ,因此我们很容易便能得到 flag 内容:

Last updated