花指令学习笔记(持续更新)

第一篇文章

花指令的介绍

花指令(JunkCode)指的是使用一些技巧将代码复杂化,使人难以阅读的技术。广义上花指令与代码混淆(ObfusedCode)同义,包括结构混淆、分支混淆、语句膨胀等等

狭义上指的主要是干扰反汇编解析的技术。

花指令的原理

本质

  1. 反汇编器无法维护执行上下文,只能静态分析
  2. x86指令集是不定长指令集,每条指令的长度不确定。

线性扫描

早期反汇编器通常使用线性反汇编技术,如hex-dump, OllyDbg
即从入口点或是代码段开头,逐条语句进行反汇编。
但这样的实现很容易被干扰。
考虑这样一段代码:

jmp label1 db 0xe8 ; 线性反汇编器会从这里开始分析 label1: nop ; CPU从这里开始运行

当CPU执行的时候,遇到jmp label1语句就会将label1的地址写入IP寄存器
而反汇编器由于是线性扫描,则会从脏字节处开始反汇编
另外还可能由于起始地址错误导致大量指令反汇编错误

递归下降

现代反汇编器则会使用改良的递归下降技术进行反汇编,如IDA Pro。
这种技术的优点在于结合了动态执行的思想,根据跳转jmp和call的目的地址决定反汇编的起始地址
从而对抗上述花指令
但本质问题并没有解决,所以仍然可以进行干扰
考虑这样一段代码:

jz label1 jnz label1 db 0xe8 ; 干扰字节 label1: nop ; 正常指令

由于jzjnz都存在理论上的连续向下执行分支,所以IDA仍然会优先反汇编干扰字节,导致反汇编出错
而这里由于两条条件跳转指令的组合使用,产生了如jmp一样的效果

除了上述两种状态以外还有很多可以导致反汇编出错的技术,究其本质都是反汇编是静态的原因。

花指令的识别

反汇编错误通常会有三个特征

  1. call目的地址畸形
  2. 跳转到某条指令的中间,IDA中形如地址+x的样子
  3. 大量不常见、不合理的指令(由于反汇编错位而出现)

但反汇编错误并不意味着花指令,还可能是SMC(代码自解密)
具体可以考虑通过动态调试查看执行时的情况(通解!!!)

将附件中的 easy_junkcode 用 IDA64 打开,观察 main 函数可以观察到花指令的上述特征。

在这里插入图片描述

该位置实际上存在如下花指令:

__asm__( "push rax;"
         "xor rax,rax;" 
         "jz $+3;" 
         ".byte 0xE9;" 
         "pop rax;" );

由于 IDA无法准确判断出 jz $+3; 这条指令一定跳转,因此将 .byte 0xE9; 识别成汇编指令导致反汇编错误。
将 0x1157 开始的代码按快捷键 U undefine 然后在 0x1158 处按快捷键 C 将其识别为代码,此时反汇编结果正确。
在这里插入图片描述

手动去除花指令

通过Patch可以修改字节,使代码与其预期,即执行时的状态一致即可。
比如可以将 0xE9 patch 成 0x90 使其反汇编为 nop 指令。
在这里插入图片描述
然后在 main 函数开始处按 P 快捷键让 IDA 重新分析该函数。
此时 main 函数可以正常识别。
在这里插入图片描述

花指令的其他影响

修复完成后按F5仍然会报错
在这里插入图片描述
这是因为该程序中除了干扰反汇编的花指令以外,还有干扰反编译的花指令
0x1165 开始的花指令和前面的花指令原来相似,这条花指令会使 IDA 误以为 0x116B 处的指令可能会执行,导致 IDA 的栈分析出现错误。
在这里插入图片描述
修复方法除了前面的 patch 外还有修改 ida 对栈的分析结果。
在Options - General菜单中勾上Stack pointer选项可以查看每行指令执行之前的栈帧大小
在这里插入图片描述
Alt + K 可以修改某条指令对栈指针的影响,从而消除这条花指令对反编译的影响。
在这里插入图片描述
修改后反编译正常。
在这里插入图片描述

利用脚本去除花指令 简单替换

用IDA打开hard_junkcode,可以发现main函数中存在花指令jz + jnz + xxx
从上往下阅读可以发现一共有三处花指令,分别在0x7540x7710x786地址,类型如下图所示:
在这里插入图片描述
观察三处花指令发现它们的机器码全都是740A7508E810000000EB04E8这一串字节序列
因此可以直接全局替换这一段内容为0x90,即NOP的机器码

from ida_bytes import get_bytes, patch_bytes 
patch_bytes(0x740, get_bytes(0x740, 0x100).replace(bytes.fromhex("740A7508E810000000EB04E8"), bytes.fromhex("90" * 12)))

运行脚本后 main 函数可正常识别。
在这里插入图片描述

花指令的分类

常见的花指令有以下几种

  1. jx + jnx
    在这里插入图片描述

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

  2. call + pop
    在这里插入图片描述

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

  3. call + add esp, 4
    在这里插入图片描述

    用add esp的方式来清除call的压栈,使栈平衡。从而用call实现jmp。

  4. call + add [esp], n + retn
    82ea591e6fc4fdefbbda2486ba69f453

    用add [esp], n和retn的方式来改变返回地址。

利用脚本去除花指令 复杂处理

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

call 花指令

  • call + pop
    例如 0x00401F9B 处的花指令2322837feea60f8d89b790c3059ed33e另外还有 push eax + call + pop eax + pop eax 类型的。
  • call + add esp, 4
    例如 0x00401F62 处的花指令
    e8cbeb3d7cb037cd3189d136ea2fe5a6
  • call + add [esp], 6 + retn
    例如 0x00401FA3 处的花指令
    bf0eedae9f017772783a306ac571c77e

对于这种花指令,先用正则表达式 /\x50\xE8(.{4})(.*?)\x58\x58/ 特判 push eax + call + pop eax + pop eax 类型的,之后可用正则表达式 /\xE8(.{4})(.*?)(\x83\xC4\x04|\x58|\x83\x04\x24\x06\xc3)/ 进行匹配,即 \xE8 + 4字节立即数 + 任意长度字节的填充 + 后续特征字节 。同时根据 call 地址的计算方式可知 call 还要确保立即数要等于后面字节填充的长度。

def call_handler(s): def work(pattern, s): t = s[:] for _ in range(end - start): it = re.match(pattern, s[_:], flags=re.DOTALL) if it is None: continue if struct.unpack("<I", it.group(1))[0] == len(it.group(2)): l, r = it.span() l += _ r += _ p(s[l:r]) t = t[:l] + b"\x90" * (r - l) + t[r:] return t s = work(rb"\x50\xE8(.{4})(.*?)\x58\x58", s) s = work(rb"\xE8(.{4})(.*?)(\x83\xC4\x04|\x58|\x83\x04\x24\x06\xc3)", s) return s

jx + jnx 花指令

例如 0x00402D67 处的花指令

09cb1ffd82b12bf4675d30830dc24ee7这类花指令可以先用正则表达式 /([\x70-\x7F])(.)([\x70-\x7F])(.).*/ 进行过滤,然后做如下检测:

  • 两个跳转指令的第一个字节相差 1 且较小的那个是偶数。
  • 前一个跳转的立即数比后一个多 2 。

因此可用如下方式去除,注意花指令包含特殊字符,在构造正则表达式时应注意转义。

def jx_jnx_handler(s): for _ in range(0x70, 0x7F, 2): def work(pattern, s): t = s[:] for _ in range(end - start): 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] if num1 != num2 + 2: continue l, r = it.span() l += _ r += _ + num2 if num2 <= len(s): p(s[l:r]) 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"(.)" s = work(pattern, s) pattern = op2 + b"(.)" + op1 + b"(.)" s = work(pattern, s) return s

fake jmp 花指令

例如 0x00401FB2 这处花指令:
在这里插入图片描述
这里有很多跳转,但分析后发现这些跳转都可以忽略。由于这一类花指令比较单一,因此直接匹配特征即可:

def fake_jmp_handle(s): def work(pattern, s): t = s[:] for _ in range(end - start): it = re.match(pattern, s[_:], flags=re.DOTALL) if it is None: continue l, r = it.span() l += _ r += _ p(s[l:r]) t = t[:l] + b"\x90" * (r - l) + t[r:] return t 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

stx + jx 花指令

例如 0x0040261F 和 0x004026D7 两处花指令:
83f421c2af9eae9bcd840757fc589b92

d8acf30e982fdd08590986d07a794d90

此类花指令本质是通过设置标志寄存器的值使得满足后面的条件跳转。
由于此类指令较少,直接匹配特征即可。注意,如果仅匹配前 2 个字节,那么可能会将某些指令中间的字节匹配上,这里通过 jx 跳转的距离来做简单的过滤。

def stx_jx_handler(s): t = s[:] pattern = rb"(?:\xF8\x73|\xF9\x72)(.)" for _ in range(end - start): 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] if r - l > 0x40: continue p(s[l:r]) t = t[:l] + b"\x90" * (r - l) + t[r:] return t

完整代码

import ida_bytes
from idaapi import get_bytes, patch_bytes
import re
import struct

start = 0x00401000
end = 0x004B9CD0


def p(s): print(''.join(['%02X ' % b for b in s]))


def call_handler(s):
    def work(pattern, s):
        t = s[:]
        for _ in range(end - start):
            it = re.match(pattern, s[_:], flags=re.DOTALL)
            if it is None: continue
            if struct.unpack("<I", it.group(1))[0] == len(it.group(2)):
                l, r = it.span()
                l += _
                r += _
                p(s[l:r])
                t = t[:l] + b"\x90" * (r - l) + t[r:]
        return t

    s = work(rb"\x50\xE8(.{4})(.*?)\x58\x58", s)
    s = work(rb"\xE8(.{4})(.*?)(\x83\xC4\x04|\x58|\x83\x04\x24\x06\xc3)", s)
    return s


def jx_jnx_handler(s):
    for _ in range(0x70, 0x7F, 2):
        def work(pattern, s):
            t = s[:]
            for _ in range(end - start):
                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]
                if num1 != num2 + 2: continue
                l, r = it.span()
                l += _
                r += _ + num2
                if num2 <= len(s):
                    p(s[l:r])
                    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"(.)"
        s = work(pattern, s)
        pattern = op2 + b"(.)" + op1 + b"(.)"
        s = work(pattern, s)
    return s


def fake_jmp_handle(s):
    def work(pattern, s):
        t = s[:]
        for _ in range(end - start):
            it = re.match(pattern, s[_:], flags=re.DOTALL)
            if it is None: continue
            l, r = it.span()
            l += _
            r += _
            p(s[l:r])
            t = t[:l] + b"\x90" * (r - l) + t[r:]
        return t

    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


def stx_jx_handler(s):
    t = s[:]
    pattern = rb"(?:\xF8\x73|\xF9\x72)(.)"
    for _ in range(end - start):
        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]
        if r - l > 0x40: continue
        p(s[l:r])
        t = t[:l] + b"\x90" * (r - l) + t[r:]
    return t


if __name__ == '__main__':
    ops = get_bytes(start, end - start)
    ops = call_handler(ops)
    ops = fake_jmp_handle(ops)
    ops = jx_jnx_handler(ops)
    ops = stx_jx_handler(ops)
    patch_bytes(start, ops)
    print("done")


运行效果

运行后 patch 掉了大量的花指令,可以进行反编译。
58c925ce4d37e8cb62cf4c6c0761cb54

                版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

原文链接:https://blog.csdn.net/qq_45323960/article/details/128056134


第二篇文章

3.入门花指令

a.互补条件代替jmp跳转

asm
{
  Jz Label
  Jnz Label
  Db thunkcode;垃圾数据
Label:
}

类似这种,无论如何都会跳转到label1处,还是能骗过反编译器。

b.跳转指令构造花指令

     __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出来。

t01962142291c823869

c.call&ret构造花指令

     __asm {
         call LABEL9;
         _emit 0x83;
     LABEL9:
         add dword ptr ss : [esp] , 8;
         ret;
         __emit 0xF3;
     }

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

ret指令的本质:pop eip

代码中的esp存储的就是函数返回地址,对[esp]+8,就是函数的返回地址+8,正好盖过代码中的函数指令和垃圾数据。(这部分建议自己调试一下)

4.创意花指令

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

a.替换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指令。

当然,这种构造跳转指令可以变化多样。

b.控制标志寄存器跳转

这一部分需要精通标志寄存器,每一个操作码都会对相应的标志寄存器产生相应的影响,如果我们对标志寄存器足够熟练,就可以使用对应的跳转指令构造永恒跳转!。

c.利用函数返回确定值

有些函数返回值是确定的,比如我们自己写的函数,返回值可以是任意非零整数,就可以自己构造永恒跳转

还有些api函数也是如此:

一方面可以传入一些错误的参数,如LoadLibraryA

HMODULE LoadLibraryA(
  LPCSTR lpLibFileName
);

如果我们故意传入一个不存在的模块名称,那么他就会返回一个确定的值NULL,我们就可以通过这个构造永恒跳转

另一方面,某些api函数,我们既然使用他,肯定就是一定要调用成功的,而这些api函数基本上只要调用成功就就会返回一个确定的零或者非零值,如MessageBox

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

该api只有在其调用失败的时候才能返回零,那么我们也可以通过这一点构造永恒跳转

PS:利用MessageBox实现花指令也是我在1.花指令简介中用到的源码

5.花指令原理另类利用

当我们理解了花指令的原理后,我们可以在将花指令中的垃圾数据替换为一些特定的特征码,可以对应的,尤其在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自解密。

6.花指令的看法

重点:构造永恒跳转,添加垃圾数据!

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

本文由1nt3原创发布

转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/236490

安全客 - 有思想的安全新媒体

网上总结的文章特别多,主要还是得实际经历过,所以我这里放一个Newstar2024-Week3的题:

flowering_shrubs(例题)

好题,让我既学到了一种花指令,也逼着我学习.

花指令的图片如下:

image-20241220113431173

.text:00000000000014D5 008 50                            push    rax
.text:00000000000014D6 010 51                            push    rcx
.text:00000000000014D7 018 52                            push    rdx
.text:00000000000014D8 020 53                            push    rbx
.text:00000000000014D9 028 E8 00 00 00 00                call    $+5
.text:00000000000014D9
.text:00000000000014DE
.text:00000000000014DE                                   loc_14DE:                               ; DATA XREF: main+17↓o
.text:00000000000014DE 030 5B                            pop     rbx
.text:00000000000014DF 028 48 81 C3 12 00 00 00          add     rbx, (offset sub_14F0 - offset loc_14DE)
.text:00000000000014E6 028 48 89 5C 24 18                mov     [rsp+20h+var_8], rbx
.text:00000000000014EB 028 48 83 C4 18                   add     rsp, 18h
.text:00000000000014EF 010 C3                            retn
.text:00000000000014EF
.text:00000000000014EF                                   main endp ; sp-analysis failed
.text:00000000000014EF
.text:00000000000014F0
.text:00000000000014F0                                   ; =============== S U B R O U T I N E =======================================
.text:00000000000014F0
.text:00000000000014F0
.text:00000000000014F0                                   sub_14F0 proc near

分析一下,push没啥用,说是为了避免影响,但其实后面直接add rsp,18h了,算是一种定式.

接着call $+5,唬人用的.

然后add rbx,call有入栈操作,存的是下一个的地址,可以从指针+0x8看出.pop出来后再加上偏移量,就是下一个函数的地址.

最后存入,retn过去.

接下来就是写脚本了,直接贴官方题解的脚本(得学了):

# remove_flower.py
import idc
import idaapi
startaddr=0x1100
endaddr=0x15FF
lis=[0x50, 0x51, 0x52, 0x53, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x5B, 0x48, 0x81, 0xC3, 0x12, 0x00, 0x00, 0x00, 0x48, 0x89, 0x5C, 0x24, 0x18, 0x48, 0x83, 0xC4, 0x18,0xC3]
#这个for循环是关键点,检测以当前地址开始的27个字节是否符合lis列表的内容。
for i in range(startaddr,endaddr):
    flag=True
    for j in range(i,i+27):
        if idc.get_wide_byte(j)!=lis[j-i]:
            flag=False
    if flag==True:
        for addr in range(i,i+27):
            idc.patch_byte(addr,0x90) # 将这部分内容全部nop掉

for i in range(startaddr,endaddr):# 取消函数定义
    idc.del_items(i)
for i in range(startaddr,endaddr):       # 添加函数定义
    if idc.get_wide_dword(i)==0xFA1E0FF3: #endbr64
        idaapi.add_func(i)

后面的解密就不多说了:

#solve.py
lis=[0x54,0xf4,0x20,0x47,0xfc,0xc4,0x93,0xe6,0x39,0xe0,
     0x6e,0x00,0xa5,0x6e,0xaa,0x9f,0x7a,0xa1,0x66,0x39,
     0x76,0xb7,0x67,0x57,0x3d,0x95,0x61,0x22,0x55,0xc9,
     0x3b,0x4e,0x4f,0xe8,0x66,0x08,0x3d,0x50,0x43,0x3e]
str="uarefirst."
offset_buf=[0,4,32,12,8,24,16,20,28,36]
#offset_buf就是通过动态调试提取出每一轮get_next_rand函数的返回值得到的
truekey=[]
for i in str:
    truekey.append(ord(i))
def decrypt(offset,key):
    a=lis[offset]
    b=lis[offset+1]
    c=lis[offset+2]
    d=lis[offset+3]
    flagc=((c+key)&0xff)^b
    flagd=c^d
    flaga=a^d^key
    flagb=((b-key)&0xff)^flaga^key
    lis[offset]=flaga
    lis[offset+1]=flagb
    lis[offset+2]=flagc
    lis[offset+3]=flagd
for i in range(10):
    decrypt(offset_buf[i],truekey[i])
print(bytes(lis).decode('utf-8'))
# flag{y0u_C4n_3a51ly_Rem0v3_CoNfu510n-!!}
posted @   T0fV404  阅读(78)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示