使用 IDA 插件自动对 x64 代码进行基于模式的去混淆

逆向工程 艾达 视窗 蟒蛇 去混淆 x64
2021-06-10 05:33:21

尝试对 x64 代码进行反混淆。您可以略读/跳过大部分内容,因为它的存在主要是为了证明我已经尝试并用尽了所有途径。

燃烧目的

在 64 位 Windows 游戏的 [内存转储] 中大规模自动化反混淆。

目前使用的方法

PCRE 驱动的字节替换

像 Variant 1 & 2 这样的简单混淆效果很好

原始(变体 1):

48 8D 64 24 F8        - lea rsp,[rsp-08]         ; Stack -= 8
48 89 2C 24           - mov [rsp],rbp            ; Push RBP
48 8D 2D 156A5A00     - lea rbp,[7FF749022784]   ; Put JMP target in RSP
48 87 2C 24           - xchg [rsp],rbp           ; Pop RBP (RBP restored)
48 8D 64 24 08        - lea rsp,[rsp+08]         ; Stack += 8 (Balanced)
FF 64 24 F8           - jmp qword ptr [rsp-08]   ; JMP (target)

原始(变体 2):

48 89 6c 24 f8         - mov [rsp-0x8], rbp
48 8d 64 24 f8         - lea rsp, [rsp-0x8]
(rest as per Variant 1)

去混淆:

90 90 90 .. ..        - (Variant 1: NOP * 9, Variant 2: NOP * 10)
90 90                   NOP * 2   ; Pad instruction to preserve
                                  ; RIP of next instruction
E9 ?? ?? ?? ??        - JMP NEAR 
90 90 90 .. ..        - NOP * 13

去混淆脚本:

#!/usr/bin/env sh
#            |------------- 11 bytes--------| |-- 5 bytes--| |---------------- - 13 bytes---------|     
# Signature: 48 8D 64 24 F8 48 89 2C 24 48 8D 2D ?? ?? ?? ?? 48 87 2C 24 48 8D 64 24 08 FF 64 24 F8 (29 bytes)
# Translate: 90 90 90 90 90 90 90 90 90 90 90 E9 ?? ?? ?? ?? 90 90 90 90 90 90 90 90 90 90 90 90 90
#    
#            |------------- 12 bytes ----------| |-- 5 bytes--| |---------------- - 13 bytes---------|
# Signature: 48 89 6c 24 f8 48 8d 64 24 f8 48 8d 2d ?? ?? ?? ?? 48 87 2c 24 48 8d 64 24 08 ff 64 24 f8 (30 bytes)
# Translate: 90 90 90 90 90 90 90 90 90 90 90 90 e9 ?? ?? ?? ?? 90 90 90 90 90 90 90 90 90 90 90 90 90

xxd -ps Game_Dumped.exe | 
    sed -e 's/\(..\)/\1 /g' | 
    tr '\n' ' '             |
    perl -p -e "s/48 8d 64 24 f8 48 \
89 2c 24 48 8d 2d (.. .. .. ..) \
48 87 2c 24 48 8d 64 24 08 ff 64 24 f8/90 90 90 90 90 \
90 90 90 90 90 90 e9 \1 90 90 90 90 90 90 90 90 90 90 \
90 90 90/g ; s/48 89 6c 24 f8 48 8d 64 24 f8 48 8d 2d\
 (.. .. .. ..) 48 87 2c 24 48 8d 64 24 08 ff 64 24 \
f8/90 90 90 90 90 90 90 90 90 90 90 90 e9 \1 90 90 90 \
90 90 90 90 90 90 90 90 90 90/g" |
    xxd -r -ps > Game_Dumped_NOP2.exe

第三个品种

143fe7a26 48 89 6c 24 f8         mov     [rsp-8], rbp
143fe7a2b 48 8d 64 24 f8         lea     rsp, [rsp-8]
143fe7a30 e9 60 71 8d ff         jmp     loc_1438beb95

1438beb95 48 8d 2d 10 b0 3a fd   lea     rbp, sub_jump_target
1438beb9c 48 87 2c 24            xchg    rbp, [rsp]
1438beba0 48 8d 64 24 08         lea     rsp, [rsp+8]
1438beba5 ff 64 24 f8            jmp     qword ptr [rsp-8]

这与变体 2 相同,只是它被分成了两个部分。计划的解决方案将涉及 distorm3 的流量控制标志,并重写jmp0x143fe7a26.

堆栈操作的变化

48 89 E0             mov     rax, rsp
48 05 F8 FF FF FF    add     rax, 0FFFFFFFFFFFFFFF8h ; Add
48 89 C4             mov     rsp, rax
48 89 1C 24          mov     [rsp], rbx

RSP8 的效果真是太可怕了。但现在,我已经习惯了,它不会打扰我,但我刚刚在 IDA [6.8] 的常规选项中启用了“堆栈指针” ,并意识到 IDA 不包括lea rsp, [rsp+-8]在它的堆栈指针计算中,这会阻止它正确分析代码。

RSP Bytes               Disassembly
--- ------------------- -------------------------------
000 48 89 E0            mov     rax, rsp
000 48 05 F8 FF FF FF   add     rax, 0FFFFFFFFFFFFFFF8h
000 48 89 C4            mov     rsp, rax ; It tracked this
-20 48 89 1C 24         mov     [rsp], rbx
-20 48 83 EC 20         sub     rsp, 20h ; and this
000 48 8B 41 10         mov     rax, [rcx+10h]
000 48 89 4C 24 F8      mov     [rsp-8], rcx
000 48 8D 64 24 F8      lea     rsp, [rsp-8] ; but not this
000 48 8B 1C 24         mov     rbx, [rsp]

我也开始怀疑所有这些技术会有很多排列,我需要开始在 IDA 中解决这个问题。

问题是,我能找到唯一示例源使用 IDAPython idaapi低级函数,所以代码长得离谱,而且当我用 4 字节指令替换 5 字节指令时,我找不到改变操作数的方法我无意中创建的。(幸运的是,在这种情况下,它只是 CLC)。

更新:我已经解决了这个问题,该解决方案大大减少了我的脚本的大小。相关修复如下:

def replace_pattern(ea):
    search = [0x48, 0x8d, 0x64, 0x24, 0xf8]
    replace = [0x48, 0x83, 0xec, 0x08, 0x90]
    current = []
    for i in xrange(5):
        current.append(idaapi.get_byte(ea+i))
    if 0 == cmp(search, current):
        for i in xrange(5):
            # fixed: replace put_byte with patch_byte
            idaapi.patch_byte(ea+i, replace[i])
        return 1
    return 0

【原代码】顺便说一下,示例代码是我们自己的Rolf Rolles写的

import idaapi
import idc

# Planned task: replace
#     48 8d 64 24 f8          lea    rsp,[rsp-0x8]
# with
#     48 83 ec 08             sub    rsp,0x8
#     90                      nop
#
# Actual result:
# Replaced:  48 8d 64 24 f8   lea    rsp,[rsp-0x8]
# with:   :  48 83 ec 08      sub    rsp,0x8
#            f8               clc
#
# Verdict, close enough, but way too much code involved.

def match_pattern(ea):
    search = [0x48, 0x8d, 0x64, 0x24, 0xf8]
    replace = [0x48, 0x83, 0xec, 0x08, 0x90]
    current = []
    for i in xrange(5):
        current.append(idaapi.get_byte(ea+i))
    if 0 == cmp(search, current):
        return 1
    return 0

    # Note: I thought I might be able to simply rewrite
    #       at a byte level, but it threw an exception.
    #
    #    for i in xrange(4):
    #        idaapi.put_byte(ea+i, replace[i])

class deobfu_hook(idaapi.IDP_Hooks):
    def __init__(self):
        idaapi.IDP_Hooks.__init__(self)
        self.n = idaapi.netnode("$ X86 Deobfuscator Modifications",0,1)

    def custom_ana(self):
        # Check first two bytes "by hand" for speed
        b = idaapi.get_byte(idaapi.cmd.ea)
        if b == 0x48: # First byte
            b = idaapi.get_byte(idaapi.cmd.ea+1)
            if b == 0x8d: # Second byte
                # Discard speed, do a full match
                if match_pattern(idaapi.cmd.ea, 0, 0):
                    # If matched, supply all required values for 
                    # SUB RSP,8 - Surely there is an easier way!
                    idaapi.cmd.itype = 0xd1
                    idaapi.cmd.size = 4
                    idaapi.cmd.auxpref = 0x1810
                    idaapi.cmd.segpref = 0
                    idaapi.cmd.insnpref = 0x48
                    idaapi.cmd.flags = 2

                    idaapi.cmd.Op1.type = 1
                    idaapi.cmd.Op1.offb = 0
                    idaapi.cmd.Op1.offo = 0
                    idaapi.cmd.Op1.flags = 8
                    idaapi.cmd.Op1.dtyp = 7
                    idaapi.cmd.Op1.reg = 4
                    idaapi.cmd.Op1.phrase = 4
                    idaapi.cmd.Op1.value = 0
                    idaapi.cmd.Op1.addr = 0
                    idaapi.cmd.Op1.specval = 0
                    idaapi.cmd.Op1.specflag1 = 0
                    idaapi.cmd.Op1.specflag2 = 0
                    idaapi.cmd.Op1.specflag3 = 0
                    idaapi.cmd.Op1.specflag4 = 0

                    idaapi.cmd.Op2.type = 5
                    idaapi.cmd.Op2.offb = 3
                    idaapi.cmd.Op2.offo = 0
                    idaapi.cmd.Op2.flags = 8
                    idaapi.cmd.Op2.dtyp = 7
                    idaapi.cmd.Op2.reg = 0
                    idaapi.cmd.Op2.phrase = 0
                    idaapi.cmd.Op2.value = 8
                    idaapi.cmd.Op2.addr = 0
                    idaapi.cmd.Op2.specval = 0
                    idaapi.cmd.Op2.specflag1 = 0
                    idaapi.cmd.Op2.specflag2 = 0
                    idaapi.cmd.Op2.specflag3 = 0
                    idaapi.cmd.Op2.specflag4 = 0

                    return True
        return False

class deobfu_t(idaapi.plugin_t):
    flags = idaapi.PLUGIN_PROC | idaapi.PLUGIN_HIDE
    comment = "Deobfuscator"
    wanted_hotkey = ""
    help = "Runs transparently"
    wanted_name = "deobx86"
    hook = None

    def init(self):
        self.hook = None

        self.hook = deobfu_hook()
        self.hook.hook()
        print("deobfu init")
        return idaapi.PLUGIN_KEEP

    def run(self, arg):
        pass

    def term(self):
        print("deobfu term")
        if self.hook:
            self.hook.unhook()

def PLUGIN_ENTRY():
    print("PLUGIN_ENTRY:deobfu")
    return deobfu_t()

你今天想去哪里?

我想要一个更好的解决方案,而且我不怕编码。我总是需要一些入门建议,而且我需要确保我没有在这里重新编码轮子。

从那以后,我使用更高级别的idautils编写了一些其他 IDAPython 代码来创建调用树,并整理外部参照等。但我不知道如何重写该级别的实际反汇编代码。IDAPython 存储库中有一个示例:https : //github.com/pfalcon/idapython/blob/master/examples/ex_idphook_asm.py但那是

  1. 充满愚蠢的错误(我修复了它们)
  2. 钩住汇编命令,而不是反汇编过程

我在创建 IDA Pro 调试器插件 - API 文档和示例?. 我查看了非常好的 IDAPython 代码的各种示例,这些代码可以:

但是,我没有看到实际更改指令的任何内容。

我没有购买IDA Pro Book,因为我不住在美国,我不想等待n周的按需印刷和交付。我并不反对写一个.idc,因为我对 C 非常熟悉(对 Python 更熟悉),尽管我怀疑尽管学习曲线较浅(并且假设准备好的例子)它会比使用更高的更难(长期)级别 IDAPython 代码。(我正在学习 Python,但是 .. 好吧,不是我们所有人吗?)

因为我使用的代码完全是 64 位的,所以几乎没有(基本上没有)预先存在的 deobfu 样本或代码。

所以我在这里发现自己,请求您的耐心指导。(如果您真的阅读了所有内容,请非常耐心)。

PS:我花时间记录了我所做的一切,因为我知道我们对那些在大喊大叫之前甚至不尝试做某事的人的尊重是多么的少。

PPS:OMG 2最狂热的RE的成员是伊戈尔Skochinsky,我只能低头谦卑

2个回答

有两种情况需要区分,您(隐含地)也提到了:

I. 在中断控制流之间没有 jmps 的混淆模式,

二、带有中断 jmps 的混淆模式。

肯定有几种可能性可以消除混淆。我将尝试描述我(部分)应用的其中之一。

场景 I很简单(我尝试了一些成功的场景并为它编写了一个程序):

  1. “手工”识别模式,记下字节码序列,并记下更简单的字节码替换。

  2. 将二进制文件读入自己编程的“删除混淆”应用程序。

  3. 让程序搜索所有出现的混淆字节码。

  4. 让程序用(希望更简单的)替换替换所有找到的事件。您可能会为可能的补丁获得大量空间。

  5. 非常重要:测试修改后的(即简化的)程序

场景 II是更困难的部分,因为中断可以出现在任何地方,并且对于混淆模式来说不再可能有唯一的字节码序列。我可能会按照以下方式进行,但还没有尝试过:

  1. 独立于混淆,找到一个解决方案来删除代码中的“中断”,即尝试删除通常很多很多无用的 jmp 语句。如果你能解决这个问题,它还有一个很大的额外优势,即 jmps 在 Ida 中不再破坏代码。也许已经存在一些自动化的解决方案,尽管我目前不知道。

  2. 应用场景一

就我的经验而言,场景 I 是不太常见的情况,尽管也遇到过。以下 - 真实 - 混淆的 x64 代码序列展示了删除“代码中断”的重要性。在这个序列中,程序每次在相距很远的地址之间跳转的时间不少于 6 次。

    loc_startObfuscatedSequence:                        
                    lea     rsp, [rsp-8]
                    mov     [rsp], rbp
                    mov     rbp, offset loc_target1
                    xchg    rbp, [rsp]    
                    mov     [rsp-8], rcx
                    jmp     loc_break1  

    loc_break1:     lea     rsp, [rsp-8]
                    jmp     loc_break2

    loc_break2:     mov     [rsp-8], rdx
                    jmp     loc_break3

    loc_break3:     lea     rsp, [rsp-8]
                    jmp     loc_ break4

    loc_ break4:    mov     rcx, [rsp+10h]
                    mov     rdx, offset loc_target2
                    cmovz   rcx, rdx
                    jmp     loc_break5

    loc_break5:     mov     [rsp+10h], rcx
                    lea     rsp, [rsp+8]
                    mov     rdx, [rsp-8]
                    jmp     loc_break6

    loc_break6:    lea     rsp, [rsp+8]
                    mov     rcx, [rsp-8]
                    retn

如果我正确地解释了这一点,混淆的序列将替代简单的

jz  loc_target2
jmp loc_target1

更复杂的模式(例如示例之一)可能包含更简单的模式。因此,您将从简单的模式开始,然后从更简单的模式转向更复杂的模式,通过每个去混淆步骤提高代码的可读性(并使 Ida 更满意)。

import struct

def readWord(array, offset):
    return struct.unpack_from("<i", bytearray(array), offset)[0]

def writeWord(array, offset, word):
    array[offset:offset+4] = bytearray(struct.pack("<i", word))

class Obfu(object):
    # Copyright 2016 Orwellophile LLC. MIT License.
    def __init__(self, start, end):
        self.start = start
        self.end = end
        self.eip = start
        self.patterns = []

    def append(self, searchText, replaceText, search, replace, replaceFunction = None):
        self.patterns.append([search, replace, replaceFunction])

    def replace_pattern(self, search, replace, replaceFunction, ea):
        searchSize = len(search)
        replaceSize = len(replace)
        # buf = idc.GetManyBytes(ea, ItemSize(ea))
        failed = 0
        for i in range(searchSize):
            if search[i] != -1 and idaapi.get_byte(ea+i) != search[i]:
                failed = 1
                break
        if not failed:
            if replaceFunction != None:
                original = []
                for i in range(max(searchSize,replaceSize)):
                    original.append(idaapi.get_byte(ea+i))
                replace = replaceFunction(search, replace, original, ea)
                replaceSize = len(replace)
            for i in range(replaceSize):
                if replace[i] != -1:
                    idaapi.patch_byte(ea+i, replace[i])
            print "patched %i bytes at 0x%x" % (replaceSize, ea)
            MakeCode(ea) # else it turns it into data
        return not failed

    def _patch(self, ea):
        for pattern in self.patterns:
            if self.replace_pattern(pattern[0], pattern[1], pattern[2], ea):
                return 1
        return 0

    def get_next_instruction(self):
        while self.eip <= self.end:
            yield [hex(self.eip), self._patch(self.eip)]
            self.eip += ItemSize(self.eip)

# Usage

obfu = Obfu(SegStart(ScreenEA()), SegEnd(ScreenEA()))

# Patch factory
def generate_patch1(jmpTargetOffset, oldRip, newRip):
    def patch(search, replace, original, ea):
        result = [0xcc]*len(original) # preallocate result with 0xcccccc...

        jmpTarget = readWord(original, jmpTargetOffset)
        adjustTarget = oldRip - newRip
        jmpTarget = jmpTarget + adjustTarget
        result[0] = 0xE9 # JMP rel32off
        writeWord(result, 1, jmpTarget)
        return result
    return patch

# Uses Patch Factory generated callback
obfu.append("""
        0:  48 8d 64 24 f8          lea    rsp,[rsp-0x8]
        5:  48 89 2c 24             mov    QWORD PTR [rsp],rbp
        9:  48 8d 2d 00 00 00 00    lea    rbp,[rip+0x0]        # 0x10
        10: 48 87 2c 24             xchg   QWORD PTR [rsp],rbp
        14: 48 8d 64 24 08          lea    rsp,[rsp+0x8]
        19: ff 64 24 f8             jmp    QWORD PTR [rsp-0x8]
        """, "test of replacement function (patch callback)",
        [0x48, 0x8D, 0x64, 0x24, 0xF8, 0x48, 0x89, 0x2C, 0x24, 0x48, 0x8D, 0x2D, -1, -1, -1, -1, 0x48, 0x87, 0x2C, 0x24, 0x48, 0x8D, 0x64, 0x24, 0x08, 0xFF, 0x64, 0x24, 0xF8], 
        # unused:
        [0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0xE9, -1, -1, -1, -1, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90],
        generate_patch1(0x0c, 0x10, 0x05)
        )

# Byte patch
obfu.append("""
        0:  48 89 6c 24 f8          mov    QWORD PTR [rsp-0x8],rbp
        5:  48 8d 64 24 f8          lea    rsp,[rsp-0x8]
        a:  48 8d 2d 00 00 00 00    lea    rbp,[rip+0x0]        # 0x11
        11: 48 87 2c 24             xchg   QWORD PTR [rsp],rbp
        15: 48 8d 64 24 08          lea    rsp,[rsp+0x8]
        1a: ff 64 24 f8             jmp    QWORD PTR [rsp-0x8]
        """, 
        "jmp ${ readWord(0x0d) + (oldRip = 0x11) - (newRip = 0x05) }",
        [0x48, 0x89, 0x6c, 0x24, 0xf8, 0x48, 0x8d, 0x64, 0x24, 0xf8, 0x48, 0x8d, 0x2d, -1, -1, -1, -1, 0x48, 0x87, 0x2c, 0x24, 0x48, 0x8d, 0x64, 0x24, 0x08, 0xff, 0x64, 0x24, 0xf8],
        [0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0xe9, -1, -1, -1, -1, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90]
        )

# This patch disables one of the longer patches, keep at end of list
obfu.append(  "lea    rsp,[rsp-0x8]",   # First two arguments aren't used
        "", # "sub    rsp,0x8; nop",    # They're just there to remind you
        [0x48, 0x8d, 0x64, 0x24, 0xf8], 
        [0x48, 0x83, 0xec, 0x08, 0x90])

# Test single replacement with: 
# obfu._patch(address)
#
# or uncomment next lines to patch everything:
#
# x = obfu.get_next_instruction()
# count = 0
# while x.next(): count = count + 1