花指令

什么是花指令

花指令就是指在程序中完全冗余,不影响程序功能但对逆向工程产生干扰的指令。

花指令没有固定格式,泛指用于干扰逆向工作的无用指令。

花指令常见类型

  • 相互抵消的操作指令

一个常见的函数头是这样的:

push ebp
mov ebp esp
sub esp,0x100

插入一些相互抵消的操作,使得上段代码成这样:

push ebp
pushfd
add esp,0xd
nop
sub esp,0xd
popfd
mov ebp esp
sub esp,0x100
  • 脏字节

有特殊的指令,其字节码是这样的: E8 FF ····
其中E8指令为call, E8 FF的意思是跳转到本指令的第二个字节,也就是说,E8 FF的下一条指令为 FF ····,这种情况下E8这个字节码完全没有作用,但是却扰乱了静态分析,将这个字节码称之为脏字节。

也会存在很多其他的脏字节,其中大多是E8(call)这个字节。
例如这一段代码:

    push ebp
jz addr2
jnz addr2
db 0xE8
add3:
sub esp,0x100
···
add2:
mov ebp,esp
jmp addr3

其中 第二行与第三行这一对相反条件的跳转,之后往往会有花指令

  • 指令替换

用复杂的指令替换简单的指令,往往涉及到栈指针的改变,导致IDA无法正常分析。
这种类型的花指令需要仔细分析每一条指令导致的改变,尤其是栈指针的改变,从而判断出哪一部分是完全无用的部分。

这一类型的花指令经常涉及到call/ret指令的替换,通过改变ESP/EBP,来实现上述两个指令的更换。

这一部分非常推荐的是刚刚过去的VNCTF2023中的confuse_re题目。

在此只粘贴本题目中的花指令部分。

.text:00000000004045D0
.text:00000000004045D0 var_8 = qword ptr -8
.text:00000000004045D0
.text:00000000004045D0 ; __unwind {
.text:00000000004045D0 endbr64
.text:00000000004045D4 push rbp
.text:00000000004045D5 mov rbp, rsp
.text:00000000004045D8 sub rsp, 10h
.text:00000000004045DC mov [rbp+var_8], rdi
.text:00000000004045E0 push rbp
.text:00000000004045E1 call $+5
.text:00000000004045E6
.text:00000000004045E6 loc_4045E6: ; DATA XREF: sub_4045D0+17↓o
.text:00000000004045E6 pop rbp
.text:00000000004045E7 add rbp, (offset loc_4045EE - offset loc_4045E6)
.text:00000000004045EB push rbp
.text:00000000004045EC retn
.text:00000000004045EC sub_4045D0 endp ; sp-analysis failed
.text:00000000004045EC
.text:00000000004045EC ; ---------------------------------------------------------------------------
.text:00000000004045ED db 0E8h
.text:00000000004045EE ; ---------------------------------------------------------------------------
.text:00000000004045EE
.text:00000000004045EE loc_4045EE: ; DATA XREF: sub_4045D0+17↑o
.text:00000000004045EE pop rbp
.text:00000000004045EF mov rax, [rbp-8]
.text:00000000004045F3 mov rdi, rax
.text:00000000004045F6 call sub_403940
.text:00000000004045FB nop
.text:00000000004045FC leave
.text:00000000004045FD retn
.text:00000000004045FD ; } // starts at 4045D0
.text:00000000004045FE

这一花指令从0x4045E0开始,一直到0x4045EE,所实现的功能仅仅只是跳转,或者说无任何功能,全部nop即可。

花指令的手动patch

  • 首先是必备的IDA基本操作,D、C、P快捷键,以及编辑函数范围操作。

本部分的例题是NSSCTF平台上的 jump_by_jump。
(作者非常幸运能够在学习花指令的时候看到一篇简单详细的题解,此题目中的Tigamm的题解是本部分的资料来源)

.text:00411888                 jz      short near ptr loc_41188C+1
.text:0041188A jnz short near ptr loc_41188C+1
.text:0041188C
.text:0041188C loc_41188C: ; CODE XREF: .text:00411888↑j
.text:0041188C ; .text:0041188A↑j
.text:0041188C call near ptr 41BC4932h
.text:00411891 add [ecx+0D8BE045h], cl
.text:00411897 xor al, 7Bh

可以看到前两条指令使得下一条指令应该是从loc_41188C+1开始的,但是IDA已经从41188C开始分析了,因此我们要将41818C的字节给nop掉。

由于作者的IDA没有keypatch插件,因此作者在此介绍通过修改16进制窗口的字节码来实现patch的方法,如果读者有patch插件,可以进行patch。

  • 首先在41188C处按下D键,将其转化为字节码。
    .text:00411888                 jz      short near ptr unk_41188D
    .text:0041188A jnz short near ptr unk_41188D
    .text:0041188A ; ---------------------------------------------------------------------------
    .text:0041188C db 0E8h
    .text:0041188D unk_41188D db 0A1h ; CODE XREF: .text:00411888↑j
    .text:0041188D ; .text:0041188A↑j
    .text:0041188E db 30h ; 0 OFF32 SEGDEF [_rdata,417B30]
    .text:0041188F db 7Bh ; {
    .text:00411890 db 41h ; A
    .text:00411891 ; ---------------------------------------------------------------------------
    .text:00411891 add [ecx+0D8BE045h], cl
    .text:00411897 xor al, 7Bh
  • 在16进制窗口中右键Edit,将E8修改为90,回到反汇编窗口。
    .text:00411888                 jz      short near ptr unk_41188D
    .text:0041188A jnz short near ptr unk_41188D
    .text:0041188A ; ---------------------------------------------------------------------------
    .text:0041188C db 90h
    .text:0041188D unk_41188D db 0A1h ; CODE XREF: .text:00411888↑j
    .text:0041188D ; .text:0041188A↑j
    .text:0041188E db 30h ; 0 OFF32 SEGDEF [_rdata,417B30]
    .text:0041188F db 7Bh ; {
    .text:00411890 db 41h ; A
    .text:00411891 ; ---------------------------------------------------------------------------
    .text:00411891 add [ecx+0D8BE045h], cl
    .text:00411897 xor al, 7Bh
  • 在4118C处按下C键,然后依次在之后的代码也按下C键,知道左边的地址栏中没有黄色的地址为止。
.text:00411888                 jz      short loc_41188D
.text:0041188A jnz short loc_41188D
.text:0041188C nop
.text:0041188D
.text:0041188D loc_41188D: ; CODE XREF: .text:00411888↑j
.text:0041188D ; .text:0041188A↑j
.text:0041188D mov eax, ds:dword_417B30
.text:00411892 mov [ebp-20h], eax
.text:00411895 mov ecx, ds:dword_417B34
.text:0041189B mov [ebp-1Ch], ecx
.text:0041189E mov edx, ds:dword_417B38
.text:004118A4 mov [ebp-18h], edx
.text:004118A7 mov eax, ds:dword_417B3C
.text:004118AC mov [ebp-14h], eax
.text:004118AF mov ecx, ds:dword_417B40
.text:004118B5 mov [ebp-10h], ecx
.text:004118B8 mov dx, ds:word_417B44
.text:004118BF mov [ebp-0Ch], dx
.text:004118C3 mov dword ptr [ebp-2Ch], 0
.text:004118CA jmp short loc_4118D5
  • 最后在函数头位置按下P键,重新定义函数。如此,花指令已经被手动修改成功了,再按下F5按键,即可得到真正的主函数伪代码。
int __cdecl main_0(int argc, const char **argv, const char **envp)
{
int i; // [esp+D0h] [ebp-2Ch]
char v5[28]; // [esp+DCh] [ebp-20h] BYREF

strcpy(v5, "NSSCTF{Jump_b9_jump!}");
for ( i = 0; i < 21; ++i )
v5[i] = (v5[i] + v5[(i * i + 123) % 21]) % 128;
sub_4110CD("%s", (char)v5);
return 0;
}

使用IDApython进行花指令的patch

本部分的例题是NSSCTF平台上的wordy。

.text:0000000000001135 ; int __fastcall main(int, char **, char **)
.text:0000000000001135 main: ; DATA XREF: start+1D↑o
.text:0000000000001135 ; __unwind {
.text:0000000000001135 push rbp
.text:0000000000001136 mov rbp, rsp
.text:0000000000001139 sub rsp, 10h
.text:000000000000113D mov dword ptr [rbp-4], 0
.text:0000000000001144
.text:0000000000001144 loc_1144: ; CODE XREF: .text:loc_1144↑j
.text:0000000000001144 jmp short near ptr loc_1144+1
.text:0000000000001144 ; ---------------------------------------------------------------------------
.text:0000000000001146 dw 0BFC0h
.text:0000000000001148 dq 0FFFEDFE800000068h, 65BFC0FFEBFFh, 0FFEBFFFFFED2E800h
.text:0000000000001148 dq 0C5E80000006CBFC0h, 6CBFC0FFEBFFFFFEh, 0FFFFFEB8E8000000h
.text:0000000000001148 dq 6FBFC0FFEBh, 0C0FFEBFFFFFEABE8h, 0FE9EE800000020BFh
.text:0000000000001148 dq 77BFC0FFEBFFFFh, 0EBFFFFFE91E80000h, 0E80000006FBFC0FFh
.text:0000000000001148 dq 0BFC0FFEBFFFFFE84h, 0FFFE77E800000072h, 6CBFC0FFEBFFh
.text:0000000000001148 dq 0FFEBFFFFFE6AE800h, 5DE800000064BFC0h, 21BFC0FFEBFFFFFEh
.text:0000000000001148 dq 0FFFFFE50E8000000h, 0ABFC0FFEBh, 0C0FFEBFFFFFE43E8h
.text:0000000000001148 dq 0FE36E800000054BFh, 68BFC0FFEBFFFFh, 0EBFFFFFE29E80000h
.text:0000000000001148 dq 0E800000065BFC0FFh, 0BFC0FFEBFFFFFE1Ch, 0FFFE0FE800000072h
.text:0000000000001148 dq 65BFC0FFEBFFh, 0FFEBFFFFFE02E800h, 0F5E800000020BFC0h
.text:0000000000001148 dq 61BFC0FFEBFFFFFDh, 0FFFFFDE8E8000000h, 72BFC0FFEBh
.text:0000000000001148 dq 0C0FFEBFFFFFDDBE8h, 0FDCEE800000065BFh, 20BFC0FFEBFFFFh
.text:0000000000001148 dq 0EBFFFFFDC1E80000h, 0E80000006DBFC0FFh, 0BFC0FFEBFFFFFDB4h

可以看到从jmp命令开始,IDA已经无法自动分析了。
可以看到,这里的JMP指令与前文中调到的E8 FF的call指令有相似之处。
因此我们只需要将EB改成90即可。
但是之后的汇编中同样有相同的花指令,而且非常多,我们可以使用IDApython来对其进行修改。

附上代码

startaddr = 0x001144
endaddr = 0x3100

for i in range(startaddr,endaddr):
if get_wide_byte(i) == 0xEB:
if get_wide_byte(i+1) == 0xFF:
patch_byte(i,0x90)
print("[+] Addr {} is patched".format(hex(i)))

关键的就是两个函数,一个是get_wide_byte()函数,一个是patch_byte()函数。

当然IDA编写脚本时有很多函数。

http://www.s0rry.cn/archives/python-ni-xiang-jiao-ben-bian-xie

可以参考上面这个链接进行学习。