6.2.4 re CSAWCTF2015 wyvern

下载文件

题目解析

  1. $ file wyvern
  2. 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
  1. $ ./wyvern
  2. +-----------------------+
  3. | Welcome Hero |
  4. +-----------------------+
  5. [!] Quest: there is a dragon prowling the domain.
  6. brute strength and magic is our only hope. Test your skill.
  7. Enter the dragon's secret: AAAAAAAA
  8. [-] You have failed. The dragon's power, speed and intelligence was greater.

看起来是 C++ 写的:

  1. [0x004013bb]> iI~lang
  2. lang cxx

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

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

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

  1. [0x004013bb]> s 0x40e271
  2. [0x0040e271]> pd 2
  3. | ; JMP XREF from 0x0040e26c (main)
  4. | 0x0040e271 8b8564feffff mov eax, dword [local_19ch]
  5. | 0x0040e277 2d37130000 sub eax, 0x1337
  6. [0x0040e271]> wa mov eax, 0x1337
  7. Written 5 bytes (mov eax, 0x1337) = wx b837130000
  8. [0x0040e271]> pd 2
  9. | ; JMP XREF from 0x0040e26c (main)
  10. | 0x0040e271 b837130000 mov eax, 0x1337
  11. | 0x0040e276 ff2d37130000 ljmp [0x0040f5b3] ; [0x40f5b3:8]=0xe4100000000a5ff
  12. [0x0040e271]> s 0x0040e276
  13. [0x0040e276]> wx 90
  14. [0x0040e276]> pd 2
  15. | 0x0040e276 90 nop
  16. | 0x0040e277 2d37130000 sub eax, 0x1337
  1. $ ./wyvern_patch
  2. +-----------------------+
  3. | Welcome Hero |
  4. +-----------------------+
  5. [!] Quest: there is a dragon prowling the domain.
  6. brute strength and magic is our only hope. Test your skill.
  7. Enter the dragon's secret: hello world
  8. [+] A great success! Here is a flag{hello world}

果然如此。

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

  1. [0x004013bb]> pdf @ sym.start_quest_std::string_
  2. ...
  3. | :| 0x0040469c e8afc8ffff call method.std::string.length()const ; 返回值 rax,是输入字符串长度 +1,因为字符末尾的 `\x00'
  4. | :| 0x004046a1 482d01000000 sub rax, 1 ; rax = rax - 1
  5. | :| 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
  6. | :| 0x004046af 41c1f902 sar r9d, 2 ; 115 >> 2 = 28
  7. | :| 0x004046b3 4963c9 movsxd rcx, r9d ; rcx = 28
  8. | :| 0x004046b6 4839c8 cmp rax, rcx ; 比较 rax 和 rcx
  9. ...
  10. [0x004013bb]> px 1 @ 0x610138
  11. - offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
  12. 0x00610138 73

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

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

  1. | :||`-``-> 0x00404c13 48bff8026100. movabs rdi, obj.hero ; 0x6102f8
  2. | :|| : 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"
  3. | :|| : 0x00404c27 e8240b0000 call method.std::vector<int,std::allocator<int>>.push_back(intconst&)
  4. ... ; 中间省略 26 段
  5. | :|| : 0x00404eb6 48bff8026100. movabs rdi, obj.hero ; 0x6102f8
  6. | :|| : 0x00404ec0 48bea8016100. movabs rsi, obj.secret_2575 ; 0x6101a8
  7. | :|| : 0x00404eca e881080000 call method.std::vector<int,std::allocator<int>>.push_back(intconst&)

找到这些加密字符:

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

继续往下看,发现函数:

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

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

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

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

继续查找 local_d8h

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

查找 local_e0h

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

查找 local_e8h

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

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

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

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

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

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

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

  1. array = [0x64, 0xd6, 0x10a, 0x171, 0x1a1, 0x20f, 0x26e,
  2. 0x2dd, 0x34f, 0x3ae, 0x41e, 0x452, 0x4c6, 0x538,
  3. 0x5a1, 0x604, 0x635, 0x696, 0x704, 0x763, 0x7cc,
  4. 0x840, 0x875, 0x8d4, 0x920, 0x96c, 0x9c2, 0xa0f]
  5. flag = ""
  6. base = 0
  7. for num in array:
  8. flag += chr(num - base)
  9. base = num
  10. print flag

Bingo!!!

  1. $ ./wyvern
  2. +-----------------------+
  3. | Welcome Hero |
  4. +-----------------------+
  5. [!] Quest: there is a dragon prowling the domain.
  6. brute strength and magic is our only hope. Test your skill.
  7. Enter the dragon's secret: dr4g0n_or_p4tric1an_it5_LLVM
  8. success
  9. [+] A great success! Here is a flag{dr4g0n_or_p4tric1an_it5_LLVM}

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

使用 Pin

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

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

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

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

编译 pintool:

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

执行下看看:

  1. $ python -c 'print("A"*28)' | ../../../pin -t obj-intel64/wyvern.so -o inscount.out -- ~/wyvern ; cat inscount.out
  2. +-----------------------+
  3. | Welcome Hero |
  4. +-----------------------+
  5. [!] Quest: there is a dragon prowling the domain.
  6. brute strength and magic is our only hope. Test your skill.
  7. Enter the dragon's secret:
  8. [-] You have failed. The dragon's power, speed and intelligence was greater.
  9. Count 1
  10. $ python -c 'print("d"+"A"*27)' | ../../../pin -t obj-intel64/wyvern.so -o inscount.out -- ~/wyvern ; cat inscount.out
  11. +-----------------------+
  12. | Welcome Hero |
  13. +-----------------------+
  14. [!] Quest: there is a dragon prowling the domain.
  15. brute strength and magic is our only hope. Test your skill.
  16. Enter the dragon's secret:
  17. [-] You have failed. The dragon's power, speed and intelligence was greater.
  18. Count 2

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

  1. import os
  2. def get_count(flag):
  3. cmd = "echo " + "\"" + flag + "\"" + " | ../../../pin -t obj-intel64/wyvern.so -o inscount.out -- ~/wyvern "
  4. os.system(cmd)
  5. with open("inscount.out") as f:
  6. count = int(f.read().split(" ")[1])
  7. return count
  8. charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-+*'"
  9. flag = list("A" * 28)
  10. count = 0
  11. for i in range(28):
  12. for c in charset:
  13. flag[i] = c
  14. # print("".join(flag))
  15. count = get_count("".join(flag))
  16. # print(count)
  17. if count == i+2:
  18. break
  19. if count == 29:
  20. break;
  21. print("".join(flag))

使用 angr

参考资料