XSLT存档  

不及格的程序员-八神

 查看分类:  ASP.NET XML/XSLT JavaScripT   我的MSN空间Blog

花指令实际上把它按照 “乱指令” 来理解可能更贴切一些。我们知道,汇编语言其实就是机器指令的符号化,从某种程度上看,它只是更容易理解一点的机器指令而已。每一条汇编语句,在汇编时,都会根据 cpu 特定的指令符号表将汇编指令翻译成二进制代码。

而日常应用中,我们通过 VC 的 IDE 或其它如 OD 等反汇编、反编译软件也可以将一个二进制程序反汇编成汇编代码。机器的一般格式为:指令+数据。

而反汇编的大致过程是:首先会确定指令开始的首地址,然后根据这个指令字判断是哪个汇编语句,然后再将后面的数据反汇编出来。由此,我们可以看到,在这一步的反汇编过程中存在漏洞:如果有人故意将错误的机器指令放在了错误的位置,那反汇编时,就有可能连同后面的数据一起错误地反汇编出来,这样,我们看到的就可能是一个错误的反汇编代码。这就是 “花指令”,简而言之,花指令是利用了反汇编时单纯根据机器指令字来决定反汇编结果的漏洞。使得杀毒软件不能正常的判断病毒文件的构造。说通俗点就是 “杀毒软件是从头到脚按顺序来查找病毒。如果我们把病毒的头和脚颠倒位置。杀毒软件就找不到病毒了。”


花指令详解

 
3 篇文章0 订阅

作者:小安
博客地址:我的博客
哎,想了好长时间这个知识点到底什么名字,还是最简单的就叫做“花指令”。本文参照网上众多大佬博客和自己的理解进行编写的。

背景

为什么会出现花指令?

image-1662120637763
划重点:

  • 线性扫描算法:逐行反汇编(无法将数据和内容进行区分)
  • 递归行进算法:按照代码可能的执行顺序进行反汇编程序。
    正是因为这两种反汇编的规格和缺陷机制,所以才导致了会有花指令的诞生。

概念

花指令是企图隐藏掉不想被逆向工程的代码块(或其它功能)的一种方法,在真实代码中插入一些垃圾代码的同时还保证原有程序的正确执行,而程序无法很好地反编译, 难以理解程序内容,达到混淆视听的效果。
简单的说就是在代码中混入一些垃圾数据阻碍你的静态分析。

分类

可执行花指令

  1. 可执行花指令指的是这部分花指令代码在程序的正常执行过程中会被执行,但执行这些代码没有任何意义,执行前后不改变任何寄存器的值(当然eip这种除外),同时这部分代码也会被反汇编器正常识别。
  2. 首先,花指令的首要目的依然是加大静态分析的难度,让你难以识别代码的真正意图;然后,这种花指令可以破坏反编译的分析,使得栈指针在反编译引擎中出现异常。(当然我们知道栈指针实际上是没有问题的,只不过反编译引擎还有待完善的空间)

不可执行式花指令

  1. 花指令虽然被插入到了正常代码的中间,但是并不意味着它一定会得到执行,这类花指令通常形式为在代码中出现了类似数据的代码,或者IDA反汇编后为jmupout(xxxxx)。
  2. 这类花指令一般不属于CPU可以识别的操作码,那么就需要在上面用跳转跳过这些花指令才能保证程序的正常运行。

实现

  1. 简单的花指令
    简单的花指令 0xe8是跳转指令,可以对线性扫描算法进行干扰,但是递归扫描算法可以正常分析。
    image-1662122257470
    两个跳转一个指向无效数据,一个指向正常数据来干扰递归扫描算法。
    image-1662122265332
  2. 简单的jmp
    OD能被骗过去,但是因为ida采用的是递归扫描的办法所以能够正常识别。
#include<stdio.h>
int main() {
	__asm{
		jmp label1;
		__emit 0xe8;
	label1:    
	}
	printf("Hello World!");
	return 0;
}

image-1662123077572
3. 多级跳转
本质上和简单跳转是一样的,只是加了几层跳转。显然无法干扰ida

#include<stdio.h>
int main() {
	__asm{
	start://花指令开始
		jmp label1;
		__emit 0xe8;
	label1:
		jmp label2;
		__emit 0xe8;
	label2:
		jmp label3;
		__emit 0xe8;
	label3:  
	}
	printf("Hello World!");
	return 0;
}

image-1662124799478
4. jnx和jx条件跳转
利用jz和jnz的互补条件跳转指令来代替jmp。竟然没有骗过OD(是因为吾爱的这个有插件吗)。但是ida竟然没有正常识别。
image-1662127609702

image-1662125780683
5. 永真条件跳转
通过设置永真或者永假的,导致程序一定会执行,由于ida反汇编会优先反汇编接下去的部分(false分支)。也可以调用某些函数会返回确定值,来达到构造永真或永假条件。ida和OD都被骗过去了。

__asm{
    push ebx
    xor ebx,ebx
    test ebx,ebx
    jnz label1
    jz label2
label1:
    _emit junkcode
label2:
   pop ebx//需要恢复ebx寄存器    
}

__asm{
	clc
	jnz label1:
	_emit junkcode
label1:
}

image-1662183250706
6. call&ret构造花指令

__asm{
    call label1
    _emit junkcode
label1:
    add dword ptr ss:[esp],8//具体增加多少根据调试来
    ret
    _emit junkcode
}

call指令:将下一条指令地址压入栈,再跳转执行
ret指令:将保存的地址取出,跳转执行

image-1662183318853
7. 汇编指令共用opcode
jmp的条指令是inc eax的第一个字节,inc eax和dec eax抵消影响。这种共用opcode确实比较麻烦。
image-1662183405533

创意花指令

前面几种花指令都是比较老套的,入门花指令还能勉勉强强骗过反编译器,不过有经验的逆向者一眼就能识破,以下几种花指令形式,可以任由自己构造。

  1. 替换ret指令
  _asm
    {
        call LABEL9;
        _emit 0xE8;
        _emit 0x01;
        _emit 0x00;
        _emit 0x00;
        _emit 0x00;

     LABEL9:
        push eax;
        push ebx;
        lea  eax, dword ptr ds : [ebp - 0x0];
        #将ebp的地址存放于eax        

        add dword ptr ss : [eax-0x50] , 26;
        #该地址存放的值正好是函数返回值,
        #不过该地址并不固定,根据调试所得。
         #加26正好可以跳到下面的mov指令,该值也是调试计算所得

        pop eax;
        pop ebx;
        pop eax;
        jmp eax;
        _emit 0xE8;
        _emit 0x03;
        _emit 0x00;
        _emit 0x00;
        _emit 0x00;
        mov eax,dword ptr ss:[esp-8];
        #将原本的eax值返回eax寄存器
    }

由于:

call指令的本质:push 函数返回地址然后jmp 函数地址
ret指令的本质: pop eip
两者都是对寄存器eip中存放的地址的操作。
所以我们可以在call指令之后,清楚的明白函数返回地址存放于esp,可以将值取出,用跳转指令跳转到该地址,即可代替ret指令。
当然,这种构造跳转指令可以变化多样。

  1. 控制标志寄存器跳转
    这一部分需要精通标志寄存器,每一个操作码都会对相应的标志寄存器产生相应的影响,如果我们对标志寄存器足够熟练,就可以使用对应的跳转指令构造永恒跳转!
  2. 利用函数返回确定值
    有些函数返回值是确定的,比如我们自己写的函数,返回值可以是任意非零整数,就可以自己构造永恒跳转。
    还有些api函数也是如此:方面可以传入一些错误的参数,如LoadLibraryA。
HMODULE LoadLibraryA(
  LPCSTR lpLibFileName
);

如果我们故意传入一个不存在的模块名称,那么他就会返回一个确定的值NULL,我们就可以通过这个构造永恒跳转。
另一方面,某些api函数,我们既然使用他,肯定就是一定要调用成功的,而这些api函数基本上只要调用成功就就会返回一个确定的零或者非零值,如MessageBox:

int MessageBox(
  HWND    hWnd,
  LPCTSTR lpText,
  LPCTSTR lpCaption,
  UINT    uType
);

该api只有在其调用失败的时候才能返回零,那么我们也可以通过这一点构造永恒跳转。
3. 花指令原理另类利用
当我们理解了花指令的原理后,我们可以在将花指令中的垃圾数据替换为一些特定的特征码,可以对应的“定位功能” “定位功能”定位功能”,尤其在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:
}

将这串特征码hElLowoRlD嵌入到代码中,那我们只需要在当前进程中搜索hElLowoRlD字符串,就可以定位到当前代码位置,然后对下面的代码进行SMC自解密。

清除

  1. 手动清除
    找到所有的花指令,重新设置数据和代码地址。或者将花指令设置为nop(0x90)
    image-1662184745624
    在0x401051设置为数据类型(快捷键D),在0x401052设置为代码类型(快捷键C)
    image-1662184763944
    这里用一个ida python脚本添加ALT+N快捷键来将指令的第一个字节设置为NOP
from idaapi import *
from idc import *
def nopIt():
	start = get_screen_ea()
	patch_byte(start,0x90)
	refresh_idaview_anyway()
add_hotkey("alt-N",nopIt)
  1. 自动清楚花指令
    面有3个类别ida无法正常识别
  • 互补条件跳转(比较好处理)
  • 永真条件跳转 (各种永真条件比较难匹配)
  • call&ret跳转(比较难处理)
    所以就只对第一种jnx和jx的花指令进行自动化处理。
    所有的跳转指令,互补跳转指令只有最后一个bit位不同。
70 <–> JO(O标志位为1跳转)
71 <–> JNO
72 <–> JB/JNAE/JC
73 <–> JNB/JAE/JNC
74 <–> JZ/JE
75 <–> JNZ/JNE
76 <–> JBE/JNA
77 <–> JNBE/JA
78 <–> JS
79 <–> JNS
7A <–> JP/JPE
7B <–> JNP/JPO
7C <–> JL/JNGE
7D <–> JNL/JGE
7E <–> JLE/JNG
7F <–> JNLE/JG

第一条指令跳转距离=第二条跳转距离+2。简单一点可以是\x03和\x01
image-1662184893959
抄的代码

from ida_bytes import get_bytes,patch_bytes
start= 0x401000#start addr
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") 

总结

重点:构造永恒跳转,添加垃圾数据!
重一方面构造一个永恒的跳转,一方面又比较隐蔽,不仅骗过反编译器,更让破解者找不到花指令。

参考文章
https://blog.csdn.net/abel_big_xu/article/details/117927674
https://www.anquanke.com/post/id/236490#h3-12加粗样式


花指令总结

 

花指令又称脏字节,英文为"junkcode",顾名思义,即在程序中加入的一些垃圾指令,其目的是在不妨碍原有程序执行的前提下,阻碍程序反编译,增加静态分析难度,隐匿不想被逆向分析的代码块,混淆代码,绕过特征识别。本文将常见的花指令进行归纳总结,作学习记录。

 

 

花指令可分为两大类:可执行花指令和不可执行花指令,具体区分如下:

一、不可执行式花指令

不可执行花指令指这部分花指令在程序执行过程中不会被执行,它是利用反汇编器 静态分析算法的缺陷 使得代码在反编译解析时出错。其原理使反汇编分析执行流命中执行会出错的垃圾数据,就会造成解析错误,而实际执行过程中垃圾数据并不会执行。反汇编器静态分析算法一般有两种:

  • 一是线性扫描,从程序入口处依次读取机器码并进行反汇编,逐行命令进行线性扫描,在于在冯诺依曼体系结构下,无法区分数据与代码,从而导致将代码段中嵌入的数据误解释为指令的操作码,采用线性扫描技术的反汇编工具如OD、Windbg;
  • 二是递归下降,从程序入口开始读取机器码进行反汇编,通过程序的控制流确定反汇编的下一条指令,遇到无条件跳转则从跳转目的地址处继续解析,遇到条件跳转则从两个命令执行分支处进行解析 (优先解析顺序执行分支),即采用模拟程序运行的方式增加反汇编的准确度,采用递归下降反汇编的如IDA。

不同的调试环境采用的反汇编分析算法不同,导致加花效果也是不同的,这里实验环境主要为IDA Pro。在设计这类花指令时要通过构造 必然条件 或者 互补条件,使得程序在实际执行时绕过垃圾数据,这样不会影响程序正常执行,以下是常见的花指令形式:

1.多字节指令

在条件跳转指令之后插入垃圾数据,这些数据是多字节指令的机器指令,产生的根本原因是x86指令集指令长度不定,通过跳转使反汇编分析执行流命中到另一条指令的中间时就会造成解析错误。由于IDA采用递归下降算法,其会优先分析顺序执行的条件分支,从而将插入数据后的指令与数据相混淆,导致反汇编分析失败。如下插入数据:

jz Label        // 采用条件互补的方法跳转绕过垃圾数据
jnz Label
junkcode
Label:
...

其中 junkcode 可以是任何的多字节指令的硬编码,常用的脏字节编码如下:

call immed16     ---->    E8    // 3字节指令,immed16为2字节,代表跳转指令的目的地址与下一条指令地址的距离
call immed32     ---->    9A    // 5字节指令,immed32为4字节,代表跳转指令的目的地址与下一条指令地址的距离
jmp  immed8      ---->    EB  
jmp  immed16     ---->    E9  
jmp  immed32     ---->    EA  
loop immed8      ---->    E2  
ret              ---->    C2
retn             ---->    C3

下面以最简单的一段代码为例,在输出字符串之前插入花指令:

注意:不同的编译环境插入汇编指令的格式不同,这里以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;
}

出现这类花指令的特征是 程序会跳转到指令执行的中间位置,如下所示:
请添加图片描述patch方法是将00401047位置代码转换为数据 (D) 或者在Edit -> Patch program -> Change byte选项中改为NOP (0x90),然后从00401047位置代码开始转换为代码 ©:
请添加图片描述
可能连续嵌套多层类似的指令,手动patch逐层将其修正即可,如果太复杂则需要使用idc和idaPython脚本。

注意,如果插入汇编指令的位置条件跳转指令换成无条件跳转jmp,则会发现IDA反汇编不会发现错误,这是因为其采用递归下降的分析方法,分析过程针对无条件跳转指令会直接跳转到目的地址继续进行解析。

2.破坏堆栈平衡

汇编中函数如果有参数或局部变量,在调用前会对堆栈进行保护 ,在返回前要还原函数调用前的堆栈,这一过程程序在编译时会自动加上,如果反编译器检测到指令破坏了堆栈平衡,即函数返回时与调用时堆栈状态发生了变化,就会报错。可以利用这一点构造破坏堆栈平衡的花指令,如下代码:

#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;
}

如下图所示,这类破环堆栈平衡的指令实际不会执行,但是由于IDA在反汇编分析时会分别从两个条件跳转处开始分析,因此判定堆栈不平衡导致反汇编失败,解决方法是NOP掉破坏堆栈平衡的指令。
请添加图片描述

二、可执行花指令

花指令在程序执行过程中会被执行,但执行这些代码没有任何意义,执行前后不改变任何寄存器的值,也不改变程序执行逻辑和结果,目的是加大静态分析的难度,或是混淆特征码,绕过特征检测。

1.利用函数调用

函数调用时将调用指令的下一条指令地址压栈,然后跳转到函数位置执行,相当于:PUSH 下一条指令地址MOV EIP,函数位置,相应地,函数返回时将函数调用时的压栈地址恢复给EIP,相当于POP EIP

可以利用函数调用和返回的原理构造花指令,如下例代码利用CALL&RET指令构造花指令:

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

如下图所示,之所以分析失败是因为IDA检测出堆栈操作破坏堆栈平衡,而这正是构造者有意为之,只有改变了返回地址,函数返回后才能正常向下执行,至于栈顶的返回地址要加多少,这取决于跨过语句的长度,如下所示,需要构造RET返回的位置和正常RET返回的位置相差5个字节,因此栈顶数据加上5个字节:
请添加图片描述patch方法也很简单,直接将调用语句和改变堆栈平衡的语句NOP掉,或者将调用语句修改成跳转语句。

根据函数调用的原理可以灵活构造花指令,不一定使用CALLRET组合,如下命令:

#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;
}

通过压栈、退栈和跳转混淆代码,IDA将退栈指令识别为破坏堆栈平衡而报错,如下所示:
请添加图片描述识别这类花指令后,将跳转和无效压栈等操作指令全部NOP掉即可。

2.混淆特征码

这种类别的花指令组合形式很多,用于混淆木马、病毒的特征码,躲避AV查杀,不一定会造成反汇编失败,但是会对反汇编分析造成干扰。

如下一些指令功能可以用替代指令完成,目的即增加反汇编分析的复杂程度:

mov op1,op2     ---->    push op2 / pop op1
jmp label       ---->    push label / ret
call label      ---->    push label_next_instruction / push label / ret
push op         ---->    sub esp,4 / mov [esp],op

一些AV查杀引擎在可执行文件一定偏移范围内进行扫描查杀,在扫描区段加入一些花指令,使恶意代码偏离引擎识别的偏移范围,再使用工具修改程序入口 (OEP),就可以逃避这种方式的查杀。

posted on 2023-01-20 09:34  不及格的程序员-八神  阅读(377)  评论(0编辑  收藏  举报