文章目录
前言
本博客是参考了诸多大佬的文章而成的,博主本人也是菜鸟,可以看作是博主的学习笔记,因此我也会尽量写的详细,希望可以对你(萌新)有所帮助。路过大佬给个大腿抱抱/(ㄒoㄒ)/~~
简介
花指令又名垃圾代码、脏字节,英文名是junk code。花指令就是在不影响程序运行的情况下,往真实代码中插入一些垃圾代码,从而影响反汇编器的正常运行;或是起到干扰逆向分析人员的静态分析,增加分析难度和分析时间。
有句话,花指令的目的是阻挠逆向人员的分析,并不是阻止。因此,我们在去除花指令的时候,要有耐心,因为这应该不会是什么难点,慢慢来一定可以解决的。
简单花指令
花指令分为俩种:
- 不可执行花指令
- 可执行花指令
可执行花指令
顾名思义,可以执行的花指令,这部分垃圾代码会在程序运行的时候执行,但是执行这些指令没有任何意义,并不会改变寄存器的值,同时反汇编器也可以正常的反汇编这些指令。目的是为了增加静态分析的难度,加大逆向分析人员的工作量。
不可执行花指令
不可以执行的花指令,这类花指令会使反编译器在反编译的时候出错,反汇编器可能错误的反汇编这些指令。根据反汇编的工作原理,只有花指令同正常指令的前几个字节被反汇编器识别成一组无用字节时,才能破坏反汇编的结果。因此,插入的花指令应当是一些不完整的指令,被插入的不完整指令可以是随机选择的。
为了能够有效迷惑反汇编器,同时又确保代码的正确运行,花指令必须满足两个基本特征,即:
- 垃圾数据必须是某个合法指令的一部分。
- 程序运行时,花指令必须位于实际不可执行的代码路径。
原理:反汇编算法的设计缺陷
反汇编算法主要可以分为俩类:递归下降算法和线性扫描算法。
1)线性扫描算法
线性扫描算法p1从程序的入口点开始反汇编,然后对整个代码段进行扫描,反汇编扫描过程中所遇到的每条指令。线性扫描算法的缺点在于在冯诺依曼体系结构下,无法区分数据与代码,从而导致将代码段中嵌入的数据误解释为指令的操作码,以致最后得到错误的反汇编结果。
2)递归下降算法
递归下降采取另外一种不同的方法来定位指令。递归下降算法强调控制流的概念。控制流根据一条指令是否被另一条指令引用来决定是否对其进行反汇编,遇到非控制转移指令时顺序进行反汇编,而遇到控制转移指令时则从转移地址处开始进行反汇编。通过构造必然条件或者互补条件,使得反汇编出错。
关于花指令的构造
永恒跳转
最简单的jmp指令
jmp LABEL1
db junk_code;
LABEL1:
这种jmp单次跳转只能骗过线性扫描算法,会被IDA识别(递归下降)。
多层跳转
__asm {
jmp LABEL1;
_emit 68h;
LABEL1:
jmp LABEL2;
_emit 0CDh;
_emit 20h;
LABEL2:
jmp LABEL3;
_emit 0E8h;
LABEL3:
}
和单次跳转一样,这种也会被IDA识别。
为了骗过IDA,我们将上面的花指令改写一下,
__asm {
_emit 0xE8
_emit 0xFF
//_emit 立即数:代表在这个位置插入一个数据,这里插入的是0xe8
}
查看反汇编后的结果
可以看到IDA错误的识别loc_411877处的代码,成功的实现了花指令的目的。那么我们知道了如何构造,自然也就明白了如何去除,只需要将插入的立即数nop掉即可,点击0xe8和0xff,点击右键,选择patching->change byte
也可以使用一个idapython脚本添加一个快捷键,
from idaapi import *
from idc import *
def nopIt():
start = get_screen_ea()
patch_byte(start,0x90)
refresh_idaview_anyway()
add_hotkey("ctrl-N",nopIt)
这个快捷键可以将选中的地方直接nop掉。(不太清楚,我看别的文章都是说可以直接ctrl+n,我没有找到/(ㄒoㄒ)/~~)(虽然这个脚本也是从别人文章扒的h)
idapython在File - Script commannd…处 也可以Shift+F2快捷键打开。
idapython需要python环境,未安装的萌新请自行搜索安装。
其它构造形式
jnz和jz互补跳转
__asm {
jz Label;
jnz Label;
_emit 0xC7;
Label:
这种混淆去除方式也很简单,特征也很明显,因为是近跳转,所以ida分析的时候会分析出jz或者jnz会跳转几个字节,这个时候我们就可得到垃圾数据的长度,将该长度字节的数据全部nop掉即可解混淆。
跳转指令构造花指令
__asm {
push ebx;
xor ebx, ebx;
test ebx, ebx;
jnz LABEL7;
jz LABEL8;
LABEL7:
_emit 0xC7;
LABEL8:
pop ebx;
}
很明显,先对ebx进行xor之后,再进行test比较,zf标志位肯定为1,就肯定执行jz LABEL8,也就是说中间0xC7永远不会执行。
不过这种一定要注意:记着保存ebx的值先把ebx压栈,最后在pop出来。
解混淆的时候也需要稍加注意,需要分析一下哪里是哪里是真正会跳到的位置,然后将垃圾数据nop掉,本质上和前面几种没什么不同。
call&ret构造花指令
__asm {
call LABEL9;
_emit 0x83;
LABEL9:
add dword ptr ss : [esp] , 8;
ret;
__emit 0xF3;
}
一点话
关于花指令的构造还有很多有趣的玩法,博主也是边看边实践的,上面用的编程环境是Microsoft Visual C++,推荐都自己写一遍然后到ida里面看看,顺便练习一下如何去除。
下面是一些关于花指令的CTF练习,题目在NSSCTF上面都有,应该都不难,跟着做一遍应该就差不多啦!一定要实操实操实操。嗯
结尾有参考文献的链接,可以看看!
CTF练习
[HNCTF 2022 WEEK2]e@sy_flower
老规矩先查壳,无壳,32bit文件。
文件打开后就发现jz和jnz互补跳转了,后面地址+1已经提示了跳转的字节大小。
打开Options-General
可以把stack pointer打开,然后再把number of opcode bytes 设为5
选中这一行,Edit-Patch program-Change bytes
把第一个e9改为90就好,接着对main函数用P重新定义下,再F5反编译就🆗了
这里代码的逻辑就是输入的flag先互换位置,再与0x30异或就好。
enc = list('c~scvdzKCEoDEZ[^roDICUMC')
flag = []
# 与 0x30异或
for i in range(len(enc)):
flag.append(chr(ord(enc[i]) ^ 0x30))
for i in range(int(len(flag) / 2)):
tmp = flag[2 * i]
flag[2 * i] = flag[2 * i + 1]
flag[2 * i + 1] = tmp
for i in flag:
print(i, end='')
[NSSRound#3 Team]jump_by_jump
查壳后,用ida打开,
看汇编代码。
又是jz和jnz构造的互补跳转花指令,还是先option设置打开opcode bytes的显示;跟上一题一样将e8改为90(nop)
再点击黄色部分,一步一步按C转换为代码,直到没有黄色为止。然后对main函数用p重定义一下,再f5反编译
很基础的一道入门题,去除花指令后,就🆗了。
[NSSRound#3 Team]jump_by_jump_revenge
这题也是先查壳,上一题的升级版好像是,无壳,32bit。拖入ida中吧
也很简单,依旧是把E9改为90nop掉,然后再p重定义,用F5反编译下。
怎么感觉…还不如上一题hhh。这俩题好像都是新生赛的题?可能难点在代码逆向了,入门看看,大佬路过就好
EXP:
#include<iostream>
#include<string.h>
using namespace std;
int IsRight(char c){
return c<=125&&c>=32;
}
int main(){
char Str1[]="~4G~M:=WV7iX,zlViGmu4?hJ0H-Q*";
char flag[30];
int i;
for ( i = 28; i >=0; --i )
{
Str1[i]=(Str1[i] -32)-Str1[(i * i + 123) % 21];
if(!IsRight(Str1[i]))
{ int j;
for(j = 0;j<5;j++){
Str1[i]+=j*96;
if(IsRight(Str1[i]))
{
break;
}
}
}
}
puts(Str1);
[MoeCTF 2022]chicken_soup
老规矩了,查壳先
这里就是401000和401080对v4加密了,点击进去,就发现401000和401080被加花了。
俩处jz和jnz互补跳转,emmm三题jzjnz了hh。依旧是打开opcode bytes的显示,然后分别对E9改为90nop掉,然后用P再F5
exp
enc =[0xcd,0x4d,0x8c,0x7d,0xad,0x1e,0xbe,0x4a,0x8a,0x7d,0xbc,0x7c,0xfc,0x2e,0x2a,0x79,0x9d,0x6a,0x1a,0xcc,0x3d,0x4a,0xf8,0x3c,0x79,0x69,0x39,0xd9,0xdd,0x9d,0xa9,0x69,0x4c,0x8c,0xdd,0x59,0xe9,0xd7]
for i in range(len(enc)):
enc[i] = ((enc[i]//16 | enc[i]<<4)) & 0xff
print((enc[i]))
for i in range(len(enc)-1, 0, -1):
enc[i-1] -= enc[i]
print(bytes(enc))
后言
暂时先写这一点了,题目都很简单,下次有空再练一些了。
参考文章:
https://www.52pojie.cn/thread-1512089-1-1.html
https://www.anquanke.com/post/id/236490
https://blog.csdn.net/Captain_RB/article/details/123858864