原文地址:https://usualsuspect.re/article/automatic-removal-of-junk-instructions-through-state-tracking
简介:本文作者设计了一个自动移除垃圾指令的脚本(下载地址:https://github.com/usualsuspect/ida_stuff/blob/master/junk_removal/runtrace.py)
同时对VMProtect进行了部分研究,发现其没有对di,ax,bh等操作进行混淆。
另:分享翻译时找到的Tirton学习笔记:http://www.qingpingshan.com/jb/python/245017.html
在翻译时发现不少学术界研究都在用Triton:)
下面是译文内容:)
通过状态跟踪自动删除垃圾指令
发表于2018-05-18
摘要
目前有许多混淆代码的方式,但他们都是通过让代码变得难以阅读从而阻碍逆向工程进行的。其中一种常见的方式是添加大量垃圾指令来增大代码大小,使其难以区分有效指令和无效指令。
我提出了一种通过追踪执行状态来自动过滤垃圾代码的解决方案,通过使用Triton可以在混淆代码中静态分析代码并追踪产生影响的指令集(例如寄存器和栈写入)。
通过人工设置断点,我们可以递归扫描获得有效改变状态的指令并标记他们为好,或者说是有效的指令。从而自动将未标记为有效的指令标记为垃圾指令。
我已经使用Python编写了一个可以运行在IDA Pro中,通过设置入口静态移除垃圾指令,大约两百行左右,证明这个方法有效的脚本。
简介
我一直在试图研究代码混淆/虚拟化,最近我找到了一个被大量混淆,代码几乎无法阅读的恶意软件样本。扫描报告显示该样本经过商业虚拟化加壳工具VMProtect保护,在IDA中发现它不仅被虚拟化处理,代码还被加入了大量垃圾指令来达到混淆的效果。
我的目标是尝试静态移除这些垃圾指令,从而能够阅读其中的代码。我首先谷歌查找了一遍自动移除垃圾指令的工具,但没找到任何有用的东西。所以只能搜索前沿的研究分析,尝试自己解决我呢提。或许之前有其他人研究过这一方法,但我重新造了遍轮子,不过我也很高兴能够读到其他人分享的解决方案。
因为我很喜欢阅读那些难以完整记录的操作过程中遇到的问题和解决方法的文档,所以我在最初开始解决问题的时候,就打算记录整个解决的过程。逆向工程不是一个能简单跟着复现的过程,但我仍然希望本文在描述过程中能吸引更多的人产生兴趣去复现它。
什么是垃圾指令
如果我们想要移除其中的垃圾指令,第一个问题是如何判断它是不是垃圾指令。
一个简单的例子:
lea eax, dword ptr [esp]
mov eax, 7
这里的lea指令就是垃圾指令,因为它写入的eax在下一条就被重新写入了。
或是使用flag标志:
clc
add eax, num
clc (清除进位标志)在这里就是垃圾代码,因为后面的add重新设置了进位标志。
通常来说,我会设定下列的定义:
垃圾指令是没有任何指令在乎效果(产生影响)的指令。
简单的来说,就是一条写入寄存器,或是标志位,或是栈,或是内存中从未被读取过的值的指令。除了上述列举的例子外,他们往往都有一定的作用(例如写入寄存器或标志位),但没有人在他们被重新写入前在意过他们的值。
范围
我面对的第一个问题是:该从哪里开始自动代码分析,又到哪里结束?哪一部分要分析的代码是我要移除垃圾指令的。这是一个实际的大难题,因为我们必须要确认分析工作的垃圾指令从哪里开始,又到哪里结束。
我们必须确保整个代码中没有其他指令对其产生影响,才能解析一条完整的指令。然而这需要对整个程序有了解,而我在看到Von Neumann architecture(冯诺依曼体系)中的计算机难题后,迅速放弃了这一想法。
对于简单的程序(比如没有任何条件判断,只有一个程序块),我们可以轻松地看懂它,但如果时有些寄存器在其他条件块中写入,后来又访问它了呢?或者更糟糕点,有些条件块是在循环1000次编译后才访问呢?我们需要考虑到每一种可能执行的代码块。所以我试图寻找一种算法在不写入死循环的状态下追踪所有的代码。
如果我们不能满足上述需求,那么该怎么办?
这困扰了我很长一段时间,直到后来我想出了一个新方法。通过不修改代码语义,混淆器必须要确保垃圾指令不产生实际影响,比如说它如果要插入一句clc,那么它必须确保直到原始代码写入前没有任何指令依赖进位标志。这样就能确定代码混淆器在什么地方插入了垃圾指令。
这就是为什么我将问题化简为下述情况:
我们只考虑其他写入指令前会插入垃圾指令,这样可以确定他们没有任何实际用处。
简单的来说,它们就像是上面例子中的不再使用的存储一样。
那么接下来的问题是,我们如何确定去除混淆后的代码结束的位置?开始的位置可以是任意人工设置的反汇编地方,毕竟我们也不能尝试分析整个程序。所以假定存在没有意义的存储空间,那么我们可以停止写入指令,开始寻找之前的垃圾指令。这样就能搜寻在写入操作之后存在的垃圾指令了。
我设想了一个不同的方法,人工确定启动和结束分析的地方,假设在这段时间内相关的寄存器、内存、栈位置都是相互有关的,后来发现,这样通过之后重新写入当前状态可以解决过度近似的问题。
所以这就是我们定义的“从什么地方开始,从什么地方结束”的范围。
算法
最后我采用的算法过程如下:
在一些设置的断点处启动追踪
追踪每一条指令发生的状态变化,我们将每一个涉及状态变量写入操作、对当前指令产生影响的指令都保存下来。
在一些设置的断点处停止追踪,并对状态结构判断是否有有意义的操作。
在追踪之后我们还需要进行溯源。
在最终指令处记录所有状态的变量和写入的指令,标记它们为有效的操作。
对每一条有意义的指令,对其涉及的读取(无论读取的是哪个寄存器或是内存位置)指令,都查找它上一条写入的状态变量,并将其依赖的寄存器和指令标记为有效。
循环直到没有更多指令可以被判断为有效。
一个简单的例子能够说明上述过程。
下面是我们准备的混淆后的代码:
1 mov eax, 1337
2 mov ecx, eax
3 add ecx, edx
4 mov edi, 42
5 mov ebp, 3
6 lea ebp, dword ptr [eax+3]
7 xor ebp, ebp
我们追踪每一条指令记录的状态变化,从最初的空白状态到记录是哪条指令写入引起状态发生变化。
# Instruction State after instruction
0 initial empty
1 mov eax, 1337 eax:1
2 mov ecx, eax eax:1, ecx:2
3 add ecx, edx eax:1, ecx:3
4 mov edi, 42 eax:1, ecx:3, edi:4
5 mov ebp, 3 eax:1, ecx:3, edi:4, ebp:5
6 lea ebp, dword ptr [eax+3] eax:1, ecx:3, edi:4, ebp:6
7 xor ebp, ebp eax:1, ecx:3, edi:4, ebp:7
现在我们来查看最终的状态和其中涉及的指令(1,3,4,7),然后我们再来查看他们需要的存储器和指令是否有意义。
指令1不需要任何存储,解决。
指令3需要ecx和edx,在edx中我们没有任何信息,但ecx中发现之前指令写入了2的状态,所以将2视作有效操作的名单中。
指令4不需要任何存储,解决。
指令7不需要任何存储,解决。
这个过程将2加入到有效操作的名单中,所以我们再检查下2:
指令2读取了eax,所以我们查找之前写入这里状态的指令,并找到了指令1。而指令1已经在有效操作名单中,所以算法是可行的。
最终有效操作的指令名单为(1,2,3,4,7),指令5和6因为没人在意他们的作用而抛弃,最终获得了下述的去除混淆后的代码:
1 mov eax, 1337
2 mov ecx, eax
3 add ecx, edx
4 mov edi, 42
7 xor ebp, ebp
当然没有人会在意edi的值是否是42,或者ebp的值是否是0,但此刻指令4和7是导致这里值发生变化的原因,所以这两条指令不能被安全的随意丢弃。
难题
当然这个过程中遇到了许多困难(好在最后解决了),下面是我在过程中遇到并解决的几个问题。
问题1 – 寄存器混淆
例子:
mov eax, 0xFFFFFFFF
mov ax, 1337
两条指令都是有意义的。第二条指令对寄存器重新写入,但头16位依然是第一条的没有变化。幸运的是我没有在我的VMProtected保护的文件中找到类似的代码,所以我偷懒地将每个寄存器在写入前转换到最大的地址中。VMProtected的确使用了一系列如di,ax,bh等操作但没有尝试混淆过他们,所以我的方法也是有效的。
问题2 –我们如何判断状态?
这里会有些难理解,因为不能简单的将上述例子中的每个相关寄存器都看作无效状态。如果我们假设eip是有效的,那么每一条修改eip状态的指令都应当是有效的,所以我排除了eip的状态。
我目前还排除了内存的写入操作,但之后会对此进行修复改进。
但esp呢?很明显有对这个栈的相关写入操作,但它改变esp本身的值了吗?
问题3 – 栈
这里也有些困难,VMProtect喜欢在栈中写入垃圾指令并在使用它们前就丢弃其中的值。
例子:
pushad
lea esp, dword ptr [esp+xxx]
当我试图认为esp有用并进行追踪的时候,发现包含进了大量垃圾指令,因此我进行了下述的定义:
esp从状态追踪开始就彻底排除。
就像追踪寄存器写入的地址一样追踪栈的写入操作。
在向指令集中添加指令前,如果涉及到esp状态,就丢弃涉及的所有栈写入状态。
这意味着上述的例子可以写成如下:
Instruction State
pushad stack0:1, stack1:1, stack2:1 ... stack7:1
lea esp, dword ptr [esp+40] empty
我们就像追踪寄存器一样追踪发现Pushad向栈中写入了8位内存地址,之后lea修改了esp地址并前移了40位。所以esp又覆盖了最初写入的栈地址,并移除了相关的状态。因此pushad被认为是无效的垃圾指令。
到目前为止一切顺利,没有任何由于忽略垃圾指令引起的栈异常问题。
工具
首先我会讲解下我的代码,最初我设想是一款能够在IDA中运行,返回可读代码的静态工具。所以我使用Triton,一款动态二进制分析架构来追踪静态代码,它还提供了例如寄存器读写、内存位置等一系列我需要的信息,这是一款十分有用的工具。
我试着对工具进行了一些改进。
首先,使用Triton对VMProtect文件混淆代码中的入口进行追踪:
100a63e2 [001] push 0x8beef346
100a63e7 [002] push dword ptr [esp]
100a63ea [003] mov dword ptr [esp + 4], 0x6fdd8840
100a63f2 [004] mov byte ptr [esp], dh
100a63f5 [005] pushal
100a63f6 [006] pushal
100a63f7 [007] mov dword ptr [esp + 0x40], 0x2383346b
100a63ff [008] mov byte ptr [esp + 8], 0x5d
100a6404 [009] lea esp, dword ptr [esp + 0x40]
100a6408 [010] jmp 0x100c8e85
100c8e85 [011] pushal
100c8e86 [012] pushfd
100c8e87 [013] pushfd
100c8e88 [014] mov dword ptr [esp + 0x24], eax
100c8e8c [015] push eax
100c8e8d [016] pushfd
100c8e8e [017] mov dword ptr [esp + 0x28], edx
100c8e92 [018] pushfd
100c8e93 [019] mov byte ptr [esp + 4], 0x6f
100c8e98 [020] call 0x100c87bd
100c87bd [021] jmp 0x100c895e
100c895e [022] mov dword ptr [esp + 0x2c], ebp
100c8962 [023] pushfd
100c8963 [024] call 0x100c8a92
100c8a92 [025] mov dword ptr [esp + 0x30], ebx
100c8a96 [026] mov byte ptr [esp], dh
100c8a99 [027] mov byte ptr [esp], ah
100c8a9c [028] push edi
100c8a9d [029] jmp 0x100c8d55
100c8d55 [030] mov dword ptr [esp + 0x30], esi
100c8d59 [031] mov byte ptr [esp + 4], ch
100c8d5d [032] mov dword ptr [esp + 0x2c], edi
100c8d61 [033] mov word ptr [esp], 0x79d1
100c8d67 [034] mov byte ptr [esp], cl
100c8d6a [035] mov dword ptr [esp + 0x28], ebx
100c8d6e [036] push ebx
100c8d6f [037] push ecx
100c8d70 [038] push edi
100c8d71 [039] lea esp, dword ptr [esp + 0x34]
100c8d75 [040] jmp 0x100c8c80
100c8c80 [041] bswap si
100c8c83 [042] not si
100c8c86 [043] pushal
100c8c87 [044] pushfd
100c8c89 [045] pop dword ptr [esp + 0x1c]
100c8c8d [046] shld si, di, cl
100c8c91 [047] jmp 0x100c7dbd
100c7dbd [048] mov dword ptr [esp + 0x18], ecx
100c7dc1 [049] bsf di, si
100c7dc5 [050] push dword ptr [0x100c6d15]
100c7dcb [051] pop dword ptr [esp + 0x14]
100c7dcf [052] movzx cx, bl
100c7dd3 [053] xadd edi, ebp
100c7dd6 [054] mov dword ptr [esp + 0x10], 0
100c7dde [055] cmc
100c7ddf [056] jmp 0x100c7c7b
100c7c7b [057] mov esi, dword ptr [esp + 0x40]
100c7c7f [058] bts di, ax
100c7c83 [059] btr di, 4
100c7c88 [060] add esi, 0x1644be2b
100c7c8e [061] neg di
100c7c91 [062] pushal
100c7c92 [063] clc
100c7c93 [064] xor esi, 0x69d1f651
100c7c99 [065] mov word ptr [esp], 0xfaa6
100c7c9f [066] or edi, eax
100c7ca1 [067] xchg di, bp
100c7ca4 [068] not esi
100c7ca6 [069] and bp, 0x4b2b
100c7cab [070] rcr bl, cl
100c7cad [071] dec bp
100c7cb0 [072] stc
100c7cb1 [073] lea ebp, dword ptr [esp + 0x30]
100c7cb5 [074] bts bx, bp
100c7cb9 [075] rcl bx, 4
100c7cbd [076] add di, di
100c7cc0 [077] neg bx
100c7cc3 [078] sub esp, 0x90
100c7cc9 [079] push esp
100c7cca [080] lea edi, dword ptr [esp + 4]
100c7cce [081] lea esp, dword ptr [esp + 4]
100c7cd2 [082] adc bl, dl
100c7cd4 [083] mov ebx, esi
100c7cd6 [084] dec al
100c7cd8 [085] xor cl, cl
100c7cda [086] cmc
100c7cdb [087] cmp esi, 0xae51dbba
100c7ce1 [088] add esi, dword ptr [ebp]
100c7ce4 [089] clc
100c7ce5 [090] not cl
100c7ce7 [091] mov al, byte ptr [esi]
100c7ce9 [092] shr cl, 3
100c7cec [093] rol cl, 6
100c7cef [094] add al, bl
100c7cf1 [095] movsx cx, dl
100c7cf5 [096] rcl ch, 2
100c7cf8 [097] movzx cx, al
100c7cfc [098] pushal
100c7cfd [099] rol al, 7
100c7d00 [100] or ch, dl
100c7d02 [101] shrd cx, di, 6
100c7d07 [102] shl cl, cl
100c7d09 [103] pushal
100c7d0a [104] neg al
100c7d0c [105] mov cl, 0x47
100c7d0e [106] call 0x100c6d19
100c6d19 [107] clc
100c6d1a [108] sub esi, -1
100c6d1d [109] test dh, 0x18
100c6d20 [110] not al
100c6d22 [111] dec ecx
100c6d23 [112] add bl, al
100c6d25 [113] rcl cx, 3
100c6d29 [114] rcr ch, 7
100c6d2c [115] rcr ch, 2
100c6d2f [116] movzx eax, al
100c6d32 [117] mov byte ptr [esp], bl
100c6d35 [118] clc
100c6d36 [119] not cl
100c6d38 [120] mov ecx, dword ptr [eax*4 + 0x100c7e65]
100c6d3f [121] lea esp, dword ptr [esp + 0x44]
100c6d43 [122] jb 0x100c8390
100c6d49 [123] push 0x968f74f5
100c6d4e [124] push dword ptr [esp]
100c6d51 [125] ror ecx, 0xc
100c6d54 [126] test bx, ax
100c6d57 [127] add ecx, 0
100c6d5d [128] mov byte ptr [esp + 4], bl
100c6d61 [129] mov dword ptr [esp + 4], ecx
100c6d65 [130] pushal
100c6d66 [131] push esp
100c6d67 [132] push dword ptr [esp + 0x28]
100c6d6b [133] ret 0x2c
从开始地方运行我的工具后获得的去除混淆的代码如下:
[003] mov dword ptr [esp + 4], 0x6fdd8840
[007] mov dword ptr [esp + 0x40], 0x2383346b
[014] mov dword ptr [esp + 0x24], eax
[017] mov dword ptr [esp + 0x28], edx
[022] mov dword ptr [esp + 0x2c], ebp
[025] mov dword ptr [esp + 0x30], ebx
[030] mov dword ptr [esp + 0x30], esi
[032] mov dword ptr [esp + 0x2c], edi
[035] mov dword ptr [esp + 0x28], ebx
[041] bswap si
[042] not si
[043] pushal
[044] pushfd
[045] pop dword ptr [esp + 0x1c]
[046] shld si, di, cl
[048] mov dword ptr [esp + 0x18], ecx
[049] bsf di, si
[050] push dword ptr [0x100c6d15]
[051] pop dword ptr [esp + 0x14]
[052] movzx cx, bl
[053] xadd edi, ebp
[054] mov dword ptr [esp + 0x10], 0
[057] mov esi, dword ptr [esp + 0x40]
[058] bts di, ax
[059] btr di, 4
[060] add esi, 0x1644be2b
[061] neg di
[062] pushal
[064] xor esi, 0x69d1f651
[065] mov word ptr [esp], 0xfaa6
[068] not esi
[073] lea ebp, dword ptr [esp + 0x30]
[080] lea edi, dword ptr [esp + 4]
[083] mov ebx, esi
[088] add esi, dword ptr [ebp]
[091] mov al, byte ptr [esi]
[094] add al, bl
[099] rol al, 7
[104] neg al
[108] sub esi, -1
[110] not al
[112] add bl, al
[116] movzx eax, al
[120] mov ecx, dword ptr [eax*4 + 0x100c7e65]
[125] ror ecx, 0xc
[127] add ecx, 0
[129] mov dword ptr [esp + 4], ecx
[132] push dword ptr [esp + 0x28]
[133] ret 0x2c
混淆过的代码包含133条指令,而去除混淆后的代码(看起来可读性高了很多)只有49条。
如果你真的试图阅读这些代码,你会发现其中栈的操作会遇到新的问题。就如我之前所说的一样,我没有追踪esp所以指令直接对esp进行的下述操作:
sub esp, 0x40
不在上述代码中。因此我们在无法判断栈位置的时候,也就无法阅读代码。我也试图解决这一问题,并选择了下述的方法:
感谢Triton工具帮助我们获取每一个接口的实际栈位置,所以我给每个特殊栈单独的名字,从而实现下述的效果:
[003] mov dword ptr [svar0], 0x6fdd8840
[007] mov dword ptr [svar1], 0x2383346b
[014] mov dword ptr [svar2], eax
[017] mov dword ptr [svar3], edx
[022] mov dword ptr [svar4], ebp
[025] mov dword ptr [svar5], ebx
[030] mov dword ptr [svar6], esi
[032] mov dword ptr [svar7], edi
[035] mov dword ptr [svar8], ebx
[041] bswap si
[042] not si
[043] pushal
[044] pushfd
[045] pop dword ptr [svar9]
[046] shld si, di, cl
[048] mov dword ptr [svar10], ecx
[049] bsf di, si
[050] push dword ptr [0x100c6d15]
[051] pop dword ptr [svar11]
[052] movzx cx, bl
[053] xadd edi, ebp
[054] mov dword ptr [svar12], 0
[057] mov esi, dword ptr [svar0]
[058] bts di, ax
[059] btr di, 4
[060] add esi, 0x1644be2b
[061] neg di
[062] pushal
[064] xor esi, 0x69d1f651
[065] mov word ptr [svar13], 0xfaa6
[068] not esi
[073] lea ebp, dword ptr [svar12]
[080] lea edi, dword ptr [svar14]
[083] mov ebx, esi
[088] add esi, dword ptr [ebp]
[091] mov al, byte ptr [esi]
[094] add al, bl
[099] rol al, 7
[104] neg al
[108] sub esi, -1
[110] not al
[112] add bl, al
[116] movzx eax, al
[120] mov ecx, dword ptr [eax*4 + 0x100c7e65]
[125] ror ecx, 0xc
[127] add ecx, 0
[129] mov dword ptr [svar15], ecx
[132] push dword ptr [svar15]
[133] ret 0x2c
我们可以轻松地阅读上述代码和栈的情况。
由于Triton不能有效地支持x86语义,因此我还需要完善一些新的工作,我已经在github上提交了新的issue。例如rol指令由于执行过程(在特定情况下无法读取右移的值)中不能有效地读取进位标志,Triton会认为rol读取了进位标志导致后续指令对其依赖,从而判断它不存在。
证明
如上去除混淆后的代码就可以正常的阅读分析了,但我认为这个结果还有其他价值,毕竟我可能还丢弃了一些重要的指令。
所以我用上述代码计算了最终跳转到的地址,下面是注释过的汇编代码:
[003] mov dword ptr [svar0], 0x6fdd8840
[007] mov dword ptr [svar1], 0x2383346b
[014] mov dword ptr [svar2], eax
[017] mov dword ptr [svar3], edx
[022] mov dword ptr [svar4], ebp
[025] mov dword ptr [svar5], ebx
[030] mov dword ptr [svar6], esi
[032] mov dword ptr [svar7], edi
[035] mov dword ptr [svar8], ebx
[041] bswap si
[042] not si
[043] pushal
[044] pushfd
[045] pop dword ptr [svar9]
[046] shld si, di, cl
[048] mov dword ptr [svar10], ecx
[049] bsf di, si
[050] push dword ptr [0x100c6d15]
[051] pop dword ptr [svar11]
[052] movzx cx, bl
[053] xadd edi, ebp
[054] mov dword ptr [svar12], 0 svar12 = 0
[057] mov esi, dword ptr [svar0] esi = 0x6fdd8840
[058] bts di, ax
[059] btr di, 4
[060] add esi, 0x1644be2b esi = 0x8622466B
[061] neg di
[062] pushal
[064] xor esi, 0x69d1f651 esi = 0xEFF3B03A
[065] mov word ptr [svar13], 0xfaa6
[068] not esi esi = 100C4FC5
[073] lea ebp, dword ptr [svar12] ebp = pointer to svar12 (which is 0)
[080] lea edi, dword ptr [svar14]
[083] mov ebx, esi ebx = 100C4FC5
[088] add esi, dword ptr [ebp] esi = 100C4FC5
[091] mov al, byte ptr [esi] al = 0x41 (lookup done in IDA)
[094] add al, bl al = 0x06
[099] rol al, 7 al = 3
[104] neg al al = 0xFD
[108] sub esi, -1 esi = 100C4FC6
[110] not al al = 2
[112] add bl, al
[116] movzx eax, al eax = 2
[120] mov ecx, dword ptr [eax*4 + 0x100c7e65] ecx = 0xc8bd0100 (lookup done in IDA)
[125] ror ecx, 0xc ecx = 100c8bd0
[127] add ecx, 0
[129] mov dword ptr [svar15], ecx
[132] push dword ptr [svar15]
[133] ret 0x2c jmp 100c8bd0
为了证明我的结果,下面是OllyDbg中的显示:
所以去除混淆后的代码的确可以正常分析了,尽管它不能证明没有丢弃重要的指令,但至少结果是可用的了。
未来的工作
我很快就注意到栈定义系统目前正常工作,但在涉及到特大的代码量时,命名就无法正常工作了。所以我还需要考虑如何处理全局栈问题来让工具更加实用。
如上所述,寄存器混淆是未来要做的工作方向之一,就如优化代码显示一样,这不是一个简单的工作。内存写入操作目前也没有涉及,是我之后要添加的功能。
这里的代码只能在x86下运行,但导出到x64中应该不难。
很高兴工具能获得目前的结果,也感谢您阅读到这里。
链接
你可以在我的github上下载脚本
如果你对原始的输出文件感兴趣,可以在output.txt处下载
[培训]《安卓高级研修班(网课)》月薪三万计划
最后于 2019-2-1 10:02
被kanxue编辑
,原因: