参考链接:
https://blog.csdn.net/m0_51246873/article/details/127167749
https://www.cnblogs.com/YenKoc/p/14136012.html
https://www.xjx100.cn/news/40167.html?action=onClick
https://mp.weixin.qq.com/s/MUth1Qw-Fl2a5OrLw_2_0g
背景
- 线性扫描算法:逐行反汇编(无法将数据和内容进行区分)
- 递归下降算法:根据一条指令是否被另一条指令引用来决定是否对其进行反汇编(难以准确定位)
正是因为这两种反汇编的规格和缺陷机制,所以才导致了会有花指令的诞生
花指令简单的说就是在代码中混入一些垃圾数据阻碍你的静态分析
常见指令
- 0xE8 call + 4字节偏移地址
- 0xE9 jmp + 4字节偏移地址
- 0xEB jmp + 2字节偏移地址
- 0xFF15 call + 4字节地址
- 0xFF25 jmp + 4字节地址
- 0xcc int 3
- 0xe2 loop
- 0x0f84 jz
- 0x0f85 jnz
指令也不一定唯一,比如上面有两种表示call的方式,0x74也能表示jz
常规
1.简单jmp
OD能被骗过去,但是因为ida主要采用的是递归扫描的办法(会用线性扫描补充)所以能够正常识别
2.jx+jnx(x可为e,z,l)
jnz实际上是fake的,因为jz这个指令,让ida认为jz下面的是另外一个分支,所以这里将jz下面包括jz 全转化为代码
call指令按u,下一行按c,再nop call,把90转为数据,再按c变为nop
3.call +add esp,4或call + add [esp], n + retn
2023.7.8凌武杯flower_tea
这里call指令,其实本质就是jmp&push 下一条指令的地址,但是这里其实就是一个jmp指令,push这条指令是多余的,需要add esp,4 调整堆栈,但是ida会默认把call 后面的那个地址当成一个函数
易语言自带的花指令
1 2 3 4
| 004010BF . E8 00000000 call 1111.004010C4 004010C4 /$ 830424 06 add dword ptr ss:[esp],0x6 004010C8 \. C3 retn 004010C9 B9 db B9
|
可以看到这里的add,是将call 00h的返回的地址,也就是[esp]+0x6(add 的四个字节+C3 B9)
来实现一个跳转,到004010CA,对于这种,只需要将下面的特征码patch掉就可以了
4.jmp XXX(红色)
这也是一种非常常见的花指令,其实这里很明显就有问题,因为虚拟地址怎么可能那么大,我们来看一下花指令源代码
实际是e9在搞鬼,ida会默认将e9后面的4个字节当成地址,只要nop掉jmp(E9)就好了
题目练习:https://www.nssctf.cn/note/set/2970
5.stx/jx
clc是清除EFlags寄存器的carry位的标志,而jnb是根据cf==0时跳转的,然而jnb这个分支指令,ida又将后面的部分认作成了另外的分支。
6.汇编指令共用opcode
inc eax和dec eax抵消影响,这种共用opcode确实比较麻烦
创意
1.替换ret指令
call指令的本质:push 函数返回地址然后jmp 函数地址
ret指令的本质: pop eip
所以我们可以在call指令之后,明白函数返回地址存放于esp,可以将值取出,用跳转指令跳转到该地址,即可代替ret指令
2.控制标志寄存器跳转
这一部分需要精通标志寄存器,每一个操作码都会对相应的标志寄存器产生相应的影响,如果对标志寄存器足够熟练,就可以使用对应的跳转指令构造永恒跳转。
3.利用函数返回确定值
有些函数返回值是确定的,比如我们自己写的函数,返回值可以是任意非零整数;如果我们故意传入一个不存在的模块名称,那么他就会返回一个确定的值NULL;另一方面,某些api函数,我们既然使用他,肯定就是一定要调用成功的,而这些api函数基本上只要调用成功就就会返回一个确定的零或者非零值,如MessageBox。这些都可以构造永恒跳转
4.针对反编译
0x1165 开始的花指令和前面的花指令原来相似,这条花指令会使 IDA 误以为 0x116B 处的指令可能会执行,导致 IDA 的栈分析出现错误
修复方法除了patch 外还有修改 ida 对栈的分析结果
在Options - General菜单中勾上Stack pointer选项可以查看每行指令执行之前的栈帧大小
Alt + K 可以修改某条指令对栈指针的影响,从而消除这条花指令对反编译的影响。
清除
nop单字节(E8)
在0x401051设置为数据类型(快捷键D),将call 转成硬编码 E8 再将光标放到 db 0E8上 将E8改成 nop(90) 再次按C键(转化为代码类型)点yes 将硬编码修复成代码
然后向下逐⼀修复 将光标放置在黄色的行上 按C修复 直到没有黄色地址
最后全选函数,按P生成函数
练习题目:https://www.nssctf.cn/problem/2313
nop跨越汇编指令
jz指令指向下一条指令中间
这个时候让jz正常分析,也就是把中间的nop
如果后面有数据没被分析为code,需要重新分析一下
nop部分汇编
一般去菜单中的编辑选项修补单字节会比较好把控
像下面这种红色标志离原函数有一定距离又是call+retn组合,400f64又没什么用,还有ret会干扰函数的分析,那就都nop,是一种暴力破解方法
nop完后看到有%lld,删除函数,修补函数即可反编译
xchg很少用到,后面还有retn,主打不想要的直接全部nop
先可以小范围的尝试,把call到红色的地方先nop,发现不行,再从头把关键数据之前(D7,flag is上面那一行)也给nop掉,发现可以了
根据汇编指令
这里汇编语句之间突然有db有点可疑,而48表示mov,一般是开头的机器码
这里把0x12以下的重新分析,就可以得到key
代码自动去花
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| static main(){ auto addr_start =0x00415990;//函数起始地址 auto addr_end = 0x00416048;//函数结束地址 auto i=0,j=0; for(i=addr_start;i<addr_end;i++){ if(Dword(i) == 0x1E8){ for(j=0 ; j<6; j++,i++ ){ PatchByte(i,0x90); } i=i+4; for(j=0 ; j<3; j++,i++ ){ PatchByte(i,0x90); } i=i+10; for(j=0 ; j<3; j++,i++ ){ PatchByte(i,0x90); } i=i+5; for(j=0 ; j<1; j++,i++ ){ PatchByte(i,0x90); } i=i+3; for(j=0 ; j<2; j++,i++ ){ PatchByte(i,0x90); } i--; } } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| from ida_bytes import get_bytes,patch_bytes start= 0x401000 end = 0x422000 buf = get_bytes(start,end-start)
def patch_at(p,ln): global buf buf = buf[:p]+b"\x90"*ln+buf[p+ln:]
fake_jcc=[] for opcode in range(0x70,0x7f,2): pattern = chr(opcode)+"\x03"+chr(opcode|1)+"\x01" fake_jcc.append(pattern.encode()) pattern = chr(opcode|1)+"\x03"+chr(opcode)+"\x01" fake_jcc.append(pattern.encode())
print(fake_jcc) for pattern in fake_jcc: p = buf.find(pattern) while p != -1: patch_at(p,5) p = buf.find(pattern,p+1)
patch_bytes(start,buf) print("Done")
|
去常见花指令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| import idc import ida_bytes import keystone import capstone
def set_x86(): global ks, cs ks = keystone.Ks(keystone.KS_ARCH_X86, keystone.KS_MODE_32) cs = capstone.Cs(capstone.CS_ARCH_X86, capstone.CS_MODE_32)
set_x86()
def asm(code, addr=0): return bytes(ks.asm(code, addr)[0])
def disasm(code, addr=0): for i in cs.disasm(code, addr): return ('%s %s' %(i.mnemonic, i.op_str))
def MakeCode(ea): return idc.create_insn(ea)
def disasm_at(addr): size = MakeCode(addr) code = idc.get_bytes(addr, size) return addr + size, disasm(code, addr)
def disasm_block(start, end): codes = [] addr = start while addr < end: _addr, code = disasm_at(addr) codes.append((addr, code)) addr = _addr return codes
addr = 0x4010b0 end = 0x402e41 while addr < end: ida_bytes.del_items(addr, 0, 10) size = MakeCode(addr) assert size, hex(addr) if size == 1: if ida_bytes.get_bytes(addr, 3) == b'\xf9\x72\x01': ida_bytes.patch_bytes(addr, b'\x90' * 4) elif ida_bytes.get_bytes(addr, 3) == b'\xf8\x73\x01': ida_bytes.patch_bytes(addr, b'\x90' * 4) else: addr += size elif size == 2 and ida_bytes.get_bytes(addr, 2) == b'\xeb\x01': ida_bytes.patch_bytes(addr, b'\x90' * 3) elif size == 5: if ida_bytes.get_bytes(addr, 10) == b'\xe8\x00\x00\x00\x00\x83\x04\x24\x06\xc3': ida_bytes.patch_bytes(addr, b'\x90' * 11) else: addr += size else: addr += size
print('Done.')
|
- 定义了一个名为f的函数,用于执行二进制搜索并对匹配的位置进行nop
- begin_addr是搜索开始的地址。
- end_addr是搜索结束的地址。
- hexStr是用于搜索的十六进制模式字符串(用空格分开,其中??表示通配符,注意不可以使用诸如2?这样的情况)
- 接着,将hexStr转换为两个字节数组:bMask和bPattern。bMask是用于表示可变字节的,其中00表示需要匹配的字节,01表示不需要匹配的字节;bPattern则是用于搜索的固定字节序列
- 定义了一个signs变量,用于指定搜索的标志位,其中BIN_SEARCH_FORWARD表示向前搜索,BIN_SEARCH_NOBREAK表示不允许搜索中途中断,BIN_SEARCH_NOSHOW表示不显示搜索结果
- 接着使用ida_bytes.bin_search函数进行二进制搜索,从begin_addr到end_addr之间搜索bPattern,其中可变字节由bMask指定。搜索到匹配的位置后,将其打印出来,并调用patch函数对其进行补丁操作
- 最后更新begin_addr为下一个搜索的起始地址,通常为当前匹配位置加上一个偏移量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import ida_bytes import ida_ida def patch(ea,num=1): for i in range(num): ida_bytes.patch_byte(ea+i,0x90) return def f(begin_addr,end_addr,hexStr) xx=(len(hexStr)-1)//2 bMask = bytes.fromhex(hexStr.replace('00', '01').replace('??', '00')) bPattern = bytes.fromhex(hexStr.replace('??', '00')) signs=ida_bytes.BIN_SEARCH_FORWARD| ida_bytes.BIN_SEARCH_NOBREAK| ida_bytes.BIN_SEARCH_NOSHOW while begin_addr<end_addr: ea=ida_bytes.bin_search(begin_addr,end_addr,bPattern,bMask,1,signs) if ea == ida_idaapi.BADADDR: break else: print(hex(ea)) patch(ea,xx) begin_addr=ea+xx f(0x0,0x1000,"?? ?? 00 00 00 ??")
|
分析main()函数可以看到是杂乱的字节,观察0x1144可以发现,存在着jmp db1这种类型的花指令,因此可以写一个idapython脚本来解决
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import ida_bytes import ida_ida def patch(ea,num=1): for i in range(num): ida_bytes.patch_byte(ea+i,0x90) return print("-----") hexStr="EB FF C0 BF ?? 00 00 00 E8" bMask = bytes.fromhex(hexStr.replace('00', '01').replace('??', '00')) bPattern = bytes.fromhex(hexStr.replace('??', '00')) signs=ida_bytes.BIN_SEARCH_FORWARD| ida_bytes.BIN_SEARCH_NOBREAK| ida_bytes.BIN_SEARCH_NOSHOW print(bMask,bPattern) begin_addr=0x1135 end_addr=0x3100 while begin_addr<end_addr: ea=ida_bytes.bin_search(begin_addr,end_addr,bPattern,bMask,1,signs) if ea == ida_idaapi.BADADDR: break else: print(hex(ea)) patch(ea,3) begin_addr=ea+8
|
pyc去花指令
1
| 脚本读取机器码+010删掉花指令+修改co_code长度
|
花指令通常开头是JUMP_ABSOLUTE X,然后填充错误代码
1 2 3 4 5 6
| import marshal, dis f = open("D:\\new\\AD\\game\\vnctf2022\\re\\BabyMaze.pyc", "rb").read() code = marshal.loads(f[16:]) dis.dis(code) dis.dis(code) print(len(code.co_code))
|
转为十六进制后去010editor修改,去掉即可,这里JUMP_ABSOLUTE16进制为71
之后重新反编译即可