背景
线性扫描算法:逐行反汇编(无法将数据和内容进行区分) 递归下降算法:根据一条指令是否被另一条指令引用来决定是否对其进行反汇编(难以准确定位) 正是因为这两种反汇编的规格和缺陷机制,所以才导致了会有花指令的诞生
花指令简单的说就是在代码中混入一些垃圾数据阻碍静态分析
常见指令 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凌武杯flower_tea
这里call指令,其实本质就是jmp&push 下一条指令的地址,但是这里只需要jmp指令,push这条指令是多余的,后续的add指令又会修改下一条指令的地址,造成爆红
易语言自带的花指令,与上面的本质相同
004010BF . E8 00000000 call 1111.004010C4
004010C4 /$ 830424 06 add dword ptr ss:[esp],0x6
004010C8 \. C3 retn
004010C9 B9 db B9 只需要将下面的特征码patch掉就可以了
E80000000083042406C3?? 4.jmp XXX(红色) 题目练习:622K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2F1M7%4y4U0N6r3k6Q4x3X3g2U0L8W2)9J5c8X3&6G2N6r3g2Q4x3V1k6K6k6i4c8Q4x3V1j5J5z5e0M7H3
这也是一种常见的花指令,虚拟地址不可能那么大,实际是e9在搞鬼,ida会默认将e9后面的4个字节当成地址,只要nop掉jmp(E9)就好了
5.stx/jx
clc是清除EFlags寄存器的carry位的标志,而jnb是根据cf==0时跳转的,然而jnb这个分支指令,ida又将后面的部分认作成了另外的分支。
6.汇编指令共用opcode 加了一些无效指令导致ida等反编译工具识别错误
创意 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/E9) 练习题目:0b4K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2F1M7%4y4U0N6r3k6Q4x3X3g2U0L8W2)9J5c8Y4m8J5L8$3u0D9k6h3#2Q4x3V1j5J5x3K6p5K6
在0x401051设置为数据类型(快捷键D),将call 转成硬编码 E8 再将光标放到 db 0E8上 将E8改成 nop(90) 后按C(转化为代码类型)点yes 将硬编码修复成代码
然后向下逐⼀修复 将光标放置在黄色的行上 按C修复 直到没有黄色地址
最后全选函数,按P生成函数
nop跨越汇编指令 jz指令指向下一条指令中间
这个时候让jz正常分析,也就是把中间的nop
如果后面有数据没被分析为code,需要重新分析一下
nop部分汇编 一般去菜单中的编辑选项修补单字节会比较好把控
像下面这种红色标志离原函数有一定距离又是call+retn组合,400f64又没什么用,还有ret会干扰函数的分析,那就都nop,这是一种暴力破解方法
nop完后看到有%lld,删除函数,修补函数即可反编译
xchg很少用到,后面还有retn,主打不想要的直接全部nop
一开始对于这一段汇编不知道怎么处理,后面看到一堆和rax有关的操作,从push rax开始异或,刚好下面有pop rax,这是ida对call retn指令识别漏洞的花指令,ida错误解析call后的指令边界,导致后续代码显示为无效指令造成的
先可以小范围的尝试,把call到红色的地方先nop,发现不行,再从头把关键数据之前(D7,flag is上面那一行)也给nop掉,发现可以了
代码自动去花 nop #include <idc.idc>
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--;
}
}
}
定义了一个名为f的函数,用于执行二进制搜索并对匹配的位置进行nop,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为下一个搜索的起始地址,通常为当前匹配位置加上一个偏移量 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 ??")
jx+jnx
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") 清除常见花指令 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': # stc; jb $+3
ida_bytes.patch_bytes(addr, b'\x90' * 4)
elif ida_bytes.get_bytes(addr, 3) == b'\xf8\x73\x01': # clc; jnb $+3
ida_bytes.patch_bytes(addr, b'\x90' * 4)
else:
addr += size
elif size == 2 and ida_bytes.get_bytes(addr, 2) == b'\xeb\x01': # jmp $+3
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': # call $+5; add dword ptr [esp], 6; ret
ida_bytes.patch_bytes(addr, b'\x90' * 11)
else:
addr += size
else:
addr += size
print('Done.')
jmp db1
分析main()函数可以看到是杂乱的字节,观察0x1144可以发现,存在着jmp db1这种类型的花指令,因此可以写idapython脚本来解决
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去花指令 脚本读取机器码+010删掉花指令+修改co_code长度
花指令通常开头是JUMP_ABSOLUTE X,然后填充错误代码
import marshal, dis
f = open("D:\\new\\AD\\game\\vnctf2022\\re\\BabyMaze.pyc", "rb").read()
code = marshal.loads(f[16:]) #这边从16位开始取因为是python3 python2从8位开始取
dis.dis(code)
dis.dis(code)
print(len(code.co_code))
转为十六进制后去010editor修改,去掉即可,这里JUMP_ABSOLUTE16进制为71
之后重新反编译即可
参考链接:
70cK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6T1L8r3!0Y4i4K6u0W2j5%4y4V1L8W2)9J5k6h3&6W2N6q4)9J5c8X3@1H3i4K6g2X3y4e0p5J5y4o6j5^5y4K6y4Q4x3V1k6S2M7Y4c8A6j5$3I4W2i4K6u0r3k6r3g2@1j5h3W2D9M7#2)9J5c8U0p5J5y4K6p5$3y4K6M7@1z5b7`.`.
545K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2U0L8X3u0D9L8$3N6K6i4K6u0W2j5$3!0E0i4K6u0r3h3h3g2F1d9$3!0U0i4K6u0r3M7q4)9J5c8U0p5@1x3e0x3$3x3o6p5J5i4K6u0W2K9s2c8E0L8l9`.`.
185K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6E0M7q4)9J5k6i4N6W2K9i4S2A6L8W2)9J5k6i4q4I4i4K6u0W2j5$3!0E0i4K6u0r3M7#2)9J5c8V1#2g2N6r3R3I4f1i4N6Q4x3X3c8r3L8o6u0S2y4f1!0J5e0s2N6Q4y4h3j5J5i4K6g2X3x3r3M7`.
517K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6T1L8r3!0Y4i4K6u0W2j5%4y4V1L8W2)9J5k6h3&6W2N6q4)9J5c8X3q4T1k6h3I4Q4y4h3k6T1K9h3N6Q4y4h3k6^5N6g2)9J5c8X3q4J5N6r3W2U0L8r3g2Q4x3V1k6V1k6i4c8S2K9h3I4K6i4K6u0r3x3e0p5%4z5e0t1%4y4U0M7@1
出题参考:
4e9K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6T1L8r3!0Y4i4K6u0W2j5%4y4V1L8W2)9J5k6h3&6W2N6q4)9J5c8X3@1H3i4K6g2X3y4o6j5J5z5e0j5&6x3o6g2Q4x3V1k6S2M7Y4c8A6j5$3I4W2i4K6u0r3k6r3g2@1j5h3W2D9M7#2)9J5c8U0p5I4y4K6x3K6y4U0f1%4y4l9`.`.
https://bbs.kanxue.com/thread-279604.htm