使用WinAppDbg解决Flareon第3题

本文已发表在看雪论坛, 详情可见: https://bbs.pediy.com/thread-223525.htm

文章作者: Parsia’s Den

博客地址: https://parsiya.net/

原文链接: WinAppDbg – Part 4 – Bruteforcing FlareOn 2017 – Challenge 3

翻译前言: 这是解决Flareon4第3题的第4种方法, 也是这个系列翻译的完结篇. 作者用的WinAppDbg跟ODScript有类似的感觉, 虽然不及之前2篇让人耳目一新, 但这是作者对于WinAppDbg写的简易教程的第4篇, 如果感兴趣可以点击原文链接从其它3篇WinAppDbg的教程开始阅读.

文中分析的程序你可以点击此处下载: greek_to_me.zip, 解压密码: www.pediy.com

如果朋友想看我之前翻译的用其他3种全新的方法解决该题的文章, 可以点击以下链接:

  1. Flareon challenge 4 第3题
  2. 使用libPeConv来解决Flareon4题目3
  3. 使用Angr解决Flareon4题目3

侦查

首先我们要运行strings程序分析文件. 在Windows我喜欢从以下两种方式获取strings

  • 从Cygwin的binutils包获取strings
  • 从微软Sysinternals套件获取strings
    运行strings我们获得下图:
  • -nobanner: 不要显示启动时的标语和版权信息
  • -o: 打印字符串偏移(如果想要找寻字符串地址这会很有帮助)
PS > .\SysinternalsSuite\strings.exe -o -nobanner .\3-GreektoMe\greek_to_me.exe
0077:!This program cannot be run in DOS mode.
0176:Rich
0432:.text
0472:.rdata
...
1584:Nope, that's not it.
1608:Congratulations! But wait, where's my flag?
1652:127.0.0.1
1752:WS2_32.dll

ws2_32.dll是Windows套接字库, 故程序中有着网络活动.

说个有趣的题外话, 当我在搜索这个DLL时我发现以下这个链接:

回归正题, 127.0.0.1表明程序有网络活动, 表明它尝试连接或监听本地端口.

为了进一步探明, 我们运行procmonwireshark

什么都没有显示. 故程序是在进行本地监听.

运行程序并以管理员身份运行命令行, 输入netstat -anb.

 TCP    127.0.0.1:2222         0.0.0.0:0              LISTENING       5816
[greek_to_me.exe]

程序正在监听本地端口2222

简短分析

程序监听端口2222, 当接收到数据, 它使用了我们输入的第1个字节(也就只用了第1个字节). 如下所示:

.text:00401029 loc_401029:      ; CODE XREF: sub_401008+1A
.text:00401029          mov     ecx, offset loc_40107C
.text:0040102E          add     ecx, 79h
.text:00401031          mov     eax, offset loc_40107C
.text:00401036          mov     dl, [ebp+buf]   ; first byte of input moved to dl

现在dl指向着我们发送给socket的第1个字节

.text:00401039 loc_401039:      ; CODE XREF: sub_401008+3D
.text:00401039          mov     bl, [eax]   ; bl = grab a byte from blob
.text:0040103B          xor     bl, dl      ; bl = blob_byte xor our_first_byte
.text:0040103D          add     bl, 22h     ; bl += 0x22
.text:00401040          mov     [eax], bl   ; *eax = bl
.text:00401042          inc     eax         ; eax++ (next char)
.text:00401043          cmp     eax, ecx    ; ecx is the address of the second section
.text:00401045          jl      short loc_401039 ; check if we have reached the next section

它抓取了一些数据(准确说是0x79或121字节), 使用我们的第1个字节跟其异或随后加上0x22. 取出的数据则是位于loc40107C偏移处的十六进制块.

33 E1 C4 99 11 06 81 16 F0 32 9F C4 91 17 06 81
14 F0 06 81 15 F1 C4 91 1A 06 81 1B E2 06 81 18
F2 06 81 19 F1 06 81 1E F0 C4 99 1F C4 91 1C 06
81 1D E6 06 81 62 EF 06 81 63 F2 06 81 60 E3 C4
99 61 06 81 66 BC 06 81 67 E6 06 81 64 E8 06 81
65 9D 06 81 6A F2 C4 99 6B 06 81 68 A9 06 81 69
EF 06 81 6E EE 06 81 6F AE 06 81 6C E3 06 81 6D
EF 06 81 72 E9 06 81 73 7C

xor_add.png

随后修改的数据块(在异或和加法操作后)传递给sub_4011E6并继续处理:

.text:00401047          mov     eax, offset loc_40107C  ; eax = *modified_blob
.text:0040104C          mov     [ebp+var_C], eax        ; varC = eax
.text:0040104F          push    79h                     ; length of modified_blob
.text:00401051          push    [ebp+var_C]
.text:00401054          call    sub_4011E6              ; sub_4011E6(*modified_blob, 0x79)
.text:00401059          pop     ecx
.text:0040105A          pop     ecx
.text:0040105B          movzx   eax, ax
.text:0040105E          cmp     eax, 0FB5Eh ; compare return value with 0xFB5E

.text:00401063          jz      short loc_40107C
.text:00401065          push    0               ; flags
.text:00401067          push    14h             ; len
.text:00401069          push    offset buf      ; "Nope, that's not it."
.text:0040106E          push    [ebp+s]         ; s
.text:00401071          call    ds:send
.text:00401077          jmp     loc_401107

sub_4011E6的返回值跟0xFB5E进行比较, 如果不匹配, jz跳转将不会实现并继续执行, 程序也会传回来Nope, that's not it..

nope.png

现在就变得越发有趣了. 如果结果匹配, 它就会跳转到我们刚刚修改过的数据块并试图将其作为代码执行. 如果程序没有崩溃且运行到结尾, 那么它就会发送回来Congratulations!.

换句话说, 我们输入的第1个字节就是用来将那数据块转换成正确的汇编操作码.

现在我们应该用另外一种方式来解决该题. 我想所有人都会用打开套接字, 发送256个可能字节并查看结果的方法来解决它. 这确实可以解决这个问题.

使用WinAppDbg暴力穷举

我使用另外一种方法解决. 复现这个方法我们需要了解一点点WinAppDbg的知识.

WinAppDbg设置断点

WinAppDng允许我们在任意地址设置断点:

debug.break_at(pid, address, action_callback)

def action_callback(event):
    # do something

当断点触发, action_callback函数就会被调用

更多信息请看:

获取和设置内存

WinAppDbg运行我们存储/恢复内存和上下文

获取和设置上下文

上下文包含寄存器和各种标志值, 是逐线程(而非逐进程的)

  • 获取上下文: context = thread.get_context()
  • 设置上下文: thread.set_context(context)

注意: 在设置完上下文后, 我们需要手动修改指令指针指向一个开始执行的具体位置. 比如说如果我们获取了上下文, 改变Eip指向一个地址, 实际的指令指针并不会变化. 我们在设置完上下文后, 需要使用thread.set_pc(address)手动将指令指针改成你需要的地址.

在进行内存和上下文的操作时, 请确保事先暂停了程序/线程, 在操作完成后再恢复.

作战计划

现在我们有了建筑模块, 我们需要制定一个作战计划. 非常简单明了.

  1. 运行程序
  2. 0x4010360x40105B设置断点
  3. 打开socket并发送任何可能的字节
  4. 0x401036的断点
    • 如果是第1次触发断点:
      • 保存内存,上下文和0x40107C的数据块
    • context["Edx"] = key – 交换key值
    • key++
    • 绕过key的赋值指令并使用thread.set_pc(0x401039)手动跳转到0x401039
  5. 0x40105B的断点:
    • 如果函数返回值是0xFB5E, 则打印key值
    • 否则:
      • 复原内存, 上下文和0x40107C处的数据块(数据块已经被修改过了, 因此这里需要复原成原来的字节)
      • 使用thread.set_pc(0x401036)返回到0x401036

plan.png

改变buf中第1个字节会比edx中更简单些, 而且能够避免途中标签2对应的跳转.

开始暴力穷举

我们使用的脚本是19-GreekToMe.py, 你需要将greek_to_me.exe放在脚本的同一目录下, 该程序可以在附件里下载.

脚本运行非常快,因为我们的穷举空间仅仅只有1字节(0x00到0xFF)

$ python 19-GreekToMe.py
[21:23:48.0743] Starting simple_debugger
[21:23:48.0753] Started simple_debugger. Sleeping for 2 seconds.
[21:23:50.0756] Starting send_me.
[21:23:50.0875] Socket connected
[21:23:50.0875] Sent 0
[21:23:53.0490]
-------------------------------------------------------------------------------
Key: 0xa2
Eax: 0000FB5E
[21:23:54.0901] Reached 0x100

Flag

重新在调试器中运行程序, 在”Congratulations!”处设下断点然后重新发送0xA2, 数据块正确解密, 我们也获得了flag

flag: et_tu_brute_force@flare-on.com

Posted in reverse engnieering | Leave a comment

使用Angr解决Flareon4题目3

本文已发表在看雪论坛, 详情可见: https://bbs.pediy.com/thread-223512.htm

文章作者: XOR Hex

博客地址: https://blog.xorhex.com/

原文链接: Flare-On 2017: Challenge 3

翻译前言: 这是解决Flareon4第3题的第3种方法. 文章中使用angr编写python脚本来获得flag, 对比之前我翻译的使用Unicorn框架的文章里的代码, 对于刚接触Angr或Unicorn的朋友会有不少帮助.

文中分析的程序你可以点击此处下载: greek_to_me.zip, 解压密码: www.pediy.com

准备

我们用IDA打开文件并调到入口点, 入口点简单地调用了函数sub_401008

entry_point.png

我们打开这个函数看看, 向下滚动我们可以看到看起来像成功提示的文本字符串, 接下来的标准流程就是找寻成功的分支.

success_message.png

从字符串往回分析, 我们发现一个在0x40105E处的比较, eax跟值0xFB5E进行比较.

eax_comparison_check.png

如果eax匹配成功, 那么随后程序就会继续执行成功分支. 所以我们该如何满足这个匹配呢?

来看看sub_4011E6函数内部. 我们看到一堆的mov,add,shlshr指令, 这可能是某种形式的混淆. 让我们看看传入进函数的参数:

.text:00401047 mov     eax, offset loc_40107C
.text:0040104C mov     [ebp+var_C], eax
.text:0040104F push    79h
.text:00401051 push    [ebp+var_C]
.text:00401054 call    sub_4011E6

第1个参数存储在eax寄存器中而第2个参数这是值0x79. 注意, eax包含的是0x40107C的偏移量而第2个参数0x79看起来很可能是一个表示长度的参数. 我们细细检查函数sub_4011E6就能证实这点. 也就是说程序即将修改如下所示的汇编代码片段.

obfuscated_assembly_code.png

并没有变量传入解混淆代码, 所以猜测应该是需要某种形式的用户输入, 所以我们继续看.
来到下一个代码块, 我们看到相同的汇编代码区段0x40107C在传入函数sub_4011E6之前xoradd指令进行修改.

first_deobfuscation_routine.png

xor的值存储在dl, 而dl总是从[ebp+buf]赋值而来, 这看上去就可能是我们的用户输入了. 继续向上跟踪我们看到[eax+buf]则是作为参数传递给函数sub_401121

sub_401121_call.png

快速浏览函数sub_401121, 我们看到0x4011BC处设置了[ebp+buf]

call_to_recv.png

函数的剩余部分则仅仅只是服务器接受输入后的一些操作

总结一下收获:

  • 找到了位于0x4010FE的成功提示字符串
  • 比较检查在0x40105E
    • 必须匹配0xFB5E才能进入成功验证分支
  • 已混淆代码起始于0x40107C
  • 已混淆代码的长度为0x79
  • 第2阶段的解混淆函数在0x401054处被调用
  • 第1阶段的解混淆操作从0x401029开始到0x401045
  • 用户输入发生在0x401015处的函数调用
    • 用户输入来自网络
    • 输入缓冲区的长度是0x4

解答

在我们开始写脚本解答之前, 我们需要提取那些混淆过的字节出来.

这里我使用IDAPython脚本来提取字节

with open('greek_to_me_buffer.asm', 'wb') as f:
  f.write(idaapi.get_many_bytes(0x40107C, 0x79))

现在我们可以进入下一步, 写脚本!

用户输入

我们知道从网络接收的长度是4字节, 但是聪明的读者可能已经注意到代码中使用dl赋值给buf而非edx, 也就导致实际值的范围是从0x00xff(dl只有1字节大小). 我们脚本的开始部分类似如下:

for buf in xrange(0x100):
    print("Using {0}".format(buf))

暴力穷举

接下来我们需要修改提取出的比特使得通过比较检查. 我们需要通过解混淆的两部分.

解混淆第1步

对于第1次的解混淆(0x401039), 我们可以用python简单写一个”解码器”.

    # Variable to store the bits written to disk using IDA
    asm = None
    # Store the output from the first de-obfuscation routine
    b2 = []
    # Read in bytes written to file from IDA
    with open('greek_to_me_buffer.asm', 'rb') as f:
        asm = f.read()

    # Re-implement loc_401039
    dl = buf
    for byte in b:
        bl = ord(byte)
        bl = bl ^ dl
        bl = bl & 0xff
        bl = bl + 0x22
        bl = bl & 0xff
        b2.append(bl)

要记住第1步解混淆操作应该放在for循环块中.

解混淆第2步

Angr的帮助下, 我们可以继续按我们的方式进行第2阶段的解混淆, 虽然有点像作弊, 但谁又想用python或c重写一遍解混淆的代码呢?

for循环之前一行声明一个angr工程实例, 这样它就不会在每次for循环执行时重新创建一遍.

p = angr.Project('greek_to_me.exe', load_options={'auto_load_libs': False})

设置Angr模拟执行sub_4011E6, 不过我们这次需要放到for循环里去.

    # Set up angr to "run" sub_4011E6 
    s = p.factory.blank_state(addr=0x4011E6)
    s.mem[s.regs.esp+4:].dword = 1    # Angr memory location to hold the xor'ed and add'ed bytes
    s.mem[s.regs.esp+8:].dword = 0x79 # Length of ASM

    # Copy bytes output from loc_401039 into address 0x1 so Angr can run it
    asm = ''.join(map(lambda x: chr(x), b2))
    s.memory.store(1, s.se.BVV(int(asm.encode('hex'), 16), 0x79 * 8 ))

    # Create a simulation manager...
    simgr = p.factory.simulation_manager(s)

    # Tell Angr where to go, though there is only one way through this function, 
    # we just need to stop after ax is set
    simgr.explore(find=0x401268)

虽然我意识到在这里使用Angr可能有点过犹不及, 但它是我手上最新的工具, 所以我所有的问题都以Angr的方式来解决.

输入合法性检查

接下来我们需要检查ax的输出是否匹配0xFB5E

    # Once ax is set, check to see if the value in ax matches the comparison value
    for found in simgr.found:
        print(hex(found.state.solver.eval(found.state.regs.ax)))
        # Comparison check
        if hex(found.state.solver.eval(found.state.regs.ax)) == '0xfb5eL':
            # Will cover what to do here in the next section
            pass

解混淆代码

现在我们已经满足了校验值匹配, 我们将解混淆的代码输出到屏幕上

�e�]��E�t�_�U��E�t�E�u�U��E�b�E�r�E�u�E�t�]߈U��E�f�E�o�E�r�E�c�]��E�@�E�f�E�l�E�a�E�r�]��E�-�E�o�E�n�E�.�E�c�E�o�E�m�E�

我们猜测这应该是汇编代码. 我们使用Capstone反编译代码.

from capstone import *
md = Cs(CS_ARCH_X86, CS_MODE_32)
for i in md.disasm(code, 0x1000):
    print("0x%x\t%s\t%s" %(i.address, i.mnemonic, i.op_str))

再次运行脚本, 我们可以确定这是汇编代码并且填充入缓冲区的内容里出现了ASCII字符.

0x1000  mov bl, 0x65    None
0x1002  mov byte ptr [ebp - 0x2b], bl
0x1005  mov byte ptr [ebp - 0x2a], 0x74
0x1009  mov dl, 0x5f    None
0x100b  mov byte ptr [ebp - 0x29], dl
0x100e  mov byte ptr [ebp - 0x28], 0x74
0x1012  mov byte ptr [ebp - 0x27], 0x75
0x1016  mov byte ptr [ebp - 0x26], dl
0x1019  mov byte ptr [ebp - 0x25], 0x62
0x101d  mov byte ptr [ebp - 0x24], 0x72
0x1021  mov byte ptr [ebp - 0x23], 0x75
0x1025  mov byte ptr [ebp - 0x22], 0x74
0x1029  mov byte ptr [ebp - 0x21], bl
0x102c  mov byte ptr [ebp - 0x20], dl
0x102f  mov byte ptr [ebp - 0x1f], 0x66
0x1033  mov byte ptr [ebp - 0x1e], 0x6f
0x1037  mov byte ptr [ebp - 0x1d], 0x72
0x103b  mov byte ptr [ebp - 0x1c], 0x63
0x103f  mov byte ptr [ebp - 0x1b], bl
0x1042  mov byte ptr [ebp - 0x1a], 0x40
0x1046  mov byte ptr [ebp - 0x19], 0x66
0x104a  mov byte ptr [ebp - 0x18], 0x6c
0x104e  mov byte ptr [ebp - 0x17], 0x61
0x1052  mov byte ptr [ebp - 0x16], 0x72
0x1056  mov byte ptr [ebp - 0x15], bl
0x1059  mov byte ptr [ebp - 0x14], 0x2d
0x105d  mov byte ptr [ebp - 0x13], 0x6f
0x1061  mov byte ptr [ebp - 0x12], 0x6e
0x1065  mov byte ptr [ebp - 0x11], 0x2e
0x1069  mov byte ptr [ebp - 0x10], 0x63
0x106d  mov byte ptr [ebp - 0xf], 0x6f
0x1071  mov byte ptr [ebp - 0xe], 0x6d
0x1075  mov byte ptr [ebp - 0xd], 0 

嵌入的ASCII字符

既然我们能够手动将上面汇编中可显字符的十六进制转换成字符, 我们为什么不用脚本来完成这一工作呢. 修改for循环:

bl = None
dl = None
flag = []
# Using capstone, interpret the ASM
from capstone import *
md = Cs(CS_ARCH_X86, CS_MODE_32)
for i in md.disasm(code, 0x1000):
    flag_char = None
    # The if statements do the work of interpreting the ASCII codes to their value counterpart
    if i.op_str.split(',')[0].startswith("byte ptr"):
        flag_char = chr(long(i.op_str.split(',')[1], 16))
    if i.op_str.split(',')[0].startswith('bl'):
        bl = chr(long(i.op_str.split(',')[1], 16))
    if i.op_str.split(',')[0].startswith('dl'):
        dl = chr(long(i.op_str.split(',')[1], 16))
    if i.op_str.split(',')[1].strip() == 'dl':
        flag_char = dl
    if i.op_str.split(',')[1].strip() == 'bl':
        flag_char = bl

    if (flag_char):
        flag.append(flag_char.strip())

    print("0x%x\t%s\t%s\t%s" %(i.address, i.mnemonic, i.op_str, flag_char))

print(''.join(flag))

最后运行脚本得到flag

et_tu_brute_force@flare-on.com

结论

总体上我们解决题目用的静态方法十分有趣, 同时也让我有机会首次在CTF竞赛中使用AngrCapstone. 一开始我使用的是Angr 6来解决该问题, 但后来因为在CTF的中途Angr更新新版本, 所以写脚本的时候用的是Angr 7. 如果我随后使用了Unicorn引擎和Angr再次解决的话我会回来更新这篇文章.

完整的脚本代码你可以在这里找到: greek_to_me_angr7.py

Posted in reverse engnieering | Leave a comment

使用libPeConv来解决Flareon4第3题

本文已发表在看雪论坛, 详情可见: https://bbs.pediy.com/thread-223494.htm

文章作者: hasherezade(@hasherezade)

原文链接: Import all the things! Solving FlareOn4 Challenge 3 with libPeConv

翻译前言: 虽然依旧是Flareon4第3题的分析,但是一道题的解决方法多种多样,这次给大家分享如何使用libPeConv来解决问题,又可以get到新姿势啦

libPeConv: 是作者hasherezade开发的用于加载和转换PE文件的库,github仓库地址是:libpeconv

文中分析的程序你可以点击此处下载: greek_to_me.zip, 解压密码: www.pediy.com

总览

题目greek_to_me.exe是一个32位PE文件, 程序已经剔除了重定位信息. 我们以下就简称该程序为crackme
fig1

我们运行crackme, 只有一个空白的控制台程序, 并且没有从标准输入中读取任何数据, 所以我们可以推断程序是使用了一些其他方式来读取用户的password

我们使用IDA静态分析, crackme结构非常简洁并没有混淆过. 我们可以在代码开头看见程序创建了一个socket并等待着输入
socket监听着本地2222端口

fig2

在建立连接后, crackme从用户输入中取前4字节读入到缓冲区中:

fig3

读入4字节后, crackme开始处理输入并用来解码已加密的缓冲区数据

fig4

如果校验值是合法的, 也就是说加密数据被正确解密了, 那么crackme就会进一步执行下去.

我们可以看到, 输入中的数据只有1字节用于解码缓冲区数据, 所以我们可以轻易地穷举获得结果. 解码部分的代码也相当简单:

const size_t encrypted_len = 0x79;
for (int i = 0; i < encrypted_len; i++) {
    BYTE val = encrypted_code[i];
    encrypted_code[i] = (unknown_byte ^ val) + 0x22;
}

程序唯一的难点在于校验值 – 这个函数并没有那么好复现. 然而如果我们想要暴力穷举, 我们却又需要在穷举后计算校验值.

在我之前的解答中, 我复现了校验函数并表现良好, 但这并没有那么好玩. 我看过了一些其他的解决方式如使用Unicorn引擎模拟执行校验函数
, 或使用angr框架, 或通过socket使用暴力穷举程序来获得原始程序等等. 但是我们可以解决得更快速吗?我们来接着看…

使用LibPeConv

使用PeConv我们可以将原始格式的任何PE文件转换成虚拟内存格式并返回. 它也提供有一个可定制的PE加载器 – 用于加载任意PE文件到当前进程(就算它不是dll文件也没有重定位表, 这我会在之后的部分进行解释). 载入的PE文件随后可以在当前进程内运行. 我们也可以选择文件中的任意函数来使用 – 而我们只需要知道函数的RVA和API.

在这次, 我将会使用libpeconv来加载crackme并导入校验值的计算函数. 不用复制加密缓冲区数据到我们的代码中, 我们可以直接从载入的PE文件中读取它.

收集需要的信息

让我们再一次在IDA中查看crackme. 我们需要找到恰当的偏移量并明白我们需要导入的API函数.

首先我们计算校验值的函数起始于RVA 0x11E6处:

fig5

函数读取2个参数: 指向缓冲区的指针和缓冲区大小
函数返回一个WORD类型数据.

fig6

总结一下, 我们可以定义一个如下的函数原型:

WORD calc_checksum(BYTE *decoded_buffer, size_t buf_size)

还有一点需要注意, 就是这个函数是可独用的并且没有调用任何的导入库函数 – 这让我们导入这个校验值函数更加轻松(我们不必加载任何导入库模块或进行重定位).

另一个我们需要的信息就是加密的缓冲区. 缓冲区起始于RVA 0x107C并且长度为0x79(121)字节

fig7

信息搜集完毕!我们开始写代码.

使用libPeConv解决crackme

当前版本的libpeconv允许两种方式来载入PE文件. 使用到的函数有load_pe_moduleload_pe_executable. 第2个函数load_pe_executable是一个完整的加载器, 它加载指定PE文件到当前进程的可读可写可执行(RWX)内存中, 并自动应用重定位信息和载入其他依赖. 第1个函数load_pe_module则不能载入依赖并且我们需要提供更多的控制: 我们可能会加载PE文件到一个不可执行的内存中而是否进行重定位也是可选的. 更多详细信息(或者该API的重要更新)请看: https://github.com/hasherezade/libpeconv/blob/master/libpeconv/include/peconv/pe_loader.h

正如我们所见, 我们想要导入的函数是独用的, 因此如果我们载入crackme的PE文件时没有加载导入表和重定位信息也不会造成什么危害(我们将在文章的下一部分看如何载入一个完整的PE文件). 我将使用到load_pe_module函数

BYTE* loaded_pe = (BYTE*)load_pe_module(
    path,
    v_size, // OUT: size of the loaded module
    true,   // executable
    false   // without relocations
);

现在, 我们来导入函数, 首先我们来定义一个指针

WORD (*calc_checksum) (BYTE *buffer, size_t buf_size) = NULL;

计算在载入模块中该函数的绝对偏移

ULONGLONG offset = DWORD(0x11e6) + (ULONGLONG) loaded_pe;

然后填充指针

calc_checksum = ( WORD (*) (BYTE *, size_t ) ) offset;

现在我们就可以在我们的应用程序里该函数
但在那之前, 我们可以开始暴力穷举, 我们也同样也需要填充缓冲区指针.

g_Buffer = (uint8_t*) (0x107C + (ULONGLONG) loaded_pe);

以下链接是我准备的完整穷举程序: https://gist.github.com/hasherezade/44b440675ccc065f111dd6a90ed34399#file-brutforcer_1-cpp
并且结果表现良好. 我们得到的结果跟crackme需要的一样.

fig8

但目前为止, 我们找到的值也只是解答过程的一部分, 并不是我们需要找到的flag. 我们从先前静态分析时可以知道, 如果给出正确值, 那么代码块就能解密并执行. 如果我们能看到解密后代码块到底是怎样的, 那岂不是很酷?

而且这也非常容易实现. 我们的PE文件载入进了当前进程可读可写可执行内存中 – 因此我们可以轻易地将解密后的数据替换回加密块代码, 我们只需要一个简单的memcpy就能完成这个工作

memcpy(g_Buffer, g_Buffer2, g_BufferLen);

随后, libPeConv可以帮助我们将PE文件转换回原始格式以便用IDA打开. 我们可以用libPeConvpe_virtual_to_raw来完成.

size_t out_size = 0;
BYTE* unmapped_module = pe_virtual_to_raw(
    loaded_pe, //pointer to the module
    v_size, //virtual size
    module_base, //in this case we need here
                 //the original module base, because
                 //the loaded PE was not relocated
    out_size //OUT: raw size of the unmapped PE
);

并且以下是完整的解答: brutforcer_2.cpp

#include <stdio.h>

#include "peconv.h"

BYTE *g_Buffer = NULL;
const size_t g_BufferLen = 0x79;

BYTE g_Buffer2[g_BufferLen] = { 0 };

WORD (*calc_checksum) (BYTE *decoded_buffer, size_t buf_size) = NULL;

bool test_val(BYTE xor_val)
{
    for (size_t i = 0; i < g_BufferLen; i++) {
        BYTE val = g_Buffer[i];
        g_Buffer2[i] = (xor_val ^ val) + 0x22;
    }
    WORD checksum = calc_checksum(g_Buffer2, g_BufferLen);
    if (checksum == 0xfb5e) {
        return true;
    }
    return false;
}

BYTE brutforce()
{
    BYTE xor_val = 0;
    do {
      xor_val++;
    } while (!test_val(xor_val));
    return xor_val;
}
//---

bool dump_to_file(char *out_path, BYTE* buffer, size_t buf_size)
{
    FILE *f1 = fopen(out_path, "wb");
    if (!f1) {
        return false;
    }
    fwrite(buffer, 1, buf_size, f1);
    fclose(f1);
    return true;
}

int main(int argc, char *argv[])
{
#ifdef _WIN64
    printf("Compile the loader as 32bit!\n");
    system("pause");
    return 0;
#endif
    char default_path[] = "greek_to_me.exe";
    char *path = default_path;
    if (argc > 2) {
        path = argv[1];
    }
    size_t v_size = 0;

    BYTE* loaded_pe = peconv::load_pe_module(path, 
                                     v_size, 
                                     true, // load as executable?
                                     false // apply relocations ?
                                    );
    if (!loaded_pe) {
        printf("Loading module failed!\n");
        system("pause");
        return 0;
    }

    g_Buffer = (BYTE*) (0x107C + (ULONGLONG) loaded_pe);

    ULONGLONG func_offset = 0x11e6 + (ULONGLONG) loaded_pe;
    calc_checksum =  ( WORD (*) (BYTE *, size_t ) ) func_offset;

    BYTE found = brutforce();
    printf("Found: %x\n", found);

    memcpy(g_Buffer, g_Buffer2, g_BufferLen);

    size_t out_size = 0;

    /*in this case we need to use the original module base, because 
    * the loaded PE was not relocated */
    ULONGLONG module_base = peconv::get_image_base(loaded_pe); 

    BYTE* unmapped_module = peconv::pe_virtual_to_raw(loaded_pe, 
                                              v_size, 
                                              module_base, //the original module base
                                              out_size // OUT: size of the unmapped (raw) PE
                                             );
    if (unmapped_module) {
        char out_path[] = "modified_pe.exe";
        if (dump_to_file(out_path, unmapped_module, out_size)) {
            printf("Module dumped to: %s\n", out_path);
        }
        peconv::free_pe_buffer(unmapped_module, v_size);
    }
    peconv::free_pe_buffer(loaded_pe, v_size);

    system("pause");
    return 0;
}

与初始的文件相比, 我们可以看到dump出来的可执行文件的缓冲区已经覆写过了.

fig9

所以我们在IDA里看下修改的可执行文件

fig10

搞定!在0x000F107C处显示出我们的flag: et_tu_brute_force@flare-on.com

福利 – 载入和运行剔除了重定位信息的PE文件

OK, 你可能会说, 这很简单呀, 导入的函数是独立的, 所以我们可以从原来文件中抽出来, 并不需要使用任何加载器. 但是如果函数调用了一些其他的模块内的其他函数或是导入函数呢? 我们之前的方法还能生效吗? 不止如此, 剔除掉重定位信息的PE文件又能行吗?

为了回答这些问题, 我准备了其他的测试用例. 与之前载一个函数相反, 我将会在穷举程序中载入并执行完整的crackme文件.

首先我们将会修改一些东西. 这次不使用load_pe_module, 我使用load_pe_executable来加载完整的可执行文件和依赖.

BYTE* loaded_pe = (BYTE*)load_pe_executable(path, v_size);

这个函数将自动地识别出这个PE文件没有重定位信息, 并且载入到初始模块基址. 注意, 分配的指定基址处的内存可能不总会生效, 因此有时需要运行多次使得程序正确地执行. 你也必须确定加载器的模块基址跟payload需要的模块基址不相冲突(如果加载器的基址是随机的话就很好).

一旦PE文件加载完毕, 我们就需要获取它的入口地址, 并且随后我们就可以像其他函数一样调用它:

// Deploy the payload:
// read the Entry Point from the headers:
ULONGLONG ep_va = get_entry_point_rva(loaded_pe)
    + (ULONGLONG) loaded_pe;

//make pointer to the entry function:
int (*loaded_pe_entry)(void) = (int (*)(void)) ep_va;

//call the loaded PE's ep:
int ret = loaded_pe_entry();

但还要注意这与payload的具体实现细节有关, 一旦你转向执行入口点代码, 它可能在完成工作后直接退出而不会返回到你的代码中.

我打算修改穷举程序的代码, 使得在找到正确值之后crackme会继续运行. 以下是代码的完整版本: brutforcer_3.cpp

#include <stdio.h>

#include "peconv.h"

BYTE *g_Buffer = NULL;
const size_t g_BufferLen = 0x79;

BYTE g_Buffer2[g_BufferLen] = { 0 };

WORD (*calc_checksum) (BYTE *decoded_buffer, size_t buf_size) = NULL;

bool test_val(BYTE xor_val)
{
    for (size_t i = 0; i < g_BufferLen; i++) {
        BYTE val = g_Buffer[i];
        g_Buffer2[i] = (xor_val ^ val) + 0x22;
    }
    WORD checksum = calc_checksum(g_Buffer2, g_BufferLen);
    if (checksum == 0xfb5e) {
        return true;
    }
    return false;
}

BYTE brutforce()
{
    BYTE xor_val = 0;
    do {
      xor_val++;
    } while (!test_val(xor_val));
    return xor_val;
}
//---

int main(int argc, char *argv[])
{
#ifdef _WIN64
    printf("Compile the loader as 32bit!\n");
    system("pause");
    return 0;
#endif
    char default_path[] = "greek_to_me.exe";
    char *path = default_path;
    if (argc > 2) {
        path = argv[1];
    }
    size_t v_size = 0;

    BYTE* loaded_pe = peconv::load_pe_executable(path, v_size);
    if (!loaded_pe) {
        printf("Loading module failed!\n");
        system("pause");
        return 0;
    }

    g_Buffer = (BYTE*) (0x107C + (ULONGLONG) loaded_pe);

    ULONGLONG func_offset = 0x11e6 + (ULONGLONG) loaded_pe;
    calc_checksum =  ( WORD (*) (BYTE *, size_t ) ) func_offset;

    BYTE found = brutforce();
    printf("Found: %x\n", found);

    // Deploy the payload!
    // read the Entry Point from the headers:
    ULONGLONG ep_va = peconv::get_entry_point_rva(loaded_pe) + (ULONGLONG) loaded_pe;

    //make pointer to the entry function:
    int (*loaded_pe_entry)(void) = (int (*)(void)) ep_va;

    //call the loaded PE's ep:
    printf("Calling the Entry Point of the loaded module:\n");
    int res = loaded_pe_entry();
    printf("Finished: %d\n", res);
    system("pause");
    return 0;
}

为了确保一切运行正常(尽管运行payload确实建立了socket并给出跟之前载入独立函数时相同的回应), 我写了一个简短的python脚本来交流和显示回应结果: test.py

import socket
import sys
import argparse

def main():
    parser = argparse.ArgumentParser(description="Send to the Crackme")
    parser.add_argument('--key', dest="key", default="0xa2", help="The value to be sent")
    args = parser.parse_args()
    my_key = int(args.key, 16) % 255
    print '[+] Checking the key: ' + hex(my_key)
    key =  chr(my_key) + '012'
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.connect(('127.0.0.1', 2222))
        s.send(key)
        result = s.recv(512)
        if result is not None:
            print "[+] Response: " + result
        s.close()
    except socket.error:
        print "Could not connect to the socket. Is the crackme running?"

if __name__ == "__main__":
    sys.exit(main())

现在, 你可以在YouTube观看整个过程的操作: https://www.youtube.com/watch?v=x3T3qFEDkF0

以上就是我今天所准备的内容, 我希望大家都能有所收获! 该库现在正处于快速开发阶段, 所以许多东西会进行重构并优化, 敬请期待.

附录

其他解决该问题的方法如下:
* emulating the checksum function by the Unicorn engine
* using angr framework
* using WinAppDbg
* a brutforcer that talks to the original program via socket

Posted in reverse engnieering | Leave a comment

Flareon4 third challenge

本文已发表在看雪论坛, 详情可见: https://bbs.pediy.com/thread-223473.htm

题目作者: Matt Williams (@0xmwilliams)

翻译前言: 文章对代码自修改的分析很细致, 使用Unicorn框架来模拟执行代码和Capstone进行反汇编.

文中分析的程序你可以点击此处下载: greek_to_me.zip, 解压密码: www.pediy.com

greek_to_me.exe是一个Windows x86可执行文件, 如下图所示, 程序中的字符串表露了00401101处要达成的情况, 如下所示.

004010F5 push 0 ; flags
004010F7 push 2Bh ; len
004010F9 push offset aCongratulation ; "Congratulations! But wait, where's...”
004010FE push [ebp+s] ; s
00401101 call ds:send

然而, 在地址00401101前面的汇编代码却包含如下所示的奇怪汇编指令

004010A0 icebp
004010A1 push es
004010A2 sbb dword ptr [esi], 1F99C4F0h
004010A8 les edx, [ecx+1D81061Ch]
004010AE out 6, al ; DMA controller, 8237A-5.
004010AE ; channel 3 base address
004010AE ; (also sets current ad

不过也许你在此时能准确地猜测到, 程序为了能达到地址0x401101, 会修改这些奇怪的指令, 因为这些奇怪的指令运行下去, 我们的程序会极有可能崩溃. 另一种迹象能暗合我们认为这是代码自修改的推测, 那就是在查看程序的文件头时, 我们发现程序入口点所在的.text区段是可写的. 到这里, 我们正常的套路就可以往上查看分析, 看是什么能让程序选择0x401063的正确分支.

当然还有另外一种方法就是确定程序的套接字是在哪里生成的, 话不多说, 我们这就来尝试.

greek_to_me.exe包含有0x401151处的一个简单socket函数调用, 如下图所示

fig3.png

sub_401121里我们可以观察到, 程序用了一系列Windows API函数: socket,bind,listen和accept创建了一个监听本地TCP端口2222(0x8AE)的套接字

程序一直等待着监听端口的连接, 直到从建立连接的客户端那接收到最多4个字节. 接收到的字节会存储在缓冲区中并以参数的形式传递给sub_401121. 一旦有接收到字节, 该函数就能在 不停止现有连接的情况下返回一个socket句柄. 要记住, 当执行到0x4010710x401101时, 程序就会使用到它.

如果sub_401121返回了一个合法的socket句柄, 程序会继续执行, 否则程序退出. 如下代码块为寄存器赋初值, 这几个寄存器将在解码循环中发挥用处

00401029 mov ecx, offset loc_40107C
0040102E add ecx, 79h
00401031 mov eax, offset loc_40107C
00401036 mov dl, [ebp+buf]

我们看这段代码, 首先, 一个位于.text区段的可执行的代码地址赋值给ECX寄存器, 并且加上了常量值79h, 这也表明了随后将介绍的解码循环里的终止地址. 地址0x40107C赋给EAX寄存器, 代表解码循环的起始地址. 在0x401036, recv缓冲区的第1个字节被赋给了EDX寄存器的低8位

继续向下看代码块, 其中包含一个进行如下操作的循环
1. 取出存储在EAX中的地址(0x40107C)所指向内容的1个字节
2. 将取出的字节跟监听端口收到的第1个字节进行异或
3. 异或操作得到的结果再加上0x22
4. 将结果覆写回第1步取出的字节处

00401039 loc_401039:
00401039 mov bl, [eax]
0040103B xor bl, dl
0040103D add bl, 22h
00401040 mov [eax], bl
00401042 inc eax
00401043 cmp eax, ecx
00401045 jl short loc_401039

EAX中存储的地址则自增1并且跟ECX中存储的最大地址进行比较, 只有当EAX的内容跟最大地址0x4010F5相等时循环才会结束.

继续向下看, 程序随后便将刚刚修改了的代码块首地址(0040107C)和块大小0x79作参数传递给sub_4011E6.

00401047 mov eax, offset loc_40107C
0040104C mov [ebp+var_C], eax
0040104F push 79h
00401051 push [ebp+var_C]
00401054 call sub_4011E6
00401059 pop ecx
0040105A pop ecx
0040105B movzx eax, ax
0040105E cmp eax, 0FB5Eh
00401063 jz short loc_40107C

我们可以看到程序返回值的低16位(AX)赋给了EAX寄存器再将EAX跟硬编码值0xFB5E进行比较, 而比较的结果则决定了程序是跳向0x40107C还是执行到显示失败信息的分支

00401065 push 0 ; flags
00401067 push 14h ; len
00401069 push offset buf ; "Nope, that's not it."
0040106E push [ebp+s] ; s
00401071 call ds:send

获得这些信息, 我们可以做出正确假设: sub_4011E6是用来计算验证值或者说是之前解码循环所修改的字节的校验值. 并且可以确定从socket接受到的字节值是用作异或修改0x40107C0x4010F4之间代码块的key值. 而程序自修改的代码则通过一个硬编码的校验值进行验证. 因为使用的key只有单字节, 因此我们可以进行简单的暴力穷举来获得期望的key.

如果修改后的代码正常执行并且通过socket返回了Congratulations字符串, 那么就可以确定暴力穷举成功了. 基于这个假设, 我们可以编写一个如下的脚本代码帮助输出正确值:

import sys
import os
import time
import socket
TCP_IP = '127.0.0.1'
TCP_PORT = 2222
BUFFER_SIZE = 1024
for i in range (0,256):
    os.startfile(sys.argv[1])
    time.sleep(0.1)
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((TCP_IP, TCP_PORT))
    s.send(chr(i))
    data = s.recv(BUFFER_SIZE)
    s.close()
    if 'Congratulations' in data:
        print "Key found: %x" % i
        break

但如果我们并不想基于解码的字节都正确执行这样一个假设来操作, 而是自己验证解码后的校验值是否匹配, 要怎么办呢? 相比花大量时间逆向校验算法, 这次我们来尝试体验一个有趣的恶意代码分析技术: 代码模拟执行

首先, 我们提取校验函数sub_4011E6的操作码, 我们只关心在0x401265执行完后存储在AX中的返回值, 如下图所示. 并且不需要提取函数的平衡栈的结尾部分.
fig9.png

我们同样也需要从0x40107C处提取0x79长度的待解码字节. 我们提取的字节集合都在如下的用于模拟执行的python简易脚本中可见

import binascii
import struct
from unicorn import *
from unicorn.x86_const import *
from capstone import *
CHECKSUM_CODE = binascii.unhexlify(
'55 8B EC 51 8B 55 0C B9 FF 00 00 00 89 4D FC 85 D2 74 51 53 8B 5D 08 56 57 '
'6A 14 58 66 8B 7D FC 3B D0 8B F2 0F 47 F0 2B D6 0F B6 03 66 03 F8 66 89 7D '
'FC 03 4D FC 43 83 EE 01 75 ED 0F B6 45 FC 66 C1 EF 08 66 03 C7 0F B7 C0 89 '
'45 FC 0F B6 C1 66 C1 E9 08 66 03 C1 0F B7 C8 6A 14 58 85 D2 75 BB 5F 5E 5B '
'0F B6 55 FC 8B C1 C1 E1 08 25 00 FF 00 00 03 C1 66 8B 4D FC 66 C1 E9 08 66 '
'03 D1 66 0B C2'.replace(' ', ''))
ENCODED_BYTES = binascii.unhexlify(
'33 E1 C4 99 11 06 81 16 F0 32 9F C4 91 17 06 81 14 F0 06 81 15 F1 C4 91 1A '
'06 81 1B E2 06 81 18 F2 06 81 19 F1 06 81 1E F0 C4 99 1F C4 91 1C 06 81 1D '
'E6 06 81 62 EF 06 81 63 F2 06 81 60 E3 C4 99 61 06 81 66 BC 06 81 67 E6 06 '
'81 64 E8 06 81 65 9D 06 81 6A F2 C4 99 6B 06 81 68 A9 06 81 69 EF 06 81 6E '
'EE 06 81 6F AE 06 81 6C E3 06 81 6D EF 06 81 72 E9 06 81 73 7C'.replace(' ',
''))

如下的代码定义了一个函数, 给定函数一个0x000xFF之间的值, 就能执行原来程序的解码操作.

def decode_bytes(i):
    decoded_bytes = ""
    for byte in ENCODED_BYTES:
        decoded_bytes += chr(((ord(byte) ^ i) + 0x22) & 0xFF)
    return decoded_bytes

接下来, 我们定义一个函数, 函数在给定待解码字节后会利用Unicorn框架来模拟执行校验值函数

def emulate_checksum(decoded_bytes):
    # establish memory addresses for checksum code, stack, and decoded bytes
    address = 0x400000
    stack_addr = 0x410000
    dec_bytes_addr = 0x420000
    # write checksum code and decoded bytes into memory
    mu = Uc(UC_ARCH_X86, UC_MODE_32)
    mu.mem_map(address, 2 * 1024 * 1024)
    mu.mem_write(address, CHECKSUM_CODE)
    mu.mem_write(dec_bytes_addr, decoded_bytes)

如上的代码中初始化了一个32位的x86模拟器, 随后创建了一个用于存储校验函数代码, 函数内部栈以及待解码字节的2MB内存. 校验代码和待解码字节可以写入内存范围的任意地址里.

校验值函数从栈上取两个参数: 待解码字节的起始地址(0x40107C)以及长度(0x79), 下图显示了校验值函数调用后的状态.

fig13.png

为了能让校验值函数能在模拟时正确执行, 我们还需要对栈进行设置来匹配上图的栈空间布局, 并适当填充ESP寄存器. 如下所示, 在模拟执行结束后, 我们可以从emulate_checksum返回计算后的校验值

    # place the address of decoded bytes and size on the stack
    mu.reg_write(UC_X86_REG_ESP, stack_addr)
    mu.mem_write(stack_addr + 4, struct.pack('<I', dec_bytes_addr))
    mu.mem_write(stack_addr + 8, struct.pack('<I', 0x79))
    # emulate and read result in AX
    mu.emu_start(address, address + len(CHECKSUM_CODE))
    checksum = mu.reg_read(UC_X86_REG_AX)
    return checksum

现在到轻松的部分了. 我们暴力穷举异或的key, 解码字节并模拟校验操作, 然后确定哪一个key能获得正确的校验值. 如下所示

for i in range(0, 256):
    decoded_bytes = decode_bytes(i)
    checksum = emulate_checksum(decoded_bytes)
    if checksum == 0xFB5E:
        print 'Checksum matched with byte %X' % i

运行脚本最后打印出正确的单字节值: 0xA2. 然而我们仍然不明白解码后0x40107C处的指令干了什么. 我们来尝试使用Capstone反汇编器来反汇编这些指令, 如下所示

    print 'Decoded bytes disassembly:'
    md = Cs(CS_ARCH_X86, CS_MODE_32)
    for j in md.disasm(decoded_bytes, 0x40107C):
        print "0x%x:\t%s\t%s" % (j.address, j.mnemonic, j.op_str)
    break

运行我们的脚本并提供指令, 结果如下所示

Success with byte A2
Decoded bytes disassembly:
0x40107c: mov bl, 0x65
0x40107e: mov byte ptr [ebp - 0x2b], bl
0x401081: mov byte ptr [ebp - 0x2a], 0x74
0x401085: mov dl, 0x5f
0x401087: mov byte ptr [ebp - 0x29], dl
0x40108a: mov byte ptr [ebp - 0x28], 0x74
0x40108e: mov byte ptr [ebp - 0x27], 0x75
0x401092: mov byte ptr [ebp - 0x26], dl
0x401095: mov byte ptr [ebp - 0x25], 0x62
0x401099: mov byte ptr [ebp - 0x24], 0x72
0x40109d: mov byte ptr [ebp - 0x23], 0x75
0x4010a1: mov byte ptr [ebp - 0x22], 0x74
0x4010a5: mov byte ptr [ebp - 0x21], bl
0x4010a8: mov byte ptr [ebp - 0x20], dl
0x4010ab: mov byte ptr [ebp - 0x1f], 0x66
0x4010af: mov byte ptr [ebp - 0x1e], 0x6f
0x4010b3: mov byte ptr [ebp - 0x1d], 0x72
0x4010b7: mov byte ptr [ebp - 0x1c], 0x63
0x4010bb: mov byte ptr [ebp - 0x1b], bl
0x4010be: mov byte ptr [ebp - 0x1a], 0x40
0x4010c2: mov byte ptr [ebp - 0x19], 0x66
0x4010c6: mov byte ptr [ebp - 0x18], 0x6c
0x4010ca: mov byte ptr [ebp - 0x17], 0x61
0x4010ce: mov byte ptr [ebp - 0x16], 0x72
0x4010d2: mov byte ptr [ebp - 0x15], bl
0x4010d5: mov byte ptr [ebp - 0x14], 0x2d
0x4010d9: mov byte ptr [ebp - 0x13], 0x6f
0x4010dd: mov byte ptr [ebp - 0x12], 0x6e
0x4010e1: mov byte ptr [ebp - 0x11], 0x2e
0x4010e5: mov byte ptr [ebp - 0x10], 0x63
0x4010e9: mov byte ptr [ebp - 0xf], 0x6f
0x4010ed: mov byte ptr [ebp - 0xe], 0x6d
0x4010f1: mov byte ptr [ebp - 0xd], 0

我们可以看出两点, 首先栈上正在填充成一个字符串, 其次, 填充到栈上的常量十六进制值在可显字符范围内(0x20~0x7E). 依据它们在栈上的顺序依次提取出这些可显字符, 或者你可以用调试器观察栈上的内容, 得到题目解答: et_tu_brute_force@flare-on.com.

以下附上python脚本

import binascii
import struct
from unicorn import *
from unicorn.x86_const import *
from capstone import *
CHECKSUM_CODE = binascii.unhexlify(
    '55 8B EC 51 8B 55 0C B9 FF 00 00 00 89 4D FC 85 D2 74 51 53 8B 5D 08 56 57 '
    '6A 14 58 66 8B 7D FC 3B D0 8B F2 0F 47 F0 2B D6 0F B6 03 66 03 F8 66 89 7D '
    'FC 03 4D FC 43 83 EE 01 75 ED 0F B6 45 FC 66 C1 EF 08 66 03 C7 0F B7 C0 89 '
    '45 FC 0F B6 C1 66 C1 E9 08 66 03 C1 0F B7 C8 6A 14 58 85 D2 75 BB 5F 5E 5B '
    '0F B6 55 FC 8B C1 C1 E1 08 25 00 FF 00 00 03 C1 66 8B 4D FC 66 C1 E9 08 66 '
    '03 D1 66 0B C2'.replace(' ', ''))
ENCODED_BYTES = binascii.unhexlify(
    '33 E1 C4 99 11 06 81 16 F0 32 9F C4 91 17 06 81 14 F0 06 81 15 F1 C4 91 1A '
    '06 81 1B E2 06 81 18 F2 06 81 19 F1 06 81 1E F0 C4 99 1F C4 91 1C 06 81 1D '
    'E6 06 81 62 EF 06 81 63 F2 06 81 60 E3 C4 99 61 06 81 66 BC 06 81 67 E6 06 '
    '81 64 E8 06 81 65 9D 06 81 6A F2 C4 99 6B 06 81 68 A9 06 81 69 EF 06 81 6E '
    'EE 06 81 6F AE 06 81 6C E3 06 81 6D EF 06 81 72 E9 06 81 73 7C'.replace(' ',
    ''))
def decode_bytes(i):
    decoded_bytes = ""
    for byte in ENCODED_BYTES:
        decoded_bytes += chr(((ord(byte) ^ i) + 0x22) & 0xFF)
    return decoded_bytes

def emulate_checksum(decoded_bytes):
    # establish memory addresses for checksum code, stack, and decoded bytes
    address = 0x400000
    stack_addr = 0x410000
    dec_bytes_addr = 0x420000
    # write checksum code and decoded bytes into memory
    mu = Uc(UC_ARCH_X86, UC_MODE_32)
    mu.mem_map(address, 2 * 1024 * 1024)
    mu.mem_write(address, CHECKSUM_CODE)
    mu.mem_write(dec_bytes_addr, decoded_bytes)
    # place the address of decoded bytes and size on the stack
    mu.reg_write(UC_X86_REG_ESP, stack_addr)
    mu.mem_write(stack_addr + 4, struct.pack('<I', dec_bytes_addr))
    mu.mem_write(stack_addr + 8, struct.pack('<I', 0x79))
    # emulate and read result in AX
    mu.emu_start(address, address + len(CHECKSUM_CODE))
    checksum = mu.reg_read(UC_X86_REG_AX)
    return checksum
for i in range(0, 256):
    decoded_bytes = decode_bytes(i)
    checksum = emulate_checksum(decoded_bytes)
    if checksum == 0xFB5E:
        print 'Checksum matched with byte %X' % i
        print 'Decoded bytes disassembly:'
        md = Cs(CS_ARCH_X86, CS_MODE_32)
        for j in md.disasm(decoded_bytes, 0x40107C):
            print "0x%x:\t%s\t%s" % (j.address, j.mnemonic, j.op_str)
        break
Posted in reverse engnieering | Leave a comment

wsnpoem unpack part two

本文已发表在看雪论坛, 详情可见: https://bbs.pediy.com/thread-223401.htm

承接之前写的zbot变种木马wsnpoem脱壳笔记 part 1,我们这次用另外一种方式来脱壳。并且本文还将分析另外两个恶意样本。

文中分析的程序你可以点击此处下载: wsnpoem恶意样本par2.zip, 解压密码: www.pediy.com

OD重载wsnpoem-with-rootkit.exe,依然是之前的顺序,在leave处设下硬件断点后运行,让第1阶段的解密完成。然后删除设下的硬件断点,向下翻看。

不过这次我们就不会在00409EDA处的jmp eax下断了,我们再向下翻到00409F26处的mov eax, 004051B7,这句汇编代码下面是call eax,也就是说程序将要执行OEP处的代码。

1.png

我们在004051B7设下硬件执行断点,然后执行断下,程序停在了OEP处,我们删除硬件断点

2.png

那么我们接下来的步骤就跟之前一样,运行步过004051D2处的call 0040aad4导入函数表,然后将EIP重设为004051B7

之前用Ollydump+ImportREC我们手动cut chunks来修复导入表,这样不仅枯燥费力,而且还有可能误删正确的chunks导致修复失败,这次我们使用额外一个工具 – Universial Import Fixer 1.0[Final],也就是UIF。这个工具可以为我们自动修复导入表,我们只需要将wsnpoem的进程id输入进去就可以。

在重设完EIP后,我们打开UIF,然后再通过在cmd里用tasklist命令查询到wsnpoem的pid,我的是1816,将其转为16进制,也就是0x718,填入到UIF的Process ID中,取消掉默认勾选的Fix NtDll to Kernel32,然后点击Start UIF就会帮你自动修复导入表并显示修复后的信息。这些信息我们等下用ImportREC是需要使用的,也就是下图的IAT RVAIAT Size

3.png

既然修复好了导入表,那么我们就可以用Ollydump将程序转储出来,记得在dump时要取消勾选rebuild imports,转储文件保存为dump.exe

4.png

打开ImortREC,然后选择wsnpoem进程,输入OEP,并按照UIF修复给出的IAT RVAIAT Size填入到ImportREC中

你可以看到导入表直接就是可用的,我们不需要手动修复导入表。我们就可以直接转储到文件就行了。IDA打开当然也是脱壳完成并且各导入函数清晰的。

当然,还是很麻烦,那有什么更好的方法吗.当然有,这里提供了一份ollydby的脚本,我们载入程序后运行脚本,就可以帮我们自动完成脱壳和修复导入表的步骤。

我们重新载入程序,然后点击插件中的ODbgScript->Run Script … 然后选择WSNPOEM-generic-unpacker.osc

5.png

一路向下点击过去,你也可以按下Alt+L来查看脚本脱壳过程的log

6.png

脚本运行完成,显示ImportREC需要的信息

7.png

我们照之前的步骤将其填入到ImportREC, 转储到文件即可。

8.png

Posted in malware, unpack | Leave a comment

wsnpoem unpack part one

本文已发表在看雪论坛, 详情可见: https://bbs.pediy.com/thread-223393.htm

wsnpoem恶意程序是zbot木马家族的变种,经过加壳保护,我们接下来就来脱壳。

文中分析的程序你可以点击此处下载: wsnpoem恶意样本par1.zip, 解压密码: www.pediy.com

OD载入wsnpoem-with-rootkit.exe

1.png

这是wsnpoem解密的第一阶段,我们直接在00409D41的LEAVE处右键设下硬件执行断点,然后运行

2.png

然后我们选择菜单Debug->Hardware Breakpoint移除刚才设下的断点,向下翻到00409EDA

3.png

我们在这行设下软件断点(F2)然后运行,停在断点处,我们取消掉这行的断点,按下Enter进入到跳转的分支去,向下翻到00412449

4.png

在翻过了第2层解密后,00412449处所指向的004051B7便是我们的OEP,同样设下一个软件断点(F2),然后运行,停在断点处,我们取消掉这行的断点,然后步入OEP

5.png

然后向下一点,看到0040523A处,在数据窗口中转向0040FD34

6.png

可以看见,这里的这个call所调用的函数地址处是全零,这会造成程序的崩溃,因此我们可以推测在call以上的代码中有填充这个空间

我们现在边步过,边观察数据窗口中的0040FD34,看什么时候向该处填充了数据,可以很容易发现,在步过004051D2处的Call 0040AAD4后,数据窗口中填充了许多的数据

7.png

那这样看来,004051D2处的Call 0040AAD4就是将所有的函数都导入到内存空间中,而我们的0040FD34则是导入表的一部分,因此我们可以右键重新将EIP设到OEP处。

向上翻看导入表空间,貌似可能的函数地址块,也就是我们的导入表头,是从0040FB3C开始

8.png

我们也可以在ascii块中右键选择Long->Address,这样数据窗口会以地址格式进行显示,方便我们查看导入表

9.png

同样,我们向下翻看,查找导入表的结尾是在0040FEB8

10.png

这样,找到了OEP,也有导入表信息,那么我们就可以用Ollydump+ImportREC来进行脱壳,如下,点击dump保存为dump.exe

11.png

打开ImportREC,选择正在运行的wsnpoem-with-rootkit.exe,然后在OEP、RVA和SIZE处填写好我们获得的信息,然后点击Get Imports

12.png

但显然,我们的导入表函数虽然有找到,但都是无效的。所以我们需要手动修复导入表函数,因为可能在导入表内混有一些垃圾地址,所以我们需要手动进行移除,比如第一个chunk中

13.png

点击对应的shell32.dll右键显示反汇编,可以看到如下代码,显然不是一个正常的函数的代码,因此可以确定是垃圾地址。我们右键cut chunks

然后有的块显示反汇编提示read error,那么其实也是垃圾地址。依照类似的方法将所有的垃圾地址清除干净后,你就可以转储到文件,然后用IDA打开,你会发现壳已经脱干净并且导入函数也很清晰

Posted in malware, unpack | Leave a comment