尝试对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/\(..\)/ /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  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  90 90 90 \
90 90 90 90 90 90 90 90 90 90/g" |
    xxd -r -ps > Game_Dumped_NOP2.exe


第3个变种

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的流控制标志,并将jmp重写为0x143fe7a26

堆栈操作的变化

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


要走很长的路要走将RSP减8的效果。但是到目前为止,我已经习惯了,它不会打扰我,但是我刚刚在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但这是


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

我已经查看了创建IDA Pro调试器插件-API文档和示例中类似问题的答案。我看过许多不错的IDAPython代码示例,它们可以:


在反汇编时添加注释
在反汇编时更改线条颜色

但是,我没有看到有关实际更改说明的任何信息。

我还没有购买IDA Pro Book,因为我不住在美国,也不想等待n周才能按需打印和交付。我对编写.idc并不不利,因为我对C相当熟悉(因此对Python更为了解),尽管我怀疑尽管学习曲线较浅(并且假定准备好了示例),但比起使用它会更难(长期)更高级别的IDAPython代码。 (我正在学习Python,但是..好吧,不是所有人吗?)

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

所以在这里我发现自己,要求您的耐心指导。 (如果您真的读过所有这些,请耐心等待。)

PS:我花时间记录了我所做的一切,因为我知道我们所有人对在寻求帮助之前甚至没有尝试过某些东西的人都没有多大的尊重。

PPS:OMG第二热RE的成员是Igor Skochinsky,我只能谦卑地鞠躬。

评论

您可以直接从No Starch通过电子书

我没有时间写完整的答案,但请查看Rolf的这篇文章

@IgorSkochinsky忽略了先前的评论-修复了Rolf代码的问题(已更新问题),我需要使用patch_byte而不是put_byte。

@IgorSkochinsky我已经了解到Rolf的代码做了一些与我认为的完全不同的事情,即它更改了IDA显示/解释的指令,而没有实际更改底层字节。这是一个非常有用(高级)的工具,但是我又遇到了一个问题,即当替换指令较短时,如何处理剩余的额外指令。有没有办法在当前的回调实例中处理此问题,还是有必要设置一个全局/静态变量并在下一次调用时捕获它?

我正是出于这个目的编写了一些C ++。“ InterObfu”部分提供了更多详细信息:x64dbg.com/blog/2016/10/30/weekly-digest-10.html

#1 楼

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

I。 II。中断控制流之间没有混淆的混淆模式

II。带有中断jmp的混淆模式。

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

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


手动识别模式,记下字节码序列,并记下-更简单的字节码替换。
将二进制文件读取到自己编程的“消除混淆”应用程序中。
/>让程序搜索混淆字节码的所有出现。
让程序用(希望更简单)替换来替换所有找到的出现。您可能会获得很多可能的补丁程序空间。
非常重要:测试修改后的程序(即简化的程序)

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


独立于混淆处理,找到一种解决方案来删除代码中的“中断”,即尝试删除通常很多很多无用的jmp语句。如果您可以解决此问题,它还有一个很大的附加优势,即jmps不再在Ida中破坏代码。也许已经存在一些自动化解决方案,尽管目前我还不知道。
应用方案I

根据我的经验,虽然也遇到过方案I,但是这种情况较少见。下面的-真实的-模糊的x64代码序列演示了删除“代码中断”的重要性。在此序列中,程序每次至少在彼此相距很远的地址之间跳转时,发生的中断不少于六个。

    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


如果我正确地解释了这一点,则混淆sequence可以代替简单的

jz  loc_target2
jmp loc_target1


更复杂的模式(例如示例之一)可能包含更简单的模式。结果,您将从简单的模式开始,然后从简单的模式转变为更复杂的模式,从而在每个去混淆步骤中提高代码的可读性(并使Ida更加满意)。

评论


这些也是我自己的想法。我的基于distorm3的反汇编程序将在跳过jmp时流式传输指令,并且我已经为示例顶部的那六行实现了基于签名的补丁。顺便说一句,您的示例与我的示例是如此相似,以至于我想知道它们是否不是由同一引擎生成的。其他一些技巧是mov rax,rsp add rax,0fffffffffffffffff8h mov rsp,rax或信仰乞讨lea rsp,[rsp-8] sub rsp,20h。

–嗜尿菌
16年7月18日在13:35

(返回主题)我发布此问题的原因是确定继续使用自己的解决方案还是更好的方法,或者进一步探索IDA SDK。

–嗜尿菌
16年7月18日在13:38

关于是否使用自己的程序,我认为最好的选择是您觉得可以在最短的时间内找到解决方案。 IMO拥有自己的解决方案,您可以根据自己的需求更好地定制它。我认为基本的算法工作可能是相似的。但是,我没有Ida SDK的经验,只有一些IDC。对于混淆引擎,确实存在惊人的相似性。

–乔什
16年7月18日在15:32

我不反对在.idc中进行开发,基于distorm3的解决方案是在C ++中(但没有STL,所以真的是C)。 distorms3的分解功能为每条指令生成一个与idaapi.cmd完全不同的结构。在IDA中开发它的好处是整个社区都将从中受益,而我将学习一种新技能。顺便说一句,我相信保护是由(我们不想让谷歌打开这个页面)生成的char url [] = {0x61,0x72,0x78,0x61,0x6e,0x2e,0x63,0x6f,0x6d};但是我确实想让IDA用IDA的堆栈计数器可以使用的东西替换基于LEA的BSP操作。

–嗜尿菌
16年7月18日在16:36

老实说,我永远不会在IDC中这样做。 idc作为一种语言太弱了。由于GUI的便利性和文件处理能力,我用C#做到了。如果需要更多基本知识,我的技术是C#GUI前端和带有纯C pinvoke接口的C ++ DLL后端。但也许其他人有另一种看法。顺便说一句,强烈推荐克里斯·伊格尔提到的书。一本好书。您对模糊处理提供程序的建议绝对有道理...

–乔什
16年7月18日在17:47

#2 楼

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


评论


一些描述正在发生的事情以及您为什么要这样做的文字可能会很好

– NirIzr
17年4月15日在10:12