6.2.4 re CSAWCTF2015 wyvern

下载文件

题目解析

$ file wyvern
wyvern: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=45f9b5b50d013fe43405dc5c7fe651c91a7a7ee8, not stripped
$ ./wyvern
+-----------------------+
|    Welcome Hero       |
+-----------------------+

[!] Quest: there is a dragon prowling the domain.
        brute strength and magic is our only hope. Test your skill.

Enter the dragon's secret: AAAAAAAA

[-] You have failed. The dragon's power, speed and intelligence was greater.

看起来是 C++ 写的:

[0x004013bb]> iI~lang
lang     cxx

而且不知道是什么操作,从汇编来看程序特别地难理解,我们耐住性子仔细看,在 main 函数里找到了验证输入的函数:

[0x004013bb]> pdf @ main
...
|           0x0040e261      e8ea60ffff     call sym.start_quest_std::string_    ; 验证函数
|           0x0040e266      898564feffff   mov dword [local_19ch], eax
|       ,=< 0x0040e26c      e900000000     jmp 0x40e271
|       |      ; JMP XREF from 0x0040e26c (main)
|       `-> 0x0040e271      8b8564feffff   mov eax, dword [local_19ch]
|           0x0040e277      2d37130000     sub eax, 0x1337                      ; 返回值 eax = eax -0x1337
|           0x0040e27c      0f94c1         sete cl                              ; 如果 eax 为零,则设置 cl = 1
|           0x0040e27f      488dbdc8feff.  lea rdi, [local_138h]
|           0x0040e286      898560feffff   mov dword [local_1a0h], eax
|           0x0040e28c      888d5ffeffff   mov byte [local_1a1h], cl            ; [local_1a1h] = cl
|           0x0040e292      e8e92cffff     call method.std::basic_string<char,std::char_traits<char>,std::allocator<char>>.~basic_string()
|       ,=< 0x0040e297      e900000000     jmp 0x40e29c
|       |      ; JMP XREF from 0x0040e297 (main)
|       `-> 0x0040e29c      8a855ffeffff   mov al, byte [local_1a1h]            ; al = [local_1a1h]
|           0x0040e2a2      a801           test al, 1                  ; 1      ; al & 1,即检查 al 是否为 0
|       ,=< 0x0040e2a4      0f8505000000   jne 0x40e2af                         ; 如果 al != 0,跳转,成功
|      ,==< 0x0040e2aa      e9bd000000     jmp 0x40e36c                         ; 否则,失败
...

于是我们知道,如果函数 sym.start_quest_std::string_ 返回 0x1337,说明验证成功了。来 patch 一下试试:

[0x004013bb]> s 0x40e271
[0x0040e271]> pd 2
|              ; JMP XREF from 0x0040e26c (main)
|           0x0040e271      8b8564feffff   mov eax, dword [local_19ch]
|           0x0040e277      2d37130000     sub eax, 0x1337
[0x0040e271]> wa mov eax, 0x1337
Written 5 bytes (mov eax, 0x1337) = wx b837130000
[0x0040e271]> pd 2
|              ; JMP XREF from 0x0040e26c (main)
|           0x0040e271      b837130000     mov eax, 0x1337
|           0x0040e276      ff2d37130000   ljmp [0x0040f5b3]           ; [0x40f5b3:8]=0xe4100000000a5ff
[0x0040e271]> s 0x0040e276
[0x0040e276]> wx 90
[0x0040e276]> pd 2
|           0x0040e276      90             nop
|           0x0040e277      2d37130000     sub eax, 0x1337
$ ./wyvern_patch
+-----------------------+
|    Welcome Hero       |
+-----------------------+

[!] Quest: there is a dragon prowling the domain.
        brute strength and magic is our only hope. Test your skill.

Enter the dragon's secret: hello world

[+] A great success! Here is a flag{hello world}

果然如此。

然后在验证函数中,又发现了对输入字符长度的验证过程:

[0x004013bb]> pdf @ sym.start_quest_std::string_
...
|     :|    0x0040469c      e8afc8ffff     call method.std::string.length()const    ; 返回值 rax,是输入字符串长度 +1,因为字符末尾的 `\x00'
|     :|    0x004046a1      482d01000000   sub rax, 1   ; rax = rax - 1
|     :|    0x004046a7      448b0c253801.  mov r9d, dword str.sd______________ ; obj.legend ; [0x610138:4]=115 ; U"sd\xd6\u010a\u0171\u01a1\u020f\u026e\u02dd\u034f\u03ae\u041e\u0452\u04c6\u0538\u05a1\u0604\u0635\u0696\u0704\u0763\u07cc\u0840\u0875\u08d4\u0920\u096c\u09c2\u0a0f"  ; r9d = 115
|     :|    0x004046af      41c1f902       sar r9d, 2       ; 115 >> 2 = 28
|     :|    0x004046b3      4963c9         movsxd rcx, r9d  ; rcx = 28
|     :|    0x004046b6      4839c8         cmp rax, rcx     ; 比较 rax 和 rcx
...
[0x004013bb]> px 1 @ 0x610138
- offset -   0 1  2 3  4 5  6 7  8 9  A B  C D  E F  0123456789ABCDEF
0x00610138  73

它将一个数读入 r9d 中,做 0x73 >> 2 = 28 的操作,然后与输入字符串比较,所以我们猜测输入字符长度应为 28。

由于有下面这段指令,它将字符放到 obj.hero 处的 vector 中,我们有理由认为,验证是一个字符一个字符进行的,而且长度就是 28:

| :||`-``-> 0x00404c13      48bff8026100.  movabs rdi, obj.hero        ; 0x6102f8
| :|| :     0x00404c1d      48be3c016100.  movabs rsi, obj.secret_100  ; 0x61013c ; U"d\xd6\u010a\u0171\u01a1\u020f\u026e\u02dd\u034f\u03ae\u041e\u0452\u04c6\u0538\u05a1\u0604\u0635\u0696\u0704\u0763\u07cc\u0840\u0875\u08d4\u0920\u096c\u09c2\u0a0f"
| :|| :     0x00404c27      e8240b0000     call method.std::vector<int,std::allocator<int>>.push_back(intconst&)
... ; 中间省略 26 段
| :|| :     0x00404eb6      48bff8026100.  movabs rdi, obj.hero        ; 0x6102f8
| :|| :     0x00404ec0      48bea8016100.  movabs rsi, obj.secret_2575 ; 0x6101a8
| :|| :     0x00404eca      e881080000     call method.std::vector<int,std::allocator<int>>.push_back(intconst&)

找到这些加密字符:

[0x004013bb]> px 28*4 @ 0x61013c
- offset -   0 1  2 3  4 5  6 7  8 9  A B  C D  E F  0123456789ABCDEF
0x0061013c  6400 0000 d600 0000 0a01 0000 7101 0000  d...........q...
0x0061014c  a101 0000 0f02 0000 6e02 0000 dd02 0000  ........n.......
0x0061015c  4f03 0000 ae03 0000 1e04 0000 5204 0000  O...........R...
0x0061016c  c604 0000 3805 0000 a105 0000 0406 0000  ....8...........
0x0061017c  3506 0000 9606 0000 0407 0000 6307 0000  5...........c...
0x0061018c  cc07 0000 4008 0000 7508 0000 d408 0000  ....@...u.......
0x0061019c  2009 0000 6c09 0000 c209 0000 0f0a 0000   ...l...........

继续往下看,发现函数:

| :|||:|    0x0040484f      e86cd4ffff     call sym.sanitize_input_std::string_

就是它决定了返回值,如果输入字符串正确,则该函数返回 0x1337

接下来就是跟踪各种交叉引用,从 obj.hero 里依次取值:

[0x0040484f]> pdf @ sym.sanitize_input_std::string_ ~obj.hero
| --------> 0x00402726      b8f8026100     mov eax, obj.hero           ; 0x6102f8
| --------> 0x00402d37      b8f8026100     mov eax, obj.hero           ; 0x6102f8
[0x0040484f]> pd 5 @ 0x00402726
|              ; JMP XREF from 0x0040271b (sym.sanitize_input_std::string_)
|           0x00402726      b8f8026100     mov eax, obj.hero           ; 0x6102f8
|           0x0040272b      89c7           mov edi, eax
|           0x0040272d      488bb530ffff.  mov rsi, qword [local_d0h]
|           0x00402734      e8072f0000     call method.std::vector<int,std::allocator<int>>.operator[](unsignedlong)
|           0x00402739      48898528ffff.  mov qword [local_d8h], rax   ; 将取出的值存入 [local_d8h]
[0x0040484f]> pd 5 @ 0x00402d37
|              ; JMP XREF from 0x00402d2c (sym.sanitize_input_std::string_)
|           0x00402d37      b8f8026100     mov eax, obj.hero           ; 0x6102f8
|           0x00402d3c      89c7           mov edi, eax
|           0x00402d3e      488bb508ffff.  mov rsi, qword [local_f8h]
|           0x00402d45      e8f6280000     call method.std::vector<int,std::allocator<int>>.operator[](unsignedlong)
|           0x00402d4a      48898500ffff.  mov qword [local_100h], rax

继续查找 local_d8h

[0x0040484f]> pdf @ sym.sanitize_input_std::string_ ~local_d8h
|           ; var int local_d8h @ rbp-0xd8
| |:||:|:   0x00402739      48898528ffff.  mov qword [local_d8h], rax
| --------> 0x00402819      488b8528ffff.  mov rax, qword [local_d8h]
[0x0040484f]> pd 15 @ 0x00402819
|              ; JMP XREF from 0x00403ea9 (sym.sanitize_input_std::string_)
|              ; JMP XREF from 0x0040280e (sym.sanitize_input_std::string_)
|           0x00402819      488b8528ffff.  mov rax, qword [local_d8h]   ; 将 [local_d8h] 的值存入 rax
|           0x00402820      8b08           mov ecx, dword [rax]         ; 将 [rax] 存入 ecx
|           0x00402822      8b1425940561.  mov edx, dword [obj.x17]    ; [0x610594:4]=0
|           0x00402829      8b3425340461.  mov esi, dword [obj.y18]    ; [0x610434:4]=0
|           0x00402830      89d7           mov edi, edx
|           0x00402832      81ef01000000   sub edi, 1
|           0x00402838      0fafd7         imul edx, edi
|           0x0040283b      81e201000000   and edx, 1
|           0x00402841      81fa00000000   cmp edx, 0
|           0x00402847      410f94c0       sete r8b
|           0x0040284b      81fe0a000000   cmp esi, 0xa                ; 10
|           0x00402851      410f9cc1       setl r9b
|           0x00402855      4508c8         or r8b, r9b
|           0x00402858      41f6c001       test r8b, 1                 ; 1
|           0x0040285c      898d20ffffff   mov dword [local_e0h], ecx   ; 将 ecx 存入 [local_e0h]

查找 local_e0h

[0x0040484f]> pdf @ sym.sanitize_input_std::string_ ~local_e0h
|           ; var int local_e0h @ rbp-0xe0
| |:||:|:   0x0040285c      898d20ffffff   mov dword [local_e0h], ecx
| --------> 0x00402a73      8b8520ffffff   mov eax, dword [local_e0h]
[0x0040484f]> pd 4 @ 0x00402a73
|              ; JMP XREF from 0x00403f39 (sym.sanitize_input_std::string_)
|              ; JMP XREF from 0x00402a68 (sym.sanitize_input_std::string_)
|           0x00402a73      8b8520ffffff   mov eax, dword [local_e0h]   ; 将 [local_e0h] 的值存入 eax,即 eax 是加密字符
|           0x00402a79      8b8d18ffffff   mov ecx, dword [local_e8h]   ; ecx 是经过处理的输入字符
|           0x00402a7f      39c8           cmp eax, ecx                 ; 进行比较。逐字符比较,不相等时退出。
|           0x00402a81      0f94c2         sete dl

查找 local_e8h

[0x0040484f]> pdf @ sym.sanitize_input_std::string_ ~local_e8h
|           ; var int local_e8h @ rbp-0xe8
| |:||:|:   0x00402a25      898518ffffff   mov dword [local_e8h], eax
| |:||:|:   0x00402a79      8b8d18ffffff   mov ecx, dword [local_e8h]
[0x0040484f]> pd -2 @ 0x00402a25
|              ; JMP XREF from 0x00402a11 (sym.sanitize_input_std::string_)
|           0x00402a1c      488b7d80       mov rdi, qword [local_80h]
|           0x00402a20      e88beaffff     call sym.transform_input_std::vector_int_std::allocator_int___
[0x0040484f]> pdf @ sym.sanitize_input_std::string_ ~local_80h
|           ; var int local_80h @ rbp-0x80
|  :||:|    0x00401e23      4c895580       mov qword [local_80h], r10
| --------> 0x0040286d      488b7d80       mov rdi, qword [local_80h]
| --------> 0x00402a1c      488b7d80       mov rdi, qword [local_80h]
| --------> 0x00402b58      488b7d80       mov rdi, qword [local_80h]

继续跟踪 local_80,你会发现输入的字符放在 0x6236a8 的位置。

继续往下看,终于看到了曙光,下面这个函数对输入字符做一些变换:

|           0x00402a20      e88beaffff     call sym.transform_input_std::vector_int_std::allocator_int___

进入该函数,找到字符转换的核心算法:

| |:||:|:   0x004017dd      e85e3e0000     call method.std::vector<int,std::allocator<int>>.operator[](unsignedlong)    ; 获得一个输入字符的地址 rax
| |:||:|:   0x004017e2      8b08           mov ecx, dword [rax]                                                         ; 将该字符赋值给 ecx
| |:||:|:   0x004017e4      488b45e0       mov rax, qword [local_20h]                                                   ; 获得上一个加密字符的地址 rax
| |:||:|:   0x004017e8      0308           add ecx, dword [rax]                                                         ; 上一个加密字符加上当前输入字符
| |:||:|:   0x004017ea      8908           mov dword [rax], ecx                                                         ; 将当前加密字符放回

例如第二个字符是 r,即 0x72 + 0x64 = 0xd6,第三个字符 4,即 0x34 + 0xd6 = 0x10a,依次类推。由此可以写出解密算法:

array = [0x64,  0xd6,  0x10a, 0x171, 0x1a1, 0x20f, 0x26e,
         0x2dd, 0x34f, 0x3ae, 0x41e, 0x452, 0x4c6, 0x538,
         0x5a1, 0x604, 0x635, 0x696, 0x704, 0x763, 0x7cc,
         0x840, 0x875, 0x8d4, 0x920, 0x96c, 0x9c2, 0xa0f]

flag = ""
base = 0
for num in array:
    flag += chr(num - base)
    base = num

print flag

Bingo!!!

$ ./wyvern
+-----------------------+
|    Welcome Hero       |
+-----------------------+

[!] Quest: there is a dragon prowling the domain.
        brute strength and magic is our only hope. Test your skill.

Enter the dragon's secret: dr4g0n_or_p4tric1an_it5_LLVM
success

[+] A great success! Here is a flag{dr4g0n_or_p4tric1an_it5_LLVM}

常规方法逆向出来了,但实在是太复杂,我们可以使用一些取巧的方法,想想前面讲过的 Pin 和 angr,下面我们就分别用这两种工具来解决它。

使用 Pin

首先要知道验证是逐字符的,一旦有不相同就会退出,也就是说执行下面语句的次数减一就是正确字符的个数:

|           0x00402a7f      39c8           cmp eax, ecx                 ; 进行比较。逐字符比较,不相等时退出。

另外只有验证成功,才会跳转到地址 0x0040e2af,所以把 6.2.1 节的 pintool 拿来改成下面这样,当 count 为 28+1=29 时,验证成功:

// This function is called before every instruction is executed
VOID docount(void *ip) {
    if ((long int)ip == 0x00402a7f) icount++;   // 0x00402a7f cmp eax, ecx
    if ((long int)ip == 0x0040e2af) icount++;   // 0x0040e2a2 jne 0x0040e2af
}

编译 pintool:

$ cp dont_panic.cpp source/tools/MyPintool
[MyPinTool]$ make obj-intel64/wyvern.so TARGET=intel64

执行下看看:

$ python -c 'print("A"*28)' | ../../../pin -t obj-intel64/wyvern.so -o inscount.out -- ~/wyvern ; cat inscount.out
+-----------------------+
|    Welcome Hero       |
+-----------------------+

[!] Quest: there is a dragon prowling the domain.
        brute strength and magic is our only hope. Test your skill.

Enter the dragon's secret:
[-] You have failed. The dragon's power, speed and intelligence was greater.
Count 1
$ python -c 'print("d"+"A"*27)' | ../../../pin -t obj-intel64/wyvern.so -o inscount.out -- ~/wyvern ; cat inscount.out
+-----------------------+
|    Welcome Hero       |
+-----------------------+

[!] Quest: there is a dragon prowling the domain.
        brute strength and magic is our only hope. Test your skill.

Enter the dragon's secret:
[-] You have failed. The dragon's power, speed and intelligence was greater.
Count 2

看起来不错,写个脚本自动化该过程:

import os

def get_count(flag):
    cmd = "echo " + "\"" + flag + "\"" + " | ../../../pin -t obj-intel64/wyvern.so -o inscount.out -- ~/wyvern "
    os.system(cmd)
    with open("inscount.out") as f:
        count = int(f.read().split(" ")[1])
    return count

charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-+*'"

flag = list("A" * 28)
count = 0
for i in range(28):
    for c in charset:
        flag[i] = c
        # print("".join(flag))
        count = get_count("".join(flag))
        # print(count)
        if count == i+2:
            break
    if count == 29:
        break;
print("".join(flag))

使用 angr

参考资料

Last updated