使用符号执行驯服受纳米虫保护的MIPS二进制文件:No Such Crackme

本文详细介绍了如何通过符号执行技术分析受纳米虫保护的MIPS二进制文件,包括环境搭建、父进程行为分析、符号执行引擎构建以及使用Z3求解器逆向加密算法,最终成功破解挑战。

使用符号执行驯服受纳米虫保护的MIPS二进制文件:No Such Crackme

逆向工程:龙潭虎穴

MIPS 101

该挑战的第一个有趣细节是它是一个MIPS二进制文件;对我来说这确实有些异国情调。我主要看的是Intel汇编,所以有机会研究一个未知架构总是很吸引人。你知道这就像发现一个新玩具,所以我忍不住开始阅读MIPS基础知识。

这部分将仅描述您需要理解和破解二进制文件的基本信息;正如我所说,我根本不是MIPS专家。根据我所看到的,这与您在Intel x86 CPU上看到的非常相似:

  • 它是小端序(注意也存在大端序版本,但本文不会涉及),
  • 它有更多的通用寄存器,
  • 调用约定类似于__fastcall:通过寄存器传递参数,并在$v0中获取函数返回值,
  • 与x86不同,MIPS是RISC,因此更容易上手(相信我),
  • 当然,有一个IDA处理器,
  • Linux和常规工具也适用于MIPS,因此我们可以使用我们习惯的“正常”工具,
  • 它也使用堆栈,但比x86少得多,因为大多数操作都在寄存器中(至少在这个挑战中)。

设置适当的调试环境

这个问题的答案是Qemu,正如预期的那样。您甚至可以在aurel32的网站上下载已经完全准备好且可用的Debian镜像。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
overclok@wildout:~/chall/nsc2014$ wget https://people.debian.org/~aurel32/qemu/mipsel/debian_wheezy_mipsel_standard.qcow2
overclok@wildout:~/chall/nsc2014$ wget https://people.debian.org/~aurel32/qemu/mipsel/vmlinux-3.2.0-4-4kc-malta
overclok@wildout:~/chall/nsc2014$ cat start_vm.sh
qemu-system-mipsel -M malta -kernel vmlinux-3.2.0-4-4kc-malta -hda debian_wheezy_mipsel_standard.qcow2 -vga none -append "root=/dev/sda1 console=tty0" -nographic
overclok@wildout:~/chall/nsc2014$ ./start_vm.sh
[    0.000000] Initializing cgroup subsys cpuset
[    0.000000] Initializing cgroup subsys cpu
[    0.000000] Linux version 3.2.0-4-4kc-malta (debian-kernel@lists.debian.org) (gcc version 4.6.3 (Debian 4.6.3-14) ) #1 Debian 3.2.51-1
[...]
debian-mipsel login: root
Password:
Last login: Sat Oct 11 00:04:51 UTC 2014 on ttyS0
Linux debian-mipsel 3.2.0-4-4kc-malta #1 Debian 3.2.51-1 mips

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
root@debian-mipsel:~# uname -a
Linux debian-mipsel 3.2.0-4-4kc-malta #1 Debian 3.2.51-1 mips GNU/Linux

随意在虚拟环境中安装您的基本工具,有些工具可能会派上用场(尽管安装它们可能需要一些时间):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
root@debian-mipsel:~# aptitude install strace gdb gcc python
root@debian-mipsel:~# wget https://raw.githubusercontent.com/zcutlip/gdbinit-mips/master/gdbinit-mips
root@debian-mipsel:~# mv gdbinit-mips ~/.gdbinit
root@debian-mipsel:~# gdb -q /home/user/crackmips
Reading symbols from /home/user/crackmips...(no debugging symbols found)...done.
(gdb) b *main
Breakpoint 1 at 0x402024
(gdb) r 'doar-e ftw'
Starting program: /home/user/crackmips 'doar-e ftw'
-----------------------------------------------------------------
[registers]
  V0: 7FFF6D30  V1: 77FEE000  A0: 00000002  A1: 7FFF6DF4
  A2: 7FFF6E00  A3: 0000006C  T0: 77F611E4  T1: 0FFFFFFE
  T2: 0000000A  T3: 77FF6ED0  T4: 77FE5590  T5: FFFFFFFF
  T6: F0000000  T7: 7FFF6BE8  S0: 00000000  S1: 00000000
  S2: 00000000  S3: 00000000  S4: 004FD268  S5: 004FD148
  S6: 004D0000  S7: 00000063  T8: 77FD7A5C  T9: 00402024
  GP: 77F67970  S8: 0000006C  HI: 000001A5  LO: 00005E17
  SP: 7FFF6D18  PC: 00402024  RA: 77DF2208
-----------------------------------------------------------------
[code]
=> 0x402024 <main>:     addiu   sp,sp,-72
    0x402028 <main+4>:   sw      ra,68(sp)
    0x40202c <main+8>:   sw      s8,64(sp)
    0x402030 <main+12>:  move    s8,sp
    0x402034 <main+16>:  sw      a0,72(s8)
    0x402038 <main+20>:  sw      a1,76(s8)
    0x40203c <main+24>:  lw      v1,72(s8)
    0x402040 <main+28>:  li      v0,2

最后,您应该能够运行这个野兽:

1
2
3
4
root@debian-mipsel:~# /home/user/crackmips
usage: /home/user/crackmips password
root@debian-mipsel:~# /home/user/crackmips 'doar-e ftw'
WRONG PASSWORD

太棒了 :-)。

大局观

现在我们有了启动和调试挑战的方法,我们可以在IDA中打开二进制文件并开始理解使用了哪种保护方案。和往常一样,在这一点上,我们对细节真的不感兴趣:我们只想了解它是如何工作的,以及我们需要针对哪些部分来获得“好孩子”消息。

在IDA中花了一些时间后,二进制文件的工作方式如下:

  • 它检查用户是否提供了一个参数:序列号
  • 它检查提供的序列号是否为48个字符长
  • 它将字符串转换为6个DWORD(/!\ 陷阱警告:转换有点奇怪,请确保验证您的算法)
  • 野兽分叉成两个:
    • [父进程] 似乎以某种方式驱动子进程,稍后会详细介绍
    • [子进程] 在执行一大段代码修改(原地)6个原始DWORD后,它们与以下字符串进行比较 [ Synacktiv + NSC = <3 ]
    • [子进程] 如果比较成功,您就赢了,否则就输了

基本上,我们需要找到6个输入DWORD,它们将在输出中生成以下内容:0x7953205b, 0x6b63616e, 0x20766974, 0x534e202b, 0x203d2043, 0x5d20333c。我们还知道父进程将与它的子进程交互,因此我们需要研究两个代码以确保正确理解挑战。

如果您更喜欢代码,以下是C语言的大局观:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
int main(int argc, char *argv[])
{
    DWORD serial_dwords[6] = {0};
    if(argc != 2)
        Usage();

    // Conversion
    a2i(argv[1], serial_dwords);

    pid_t pid = fork();
    if(pid != 0)
    {
        // Father
        // a lot of stuff going on here, we will see that later on
    }
    else
    {
        // Son
        // a lot of stuff going on here, we will see that later on

        char *clear = (char*)serial_dwords;
        bool win = memcmp(clear, "[ Synacktiv + NSC = <3 ]", 48);
        if(win)
            GoodBoy();
        else
            BadBoy();
    }
}

动手实践

父进程负责

在了解大局后,我做的第一件事是查看父进程的代码。为什么?代码似乎比子进程的代码简单一些,所以我认为研究父进程会更合理,以理解我们需要颠覆的保护类型。

您甚至可以使用strace来更清晰地了解使用的系统调用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
root@debian-mipsel:~# strace -i /home/user/crackmips $(python -c 'print "1"*48')
[7734e224] execve("/home/user/crackmips", ["/home/user/crackmips", "11111111111111111111111111111111"...], [/* 12 vars */]) = 0
[...]
[77335e70] clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x77491068) = 2539
[77335e70] --- SIGCHLD (Child exited) @ 0 (0) ---
[7733557c] waitpid(2539, [{WIFSTOPPED(s) && WSTOPSIG(s) == SIGTRAP}], __WALL) = 2539
[7737052c] ptrace(PTRACE_GETREGS, 2539, 0, 0x7f8f87c4) = 0
[7737052c] ptrace(PTRACE_SETREGS, 2539, 0, 0x7f8f87c4) = 0
[7737052c] ptrace(PTRACE_CONT, 2539, 0, SIG_0) = 0
[7737052c] --- SIGCHLD (Child exited) @ 0 (0) ---
[7733557c] waitpid(2539, [{WIFSTOPPED(s) && WSTOPSIG(s) == SIGTRAP}], __WALL) = 2539
[7737052c] ptrace(PTRACE_GETREGS, 2539, 0, 0x7f8f87c4) = 0
[7737052c] ptrace(PTRACE_SETREGS, 2539, 0, 0x7f8f87c4) = 0
[7737052c] ptrace(PTRACE_CONT, 2539, 0, SIG_0) = 0
[7737052c] --- SIGCHLD (Child exited) @ 0 (0) ---
[7733557c] waitpid(2539, [{WIFSTOPPED(s) && WSTOPSIG(s) == SIGTRAP}], __WALL) = 2539
[7737052c] ptrace(PTRACE_GETREGS, 2539, 0, 0x7f8f87c4) = 0
[7737052c] ptrace(PTRACE_SETREGS, 2539, 0, 0x7f8f87c4) = 0
[7737052c] ptrace(PTRACE_CONT, 2539, 0, SIG_0) = 0
[7733557c] waitpid(2539, [{WIFSTOPPED(s) && WSTOPSIG(s) == SIGTRAP}], __WALL) = 2539
[7733557c] --- SIGCHLD (Child exited) @ 0 (0) ---
[7737052c] ptrace(PTRACE_GETREGS, 2539, 0, 0x7f8f87c4) = 0
[7737052c] ptrace(PTRACE_SETREGS, 2539, 0, 0x7f8f87c4) = 0
[7737052c] ptrace(PTRACE_CONT, 2539, 0, SIG_0) = 0
[7737052c] --- SIGCHLD (Child exited) @ 0 (0) ---
[7733557c] waitpid(2539, [{WIFSTOPPED(s) && WSTOPSIG(s) == SIGTRAP}], __WALL) = 2539
[7737052c] ptrace(PTRACE_GETREGS, 2539, 0, 0x7f8f87c4) = 0
[7737052c] ptrace(PTRACE_SETREGS, 2539, 0, 0x7f8f87c4) = 0
[7737052c] ptrace(PTRACE_CONT, 2539, 0, SIG_0) = 0
[7737052c] --- SIGCHLD (Child exited) @ 0 (0) ---
[7733557c] waitpid(2539, [{WIFSTOPPED(s) && WSTOPSIG(s) == SIGTRAP}], __WALL) = 2539
[7737052c] ptrace(PTRACE_GETREGS, 2539, 0, 0x7f8f87c4) = 0
[7737052c] ptrace(PTRACE_SETREGS, 2539, 0, 0x7f8f87c4) = 0
[7737052c] ptrace(PTRACE_CONT, 2539, 0, SIG_0) = 0
[...]

这是一个我完全没有预料到的有趣输出。我们在这里看到的是父进程通过修改(可能,我们稍后会找出)其上下文来驱动其子进程,每次子进程SIGTRAP时(注意waitpid的第二个参数)。

从这里开始,如果您非常熟悉不同类型的软件保护(我不是说我是这个领域的专家,但我恰好知道这个 :-P),您几乎可以猜到那是什么:纳米虫!

纳米虫 101

纳米虫是一种相当不错的保护。不过,这是一个相当通用的名称;您真的可以以任何您喜欢的方式使用这种保护方案:您的想象力是唯一的限制。老实说,这是我第一次在Unix系统上看到这种保护;真是个好惊喜!

它通常这样工作:

  • 您有两个进程:一个驱动进程和一个被驱动进程;一个父进程和一个子进程
  • 驱动进程使用目标平台上可用的调试API(这里是ptrace,Windows上是CreateProcess/DebugActiveProcess)附加到被驱动进程
  • 请注意,根据设计,您将无法附加到子进程,因为Windows和Linux都阻止了这一点(根据设计):有些人称这部分为DebugBlocker
  • 不过,您将能够调试驱动进程

通常有趣的代码在子进程中,但再次强调,您可以做任何您想做的事情。基本上,如果您想要一个有效的保护,有两个规则:

  • 确保被驱动进程没有其驱动进程就无法运行,并且它们彼此紧密绑定
  • 保护的强度在于两个进程之间的这种强大/紧密的绑定
  • 设计您的算法,使得移除驱动进程真的非常困难/痛苦/让攻击者发疯

被驱动进程可以通过例如int3/break指令来调用/通知驱动进程SIGTRAP

正如我所说,我更将这种保护方案视为一个配方:您真的可以随意自定义它。如果您想了解更多关于这个主题的内容,这里有一些您应该查看的链接:

父进程如何工作

现在是时候详细研究父进程了;以下是它的工作方式:

  • 它做的第一件事是waitpid,直到其子进程触发SIGTRAP
  • 驱动进程检索子进程的CPU上下文,更具体地说是其程序计数器:$pc
  • 然后我们有一个巨大的算术计算块。但在花了一些时间研究它之后,我们可以将这个巨大的块视为一个黑盒函数,它接受两个参数:子进程的程序计数器和某种计数器值(因为这段代码将在循环中执行,对于每个SIGTRAP,这个变量将递增)。它生成一个单一输出,这是一个32位值,我称之为第一个魔法值。
    • 不过,让我们不要专注于这个块实际上在做什么,我们将在下一部分开发一些工具来处理这个问题 :-) 所以让我们继续前进!
  • 然后,这个魔法值用于在QWORDs数组(606个QWORD,是子进程中断指令数量的6倍——稍后您会理解这一点,别担心)中找到一个特定条目。基本上,
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计