花指令

参考链接:

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掉就可以了

1
E80000000083042406C3??

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
#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--;
}
}
}
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': # 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.')
  1. 定义了一个名为f的函数,用于执行二进制搜索并对匹配的位置进行nop
    • begin_addr是搜索开始的地址。
    • end_addr是搜索结束的地址。
    • hexStr是用于搜索的十六进制模式字符串(用空格分开,其中??表示通配符,注意不可以使用诸如2?这样的情况)
  1. 接着,将hexStr转换为两个字节数组:bMask和bPattern。bMask是用于表示可变字节的,其中00表示需要匹配的字节,01表示不需要匹配的字节;bPattern则是用于搜索的固定字节序列
  2. 定义了一个signs变量,用于指定搜索的标志位,其中BIN_SEARCH_FORWARD表示向前搜索,BIN_SEARCH_NOBREAK表示不允许搜索中途中断,BIN_SEARCH_NOSHOW表示不显示搜索结果
  3. 接着使用ida_bytes.bin_search函数进行二进制搜索,从begin_addr到end_addr之间搜索bPattern,其中可变字节由bMask指定。搜索到匹配的位置后,将其打印出来,并调用patch函数对其进行补丁操作
  4. 最后更新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:]) #这边从16位开始取因为是python3 python2从8位开始取
dis.dis(code)
dis.dis(code)
print(len(code.co_code))

转为十六进制后去010editor修改,去掉即可,这里JUMP_ABSOLUTE16进制为71

之后重新反编译即可


花指令
https://j1ya-22.github.io/2024/10/22/花指令/
作者
j1ya
发布于
2024年10月22日
许可协议