花指令的解析

1.反编译算法

  1. 线性扫描算法:逐行反汇编(无法将数据和内容进行区分),遇到跳转不会跳过去进行分析,而是继续分析下一行
  2. 递归行进算法:按照代码可能的执行顺序进行反汇编程序

2.花指令解析

花指令(JunkCode)指的是使用一些技巧将代码复杂化,使人难以阅读的技术

广义上:

花指令与代码混淆(ObfusedCode)同义,包括结构混淆、分支混淆、语句膨胀等等

狭义上:

指的主要是干扰反汇编解析的技术

1.花指令的目的

是干扰ida和od等软件对程序的静态分析,使这些软件无法正常反汇编出原始代码

2.花指令的原理

本质

通过巧妙的构造,引导反汇编引擎解析一条错误的指令,扰乱指令的长度,就能使反汇编引擎无法按照正常的指令长度一次解析邻接未解析的指令,最终使反汇编引擎输出错误的反汇编结果 构造有效花指令的关键思路就是构造使源程序逻辑不受影响的内联汇编代码,同时在内联汇编代码中嵌入jmp call+ret之类的对应机器码指令,使反汇编软件在反汇编时错误地识别这些机器码为汇编指令,从而影响反汇编出来的程序的正常流程

原则

保持堆栈的平衡

3.常见汇编指令

指令说明
push ebp把基址指针寄存器 ebp 压入堆栈
pop ebp把基址指针寄存器 ebp 弹出堆栈
push eax把数据寄存器 eax 压入堆栈
pop eax把数据寄存器 eax 弹出堆栈
nop不执行任何操作(No Operation)
add esp, 1将栈指针寄存器 esp 加 1
sub esp, -1将栈指针寄存器 esp 加 1
add esp, -1将栈指针寄存器 esp 减 1
sub esp, 1将栈指针寄存器 esp 减 1
inc ecx计数器寄存器 ecx 加 1
dec ecx计数器寄存器 ecx 减 1
sub esp, 1将栈指针寄存器 esp 减 1
sub esp, -1将栈指针寄存器 esp 加 1
jmp 入口地址跳到程序入口地址
push 入口地址把入口地址压入堆栈
retn返回到入口地址,效果与 jmp 入口地址 相同
mov eax, 入口地址把入口地址转送到数据寄存器 eax 中
jmp eax跳到数据寄存器 eax 中存储的地址
jb 入口地址如果上次操作的结果小于零,跳到入口地址
jnb 入口地址如果上次操作的结果不小于零,跳到入口地址,效果与 jmp 入口地址 相同
xor eax, eax将寄存器 eax 清 0
CALL 空白命令的地址调用空白命令地址,无效的 call 指令

4.花指令的分类

常见的花指令有以下几种

jx + jnx

image-20240725172152935

用连续两条相反的条件跳转,或是通过stc/clc汇编指令来设置位,使条件跳转变为跳转

call + pop

image-20240725172145511

用pop的方式来清除call的压栈,使栈平衡。从而用call实现jmp。IDA会认为call的目标地址为函数起始地址,导致函数创建错误

call + add esp, 4

image-20240725172138088

call + add [esp], n + retn

image-20240725172126620

3.花指令的实现

1.jmp跳转构造

最简单的花指令(可以被识别到)

__asm {
   jmp Label1       // 跳转到 Label1 位置,跳过中间的垃圾数据
   db thunkcode1    // 垃圾数据,例如:_emit 0xE8
   // 可以添加更多的垃圾数据
   Label1:          // Label1 标签,控制流将跳转到这里
   // 这里可以继续执行其他有效代码
}

2.多节形式与多层乱序构造

和前面类似只是乱序而且数量多(也能识别)

JMP Label1
 Db thunkcode1     ; 垃圾数据1
Label1:
...               ; 有用代码
 JMP Label2
 Db thunkcode2     ; 垃圾数据2
Label2:
...               ; 有用代码
 JMP Label1
 Db thunkcode1     ; 重复的垃圾数据1
Label2:
...
 JMP Label3
 Db thunkcode3     ; 垃圾数据3
Label1:
...               ; 有用代码
 JMP Label2
 Db thunkcode2     ; 重复的垃圾数据2
Label3:
...               ; 有用代码

3.跳转指令构造

插入不必要的跳转和条件判断,使反编译器或逆向工程人员更难理解实际的代码流程

__asm {
   push ebx;         // 保存寄存器 ebx 的当前值
   xor ebx, ebx;     // 将寄存器 ebx 清零
   test ebx, ebx;    // 对寄存器 ebx 执行测试(实际上是检查是否为零)
   jnz LABEL7;       // 如果 ZF 标志位为 0,则跳转到 LABEL7
   jz LABEL8;        // 如果 ZF 标志位为 1,则跳转到 LABEL8
LABEL7:
   _emit 0xC7;       // 垃圾数据(不会执行)
LABEL8:
   pop ebx;          // 恢复寄存器 ebx 的值
}

简单说一下标志位

ZF (Zero Flag): 零标志

表示结果是否为零 如果结果为零,则 ZF 置为 1;否则置为 0 常用于条件跳转,如 JZ (Jump if Zero) 和 JNZ (Jump if Not Zero)

CF (Carry Flag): 进位标志

表示无符号运算的进位或借位情况 在加法运算中,如果发生进位,则 CF 置为 1;在减法运算中,如果发生借位,则 CF 置为 1 常用于多字节运算、无符号比较、条件跳转等

OF (Overflow Flag): 溢出标志

表示有符号运算的结果是否超出表示范围 在有符号数运算中,如果结果超出了能表示的范围(即正数溢出为负数或负数溢出为正数),则 OF 置为 1 常用于判断溢出条件的跳转,如 JO (Jump if Overflow) 和 JNO (Jump if Not Overflow)

SF (Sign Flag): 符号标志

表示运算结果的符号(正或负) 如果结果为负,则 SF 置为 1;如果为正或零,则 SF 置为 0 常用于有符号数比较和条件跳转,如 JS (Jump if Sign) 和 JNS (Jump if Not Sign)

PF (Parity Flag): 奇偶标志

表示结果的最低字节中 1 的个数是否为偶数 如果最低字节中 1 的个数为偶数,则 PF 置为 1;为奇数则为 0 用于奇偶性检查和相关操作。

AF (Auxiliary Carry Flag): 辅助进位标志

表示在 BCD(十进制调整)操作中,低 4 位的进位情况 在加法中,如果从低 4 位产生进位,则 AF 置为 1;在减法中,如果发生借位,则 AF 置为 1

4.永真永假构造

通过设置永真或者永假的,导致程序一定会执行,由于ida反汇编会优先反汇编接下去的部分(false分支)

也可以调用某些函数会返回确定值,来达到构造永真或永假条件(ida和OD都被骗过去了)

__asm{
   clc                  // 清除进位标志 CF,设置 CF 为 0
   jnz label1           // 如果 CF 为 1,跳转到 label1
   _emit junkcode       // 垃圾数据,不会执行
label1:
}

清除进位标志(CF)并进行条件跳转,插入垃圾代码作为混淆

5.call&ret构造

call指令的本质:push 函数返回地址然后jmp 函数地址

ret指令的本质:pop eip

__asm {
   call LABEL9;        // 调用 LABEL9,实际上是将 LABEL9 的地址压入堆栈,并跳转到 LABEL9 执行
   _emit 0x83;         // 插入字节 0x83,作为垃圾数据,不会被执行到
LABEL9:
   add dword ptr ss : [esp], 8; // 修改堆栈顶的返回地址,加上 8
   ret;                // 从 LABEL9 返回,返回地址被修改过
   __emit 0xF3;        // 插入字节 0xF3,作为垃圾数据,不会被执行到
}

esp存储的就是函数返回地址,对[esp]+8,就是函数的返回地址+8,正好盖过代码中的函数指令和垃圾数据

6.自定义构造

1.替换ret指令

_asm
{
   call LABEL9;               // 调用 LABEL9,将 LABEL9 的地址压入堆栈,并跳转到 LABEL9 执行
   _emit 0xE8;                // 插入字节 0xE8(跳转指令的前缀)
   _emit 0x01;                // 插入字节 0x01
   _emit 0x00;                // 插入字节 0x00
   _emit 0x00;                // 插入字节 0x00
   _emit 0x00;                // 插入字节 0x00

LABEL9:
   push eax;                  // 将 eax 寄存器的当前值压入堆栈
   push ebx;                  // 将 ebx 寄存器的当前值压入堆栈
   lea  eax, dword ptr ds:[ebp - 0x0];  // 将 ebp 的地址加载到 eax 寄存器中

   // 计算函数返回值的存储位置
   add dword ptr ss:[eax - 0x50], 26;    // 将栈中指定位置的值加上 26,确保跳过垃圾数据

   pop eax;                   // 恢复 eax 寄存器的值
   pop ebx;                   // 恢复 ebx 寄存器的值
   pop eax;                   // 再次恢复 eax 寄存器的值
   jmp eax;                   // 跳转到 eax 寄存器中的地址,代替 ret 指令

   _emit 0xE8;                // 插入字节 0xE8(跳转指令的前缀)
   _emit 0x03;                // 插入字节 0x03
   _emit 0x00;                // 插入字节 0x00
   _emit 0x00;                // 插入字节 0x00
   _emit 0x00;                // 插入字节 0x00
   mov eax, dword ptr ss:[esp - 8];   // 将原本的 eax 值恢复到 eax 寄存器
}

这里就是定义一个函数读取函数的返回值,进行使用jmp跳转执行,这样实现了ret的功能,还可以在其中插入垃圾数据

2.永恒跳转构造

使用 jmp指令和一些条件跳转指令(如 jz、jnz等),结合对标志寄存器的控制,来实现永恒跳转

ZF跳转

__asm {
   xor eax, eax        // 将 eax 寄存器清零,设置 ZF (Zero Flag)
   test eax, eax       // 测试 eax 是否为零,ZF 将被设置为 1
   jz label1           // 如果 ZF 为 1,则跳转到 label1
   jmp label2          // 否则跳转到 label2

label1:
   _emit 0x90          // 插入 NOP 指令 (作为示例,实际可以是任何代码)
   jmp label1          // 跳回到 label1,形成一个永恒的跳转

label2:
   _emit 0x90          // 插入 NOP 指令 (作为示例,实际可以是任何代码)
}

CF跳转

__asm {
   clc              // 清除进位标志 CF
   adc al, 0        // 加上 0(不改变 AL,但设置 CF),CF 仍为 0
   jnc label1       // 如果 CF 为 0,则跳转到 label1
   jmp label2       // 否则跳转到 label2

label1:
   _emit 0x90       // 插入 NOP 指令
   jmp label1       // 永恒跳转到 label1

label2:
   _emit 0x90       // 插入 NOP 指令
}

OF跳转

__asm {
mov al, 0x7F // 将 127 存入 AL
add al, 1 // AL 加 1,产生溢出
jo label1 // 如果 OF 为 1,则跳转到 label1
jmp label2 // 否则跳转到 label2

label1:
_emit 0x90 // 插入 NOP 指令
jmp label1 // 永恒跳转到 label1

label2:
_emit 0x90 // 插入 NOP 指令
}

SF跳转(类似)

__asm {
mov al, 0x80 // 将 -128 存入 AL(最高位为 1,表示负数)
neg al // 取反,AL 变为 0x80,SF 为 1
js label1 // 如果 SF 为 1,则跳转到 label1
jmp label2 // 否则跳转到 label2

label1:
_emit 0x90 // 插入 NOP 指令
jmp label1 // 永恒跳转到 label1

label2:
_emit 0x90 // 插入 NOP 指令
}

PF跳转

__asm {
mov al, 0xAA // 将 0xAA 存入 AL,AL 中有偶数个 1,PF 为 1
test al, al // 测试 AL 的值
jp label1 // 如果 PF 为 1,则跳转到 label1
jmp label2 // 否则跳转到 label2

label1:
_emit 0x90 // 插入 NOP 指令
jmp label1 // 永恒跳转到 label1

label2:
_emit 0x90 // 插入 NOP 指令
}

7.拓展(SMC自解密)

将花指令中的垃圾数据替换为一些特定的特征码,可以对应的“定位功能”

将他嵌套进代码中,在当前进程中搜索hElLowoRlD字符串,定位,对下方的代码进行SMC自解密

asm
{
Jz Label
Jnz Label
_emit 'h'
_emit 'E'
_emit 'l'
_emit 'L'
_emit 'e'
_emit 'w'
_emit 'o'
_emit 'R'
_emit 'l'
_emit 'D'
Label:
}

关于加密和解密器的实现

#include <iostream>
#include <windows.h>
void executeDecryption() {
__asm {
; 设置解密密钥
mov al, 0xAA

; 加密的代码
mov byte ptr [0x1000], 0x9A ; 加密的NOP指令(0x9A XOR 0xAA = 0x90)
mov byte ptr [0x1001], 0x9A ; 第二个加密字节

; 解密
xor byte ptr [0x1000], al
xor byte ptr [0x1001], al

; 执行解密后的代码
jmp 0x1000 ; 跳转到解密后的代码
}
}
int main() {
// 加密代码的存储区域
unsigned char encryptedCode[] = {
0x9A, 0x9A, // 加密的NOP指令(0x9A)
};
// 存储解密后的代码
unsigned char decryptedCode[] = {
0x90, 0x90, // 解密后的NOP指令(0x90)
};
// 将加密代码拷贝到内存中(示例)
memcpy((void*)0x1000, encryptedCode, sizeof(encryptedCode));
// 执行解密
executeDecryption();
// 在此之后,0x1000地址处的代码已经被解密并可以执行
return 0;
}

4.花指令实例

1.jz jnz/jmp

__asm {
_emit 075h ; 0x75 - JNZ (Jump if Not Zero) 指令,跳转到目标偏移量 0x02 处
_emit 2h ; 0x02 - 偏移量,表示跳过接下来的 2 个字节(即跳过 0xE9 和 0xED)
_emit 0E9h ; 0xE9 - JMP (Jump) 指令,跳转到目标地址
_emit 0EDh ; 0xED - 作为 JMP 指令的偏移量,但通常这个值是不合法的
}

2.call ret

call+pop/add esp/add [esp] + retn

#include <iostream.h>
#include <windows.h>

void main()
{
DWORD p;
_asm
{
call l1 ; 调用 l1,保存返回地址到栈中
l1:
pop eax ; 弹出返回地址到 EAX 寄存器
mov p, eax ; 将返回地址存储到 p 变量中
call f1 ; 调用 f1

_EMIT 0xEA ; 花指令,这个指令永远不会被执行到

jmp l2 ; 跳转到 l2 标签

f1:
pop ebx ; 从栈中弹出值到 EBX
inc ebx ; 增加 EBX 的值
push ebx ; 将 EBX 的新值压入栈中
mov eax, 0x11111111 ; 将常数 0x11111111 存入 EAX
ret ; 返回到 l2 标签

l2:
call f2 ; 调用 f2,用 ret 实现跳转
mov ebx, 0x33333333 ; 这行代码永远不会被执行
jmp e ; 这行代码也永远不会被执行

f2:
mov ebx, 0x11111111 ; 将常数 0x11111111 存入 EBX
pop ebx ; 从栈中弹出地址到 EBX
mov ebx, offset e ; 将 e 的地址存入 EBX
push ebx ; 将 e 的地址压入栈中
ret ; 跳转到 e 标签

e:
mov ebx, 0x22222222 ; 将常数 0x22222222 存入 EBX
}

cout << hex << p << endl; // 输出 p 的值(返回地址),以十六进制形式显示
}

call指令可以理解为jmp + push ip 因此如果通过add esp,4来降低栈顶即可去除push ip的影响,从而使call等价于jmp 但IDA会认为这是函数的分界,导致出错

5.IDAPython处理

用IDA打开DancingCircle,按G输入0x401f58跳转至核心函数,发现有大量花指令。因此需要借助 ida python 脚本正则表达式匹配去除。 分析汇编代码,发现花指令有如下几类:

call 花指令

call + pop

image-20240726183114778

call + add esp, 4

image-20240726183146448

call + add [esp], 6 + retn

image-20240726183211047
def call_handler(s):
# 定义一个内部函数 work,它接受一个正则表达式模式 pattern 和一个字符串 s。
def work(pattern, s):
t = s[:] # 创建字符串 s 的一个副本并赋值给 t,通常这是为了保护原始数据不被修改。

# 这个 for 循环在 s 的所有可能的起始位置进行迭代。
for _ in range(end - start):
# 使用正则表达式模式 pattern 来匹配字符串 s 的一个子字符串。
it = re.match(pattern, s[_:], flags=re.DOTALL)

# 如果没有找到匹配项,继续下一个位置。
if it is None:
continue

# 检查匹配项中捕获的立即数(it.group(1))的值是否等于填充字节的长度(it.group(2))。
# 使用 struct.unpack("<I", it.group(1))[0] 将捕获的立即数从字节转换为整数。
if struct.unpack("<I", it.group(1))[0] == len(it.group(2)):
# 如果条件满足,获取匹配的子字符串的起始和结束位置。
l, r = it.span()

# 调整位置,以反映在原始字符串中的实际位置。
l += _
r += _

# 调用函数 p 处理从 l 到 r 的子字符串。
p(s[l:r])

jx + jnx 花指令

例如 0x00402D67 处的花指令

image-20240726184156926
def jx_jnx_handler(s):
# 遍历 0x70 到 0x7F 的范围,每次递增 2
for _ in range(0x70, 0x7F, 2):
# 定义一个内部函数 work,它接受一个正则表达式模式 pattern 和一个字符串 s。
def work(pattern, s):
t = s[:] # 创建字符串 s 的副本 t,通常是为了保护原始数据不被修改。

# 在 s 的所有可能的起始位置进行迭代。
for _ in range(end - start):
# 使用正则表达式模式 pattern 来匹配字符串 s 的一个子字符串。
it = re.match(pattern, s[_:], flags=re.DOTALL)

# 如果没有找到匹配项,继续下一个位置。
if it is None:
continue

# 将匹配到的字节转换为整数。
num1 = struct.unpack("<B", it.group(1))[0]
num2 = struct.unpack("<B", it.group(2))[0]

# 如果 num1 不等于 num2 + 2,跳过当前匹配。
if num1 != num2 + 2:
continue

# 获取匹配的子字符串的起始和结束位置。
l, r = it.span()

# 调整位置,以反映在原始字符串中的实际位置。
l += _
r += _ + num2

# 如果 num2 小于等于字符串的长度,进行替换操作。
if num2 <= len(s):
# 调用函数 p 处理从 l 到 r 的子字符串。
p(s[l:r])

# 将匹配到的子字符串替换为一系列 NOP 指令 (0x90)。
t = t[:l] + b"\\x90" * (r - l) + t[r:]

return t

# 构造两个字节的正则表达式模式,分别表示跳转指令的第一个字节和第二个字节。
op1 = (b"\\\\" if _ in b"{|}" else b"") + struct.pack("<B", _)
op2 = (b"\\\\" if _ + 1 in b"{|}" else b"") + struct.pack("<B", _ + 1)

# 创建正则表达式模式,用于匹配第一个跳转指令字节、任意字节和第二个跳转指令字节。
pattern = op1 + b"(.)" + op2 + b"(.)"

# 调用 work 函数处理字符串 s,并更新 s 为处理后的结果。
s = work(pattern, s)

# 创建另一个正则表达式模式,用于匹配第二个跳转指令字节、任意字节和第一个跳转指令字节。
pattern = op2 + b"(.)" + op1 + b"(.)"

# 再次调用 work 函数处理字符串 s,并更新 s 为处理后的结果。
s = work(pattern, s)

return s

fake jmp 花指令

image-20240726184355729
def fake_jmp_handle(s):
# 定义一个内部函数 work,用于根据给定的正则表达式模式处理字符串 s。
def work(pattern, s):
t = s[:] # 创建字符串 s 的副本 t,通常是为了保护原始数据不被修改。

# 在 s 的所有可能的起始位置进行迭代。
for _ in range(end - start):
# 使用正则表达式模式 pattern 来匹配字符串 s 的一个子字符串。
it = re.match(pattern, s[_:], flags=re.DOTALL)

# 如果没有找到匹配项,继续检查下一个位置。
if it is None:
continue

# 获取匹配的子字符串的起始和结束位置。
l, r = it.span()

# 调整位置,以反映在原始字符串中的实际位置。
l += _
r += _

# 调用函数 p 处理从 l 到 r 的子字符串。
p(s[l:r])

# 将匹配到的子字符串替换为一系列 NOP 指令 (0x90)。
t = t[:l] + b"\\x90" * (r - l) + t[r:]

return t # 返回处理后的字符串 t

# 使用不同的正则表达式模式对字符串 s 进行处理。
# 处理模式包括:
# - `\x7C\x03\xEB\x03`:这些是特定的花指令模式,可能表示特定的指令序列。
# - `\xEB\x07.\\xEB\x01.\\xEB\x04.\\xEB\xF8.`:其他指令序列的模式。
# - `\xEB\x01.`:另一种指令序列的模式。
s = work(rb"\\x7C\\x03\\xEB\\x03.\\x74\\xFB", s)
s = work(rb"\\xEB\\x07.\\xEB\\x01.\\xEB\\x04.\\xEB\\xF8.", s)
s = work(rb"\\xEB\\x01.", s)

return s # 返回处理后的字符串 s

stx + jx 花指令

image-20240726184600140
image-20240726184611492
def stx_jx_handler(s):
   t = s[:]  # 创建字符串 s 的副本 t,通常是为了保护原始数据不被修改。
   
   # 定义一个正则表达式模式,匹配特定的指令模式:
   # - \xF8\x73 或 \xF9\x72:这是两种可能的跳转指令的前缀。
   # - (.):捕获紧跟在前缀后的一个字节。
   pattern = rb"(?:\xF8\x73|\xF9\x72)(.)"
   
   # 遍历字符串 s 的所有可能起始位置。
   for _ in range(end - start):
       # 使用正则表达式模式 pattern 从位置 _ 开始匹配字符串 s 的一个子字符串。
       it = re.match(pattern, s[_:], re.DOTALL)
       
       # 如果没有找到匹配项,继续检查下一个位置。
       if it is None:
           continue
       
       # 获取匹配的子字符串的起始和结束位置。
       l, r = it.span()
       
       # 调整匹配位置,以适应原始字符串中的实际位置。
       l += _
       r += _ + struct.unpack("<B", it.group(1))[0]
       
       # 只处理跳转距离小于等于 0x40 的匹配项。
       if r - l > 0x40:
           continue
       
       # 调用函数 p 处理从 l 到 r 的子字符串。
       p(s[l:r])
       
       # 将匹配到的子字符串替换为一系列 NOP 指令 (0x90)。
       t = t[:l] + b"\x90" * (r - l) + t[r:]
   
   return t  # 返回处理后的字符串 t
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇