花指令(junk code)是一种用于混淆程序逻辑、增加逆向工程难度的技术。(虽然对免杀也有帮助)

通过在程序中插入看似无用但实际上不影响程序功能的代码段,可以保护软件免受未经授权的分析或篡改。

这些无用代码通常不会被程序执行流程直接调用,但它们的存在使得反编译和理解程序变得复杂。

不可执行花指令

这类花指令的在程序正常运行过程中并不会被执行

但是在使用工具进行反编译的时候会影响工具判断,导致工具反编译错误

这里我们要先了解反编译该工具的原理

反编译原理

线性扫描和递归下降

  • 线性扫描,反编译扫描过程中遇到的每一条指令。但是在冯诺依曼体系结构下,无法区分数据与代码可能会将代码段中的数据误认为是指令的操作码,从而导致反汇编结果错误(OD、Windbg)

在计算机的存储器中,程序代码和数据被存储在同一个物理存储空间内,它们的格式和表示方式完全相同,都是以二进制的形式存在,因此从存储器的角度来看,无法直接判断某个存储单元中的内容是代码还是数据

  • 递归下降,强调控制流的概念,当分析的时候遇到了分支,将会递归进入分支进行反编译,模拟程序正常运行的模式(IDA)

多字节指令

利用了x86指令集的特性(指令长度不定)和反汇编器的解析逻辑。通过在条件跳转指令之后插入垃圾数据,反汇编器会尝试解析垃圾数据,导致指令边界、长度或逻辑错误

我们用Visual Studio 编译下面代码

#include "stdio.h"
int main(int argc, char const* argv[]){
	_asm{
		jz label       
		jnz label           
		_emit 0xe9      
		label:
	}
	printf("Hello world!\n");
	return 0;
}

将程序放入 ida,我们可以看下面的情况

jz 和 jnz 的跳转地址是处于一个指令的中间

造成这个情况的原因,jz 和 jnz 的互补条件让程序的正常执行可以跳过_emit 0xe9,但是 ida 在反编译的时候,会尝试解析_emit 0xe9,而 0xE9是一个jmp指令的操作码,IDA会将其误解析为跳转指令,并尝试解析其后的字节作为跳转目标地址。也就是错误拼接了label

遇到这种情况 patch 可以

1.D 键将反编译错误的位置直接转换成数据

2.讲错误的 0xe9 转换成 nop(0x90)Edit -> Patch program -> Change byte

实际情况中有可能会嵌套多层,使用 idc 脚本转化即可

破坏堆栈平衡

IDA采用递归下降算法进行反汇编分析。它会从程序入口开始,逐步解析指令,并模拟程序的控制流。在分析过程中,IDA会跟踪堆栈指针的变化,确保堆栈操作的逻辑正确性。如果IDA检测到堆栈指针被非法修改(如堆栈指针增加或减少的值不符合逻辑),它会认为代码存在错误。例如:

add esp, 1       #堆栈指针增加1
sub esp, 5       #堆栈指针减少5

这种操作在正常的函数调用和返回逻辑中是不合理的,因此IDA会报错。

我们用Visual Studio 编译下面代码

#include "stdio.h"
int main(int argc, char const* argv[]){
    _asm{
        test eax,0
            jz label           
            add esp,0x1        
            label:
    }
    printf("Hello world!\n");
    return 0;
}

正常情况test eax, 0操作的结果总是0,零标志位(ZF)一定会被设置为1因此,jz label总是会跳转到label,而add esp, 0x1永远不会被执行

但是 IDA 反编译的时候尝试分析如果jz不跳转,开始分析add esp, 0x1 会修改堆栈指针 esp 的值,而IDA 在分析时会假设所有代码路径都可能被执行,因此它会检测到堆栈指针被非法修改(堆栈平衡被破坏),从而报错

patch 的方法,就是 nop 掉用来破坏堆栈平衡的指令

可执行花指令

这类花指令在程序执行过程中会被执行

但执行前后不改变程序执行逻辑,不改变任何寄存器的值,只是加大静态分析的难度

各种函数调用

如 call 和 ret 构造的花指令

我们用Visual Studio 编译下面代码

#include "stdio.h"
int main(int argc, char const* argv[]){
	_asm{
	   call label
	   label:
	   add [esp],5
       ret
	}
	printf("Hello world!\n");
	return 0;
}

这种精心构造的代码,本身执行并没有问题,假设call label下一条指令的地址为 0x1000,顺序执行到add [esp],5,给堆栈指针的内容+5(0x1005),然后 ret 从堆栈中弹出返回地址 0x1005,继续执行到后续的printf("Hello world!\n")

但是 IDA 在分析的时候会检测到一次add [esp],5会认为破坏了堆栈平衡,从而报错

这种情况 patch 就直接就将 call 改为jmp 即可,jmp无条件跳转,并不会将地址压入堆栈,让add [esp],5操作无效

还有例如更复杂的情况

#include "stdio.h"
int main(int argc, char const* argv[]) {
    _asm {
        push eax
        push ecx
        jmp label_1
        label_2:
        mov eax,[esp]
        add [esp],8
        jmp eax
        label_1:
        call label_2
        pop eax
    }
    printf("Hello world!\n");
    return 0;
}

精心构造的执行依旧没有问题,但是代码中多次使用 pushadd [esp], 8 等操作,这些操作会修改堆栈内容。特别是jmp eax 非线性指令,它会跳转到一个动态计算的地址,这使得IDA难以确定后续指令的执行路径。

patch 的方法就是将各种无效的压栈、跳转 nop 掉即可

免杀应用

AV(杀毒软件)通常通过特征码扫描、启发式分析和行为分析来检测恶意软件。花指令可以通过以下方式躲避AV查杀:

修改特征码

通过插入花指令,改变恶意代码的特征码,使其无法被AV引擎直接识别。例如:

jmp label
db 0x90, 0x90, 0x90  //插入NOP指令
label:

这种插入的NOP指令不会影响程序逻辑,但会改变代码的特征码

动态跳转、混淆控制流、修改入口点等等

使用动态计算的跳转地址(如 jmp eax)、修改堆栈中的返回地址、或复杂的、多层的跳转逻辑,配合修改程序入口点(OEP)跳入插花的区域,使得AV难以跟踪指令流。这种也类似于各种函数调用这一部分的例子。


"The quieter you become, the more you are able to hear."