前回 は、picoCTF の picoCTF 2023 の General Skills をやってみました。全6問を全て解きました。
今回は、引き続き、picoCTF 2023 の Reverse Engineering をやっていきます。Medium が 7問、Hard が 2問です。
それでは、やっていきます。
はじめに
「セキュリティ」の記事一覧です。良かったら参考にしてください。
セキュリティの記事一覧
picoCTF の公式サイトは以下です。英語のサイトですが、シンプルで分かりやすいので困らずに進めることができます。
picoctf.com
それでは、やっていきます。
picoCTF 2023:Reverse Engineering
ポイントの低い順にやっていきます。
Ready Gladiator 0(100ポイント)
Medium の問題です。サーバを起動して進める問題のようです。
インスタンス(サーバ)を起動すると、ソースコード?(imp.red)がダウンロードできました。内容は以下です。
;redcode
;name Imp Ex
;assert 1
mov 0, 1
end
とにかく、サーバに接続してみます。うーん、全く分かりません。
$ nc saturn.picoctf.net 52451 < imp.red
;redcode
;name Imp Ex
;assert 1
mov 0, 1
end
Submit your warrior: (enter 'end' when done)
Warrior1:
;redcode
;name Imp Ex
;assert 1
mov 0, 1
end
Rounds: 100
Warrior 1 wins: 0
Warrior 2 wins: 0
Ties: 100
Try again. Your warrior (warrior 1) must lose all rounds, no ties.
次は、imp.red を入力せずに接続してみます。あ、フラグが表示されました。全然分かりませんでした。
$ nc saturn.picoctf.net 52451
Submit your warrior: (enter 'end' when done)
end
end
Warrior1:
end
Warning:
Missing ';assert'. Warrior may not work with the current setting
Warning:
No instructions
Number of warnings: 2
Rounds: 100
Warrior 1 wins: 0
Warrior 2 wins: 100
Ties: 0
You did it!
picoCTF{h3r0_t0_z3r0_4m1r1gh7_a220a377}
「Ready Gladiator 1」、「Ready Gladiator 2」と続くみたいなので、その時に理解しようと思います。
Reverse(100ポイント)
Medium の問題です。1つのファイル()がダウンロードできます。
表層解析します。
$ file ret
ret: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=32195c65c0c8ca5bd239fa824d4d79231cca5f78, for GNU/Linux 3.2.0, not stripped
$ checksec --file=ret
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Full RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH 69 Symbols No 0 1 ret
実行できそうなので、やってみます。正しいパスワードを探す必要がありそうです。
$ ./ret
Enter the password to unlock this file: abc
You entered: abc
Access denied
Ghidra で見てみます。
main関数です。うーん、フラグが見えちゃってますね(笑)。
一応、local_38 と同じ値の文字列を入力すればいいようです。local_38 は、8byteですが、文字列なので local_30、local_28、local_20、local_18 と続きます。local_10 はスタックカナリアなので、何が入るかわかりません。これは、解けない問題ということかもしれません。
undefined8 main(void)
{
int iVar1;
long in_FS_OFFSET;
char local_68 [48];
undefined8 local_38;
undefined8 local_30;
undefined8 local_28;
undefined8 local_20;
undefined8 local_18;
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
local_38 = 0x7b4654436f636970;
local_30 = 0x337633725f666c33;
local_28 = 0x75735f676e693572;
local_20 = 0x6c75663535656363;
local_18 = 0x643536393338635f;
printf("Enter the password to unlock this file: ");
__isoc99_scanf(&DAT_00102031,local_68);
printf("You entered: %s\n",local_68);
iVar1 = strcmp(local_68,(char *)&local_38);
if (iVar1 == 0) {
puts("Password correct, please see flag: picoCTF{3lf_r3v3r5ing_succe55ful_c83965de}");
puts((char *)&local_38);
}
else {
puts("Access denied");
}
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
__stack_chk_fail();
}
return 0;
}
文字列は以下です。フラグがパスワードになっていました。途中までですけど。
$ python -c 'import struct; print(struct.pack("<QQQQQ",0x7b4654436f636970, 0x337633725f66
6c33, 0x75735f676e693572, 0x6c75663535656363, 0x643536393338635f))'
b'picoCTF{3lf_r3v3r5ing_succe55ful_c83965d'
Safe Opener 2(100ポイント)
Medium の問題です。1つのファイル(SafeOpener.class)がダウンロードできます。
fileコマンドを実行してみます。Java の classファイルでした。
$ file SafeOpener.class
SafeOpener.class: compiled Java class data, version 52.0 (Java 1.8)
jadx を使ってデコンパイルします。また、フラグが見えちゃってます(笑)。
package defpackage;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Base64;
public class SafeOpener {
public static void main(String[] args) throws IOException {
BufferedReader keyboard = new BufferedReader(new InputStreamReader(System.in));
Base64.Encoder encoder = Base64.getEncoder();
for (int i = 0; i < 3; i++) {
System.out.print("Enter password for the safe: ");
String key = keyboard.readLine();
String encodedkey = encoder.encodeToString(key.getBytes());
System.out.println(encodedkey);
boolean isOpen = openSafe(encodedkey);
if (!isOpen) {
System.out.println("You have " + (2 - i) + " attempt(s) left");
} else {
return;
}
}
}
public static boolean openSafe(String password) {
if (password.equals("picoCTF{SAf3_0p3n3rr_y0u_solv3d_it_5bfbd6f1}")) {
System.out.println("Sesame open");
return true;
}
System.out.println("Password is incorrect\n");
return false;
}
}
簡単な問題と難しい問題の差が激しいですね。
timer(100ポイント)
Medium の問題です。1つのファイル(timer.apk)がダウンロードできます。
fileコマンドを実行してみます。APKファイルでした。2問続けて JAVAのようです。
$ file timer.apk
timer.apk: Android package (APK), with zipflinger virtual entry, with APK Signing Block
jadx を使ってデコンパイルします。今度は、とても大きなファイル(約4.5MB)です。
あ、2個目に見たファイル(AndroidManifest.xml)にフラグがありました(笑)。
picoCTF{t1m3r_r3v3rs3d_succ355fully_17496}
でした。
Virtual Machine 0(100ポイント)
Medium の問題です。2つのファイル(input.txt と Virtual-Machine-0.zip)がダウンロードできます。
1つ目のファイルは、テキストファイルで、内容は、39722847074734820757600524178581224432297292490103995897672826024691504153
でした。16進数では、0x167b7a4973dda7b247d70a7d3ddfd67adfe2a34a0a463cd77aa43d09a41419
でした。ASCIIコードにはなりそうにない感じです。
2つ目のファイルは、ZIPファイルで解凍したら、Virtual-Machine-0.dae というファイルが得られました。うさみみハリケーンで確認します。COLLADAデータというもので、Web検索したところ、3DCG用のデータらしいです。
うーん、困ったときは、とりあえず、Chrome に放り込んでみます。XML形式でそれなりに読める感じです。オンラインにビューアがあったのでアップしてみます。なんか 3D でグルグル動かせる感じです。ビューアでは何ともならなさそうなので、XML のところを見てみると、MecaBricks というソフトで作られたようで、オンラインでレゴブロックを作れるソフトのようです。
ただ、調べてみると、MecaBricks のソフトでは、dae の拡張子からはインポートできなさそうです。さらに調べてみると、blender というソフトがインポートできるようです。フリー、かつ、インストールせずに、実行ファイルがダウンロードできるようなので、Linux(ParrotOS)にダウンロードして実行してみました。しかし、セグメンテーションフォールトで起動しませんでした。仕方ないので、Windows にインストールしてみます。インストールが終わって、daeファイルをインポートすることが出来ました。黒い箱に赤と青のつまみ?があります。内部を見ると歯車があって、赤を回すと、青が回る感じになっています。周りの黒いレゴブロックが邪魔なので削除してみました。
赤を回すと青が回るようですが、実際に動かそうとしても、赤のつまみだけが動いて、連動して青を動かすことは出来ませんでした。YouTube で blender入門みたいな動画を見ると、連動して動かせそうでしたが、設定が難しそうだったので断念しました。
動かすことは本筋ではないので、問題の答えを考えていきます。input.txt の内容で赤を動かすと、青が出力ということなので、青がどれだけ回るかということでしょうか。赤い歯は 40個の凹凸がありました。上のキャプチャでは見にくいかもしれませんが、カメラ視点を動かして、黒の歯と青の歯が 8個だったことが分かりました。赤の歯が 1回転すると、黒と青の歯は 5回転することになります。入力が 5倍になるってことでしょうか。python でやってみます。
5倍にするとなんかいけそうな数字に見えてきました。フラグ出ました!インストールした甲斐がありました!
$ python
Python 3.11.2 (main, May 2 2024, 11:59:08) [GCC 12.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> red = 39722847074734820757600524178581224432297292490103995897672826024691504153
>>> red
39722847074734820757600524178581224432297292490103995897672826024691504153
>>> blue = red * 5
>>> blue
198614235373674103788002620892906122161486462450519979488364130123457520765
>>> hex(red)
'0x167b7a4973dda7b247d70a7d3ddfd67adfe2a34a0a463cd77aa43d09a41419'
>>> hex(blue)
'0x7069636f4354467b67333472355f30665f6d3072335f30356535313034647d'
>>> hh = hex(blue)[2:]
>>> "".join([chr(int(hh[ii] + hh[ii+1], base=16)) for ii in range(0, len(hh), 2)])
'picoCTF{g34r5_0f_m0r3_05e5104d}'
これって、リバースエンジニアリングは関係あったんでしょうか(笑)
No way out(200ポイント)
200ポイントですが、Hard の問題です。Windows用のプログラムと MAC用のプログラムがダウンロードできるようです。Linux用はありません。
ダウンロードした ZIPファイルを解凍すると、いくつかのファイルを格納したフォルダが得られます。pico.exe というファイルがあるので、ダブルクリックで起動してみます。え、結構本格的なゲームっぽい感じです。どうやってリバースエンジニアリングするんでしょうか、Ghidra か、Windows の場合は、C# なら、ILSpy というツールがありますが、それ以外は知りません。
メニューを開くと、WASD で移動、SHIFT が SPRINT(走る)、CTRL が CROUPH(しゃがむ)、SPACE がジャンプのようです。
ハシゴに近づくと、ハシゴに登れるか?とメッセージが出ました。まずは、ハシゴを上ることが目標です。ハシゴに真っすぐ向かって、W を押し続けてると、ハシゴを登ることが出来ました。次はどうしたらいいでしょうか。あ、最初はバールを持ってましたが、上がった後は、スコップ?みたいなものに変わりました。うっかり下に落ちてしまいましたが、落ちた後もスコップのままです。次はこれで何かをするようです。久しぶりの 3D のゲームで、ちょっと気持ち悪くなってきました。
次の日になりました。落ち着いて考えたら、まだうさみみハリケーンで確認してませんでした。解凍したフォルダに対して、YARAルールスキャンをやってみます。うーん、何も見つかりませんでした。仕方ないおんで、また、ゲームと向き合います。
ハシゴで上がったあと、周りの塀の上を歩くことが出来るようです。グルっと一周できます。あと、マウスホイールを回すと武器が変わります。2階に上がったから武器が変わったわけではありませんでした。今のところ、何か変化というか動きがあったのは、ハシゴだけです。2階に上がると、旗と近づけます。この旗がめちゃくちゃ怪しいですよね。しかし、旗には近づけません。
よく分からなくなったので、本題のリバースエンジニアリングについてやり方を考えていきます。ダウンロードした ZIPファイルを解凍したフォルダには多くのファイルが含まれている状態です。
とりあえず、起動する pico.exe というプログラムを ILSpy で開いてみました。「PE file does not contain any managed metadata.」と言われて、うまくいってなさそうです。
次に、Ghidra で、pico.exe を開いてみます。解析は順調に進んでいます。何となく、フォルダ内にある依存関係のあるファイルについても解析が進んでいるようです。結構な時間がかかって完了しました。entry は以下の感じです。__security_init_cookie()
はセキュリティ機構の何かだと思います。そうすると、FUN_1400010ec()
だけになります。
FUN_1400010ec()
の真ん中より少し下に、UnityMain という関数が見えます。いくつか関数名が見えてるものもあります。おそらく、外部関数なんだと思います。ちらちらと下の層に潜っていったりはりましたが、GUIプログラムの見方が分からず、規模も大きそうで厳しい感じです。
void entry(void)
{
__security_init_cookie();
FUN_1400010ec();
return;
}
ulonglong FUN_1400010ec(void)
{
code *pcVar1;
bool bVar2;
char cVar3;
undefined uVar4;
undefined2 uVar5;
int iVar6;
uint uVar7;
code **ppcVar8;
longlong *plVar9;
undefined8 uVar10;
ulonglong uVar11;
ulonglong unaff_RBX;
cVar3 = __scrt_initialize_crt(1);
if (cVar3 == '\0') {
__scrt_fastfail(7);
}
else {
bVar2 = false;
uVar4 = __scrt_acquire_startup_lock();
unaff_RBX = CONCAT71((int7)(unaff_RBX >> 8),uVar4);
if (DAT_140015a18 != 1) {
if (DAT_140015a18 == 0) {
DAT_140015a18 = 1;
iVar6 = _initterm_e(&DAT_14000c260,&DAT_14000c290);
if (iVar6 != 0) {
return 0xff;
}
_initterm(&DAT_14000c248,&DAT_14000c258);
DAT_140015a18 = 2;
}
else {
bVar2 = true;
}
__scrt_release_startup_lock(uVar4);
ppcVar8 = (code **)FUN_1400015d4();
if ((*ppcVar8 != (code *)0x0) &&
(cVar3 = __scrt_is_nonwritable_in_current_image(ppcVar8), cVar3 != '\0')) {
(**ppcVar8)(0,2);
}
plVar9 = (longlong *)FUN_1400015dc();
if ((*plVar9 != 0) && (cVar3 = __scrt_is_nonwritable_in_current_image(plVar9), cVar3 != '\0'))
{
_register_thread_local_exe_atexit_callback(*plVar9);
}
uVar5 = __scrt_get_show_window_mode();
uVar10 = _get_wide_winmain_command_line();
uVar7 = UnityMain(&IMAGE_DOS_HEADER_140000000,0,uVar10,uVar5);
unaff_RBX = (ulonglong)uVar7;
cVar3 = __scrt_is_managed_app();
if (cVar3 != '\0') {
if (!bVar2) {
_cexit();
}
__scrt_uninitialize_crt(1,0);
return unaff_RBX;
}
goto LAB_14000124d;
}
}
__scrt_fastfail(7);
LAB_14000124d:
FUN_140003520(unaff_RBX & 0xffffffff);
FUN_1400034d8(unaff_RBX & 0xffffffff);
pcVar1 = (code *)swi(3);
uVar11 = (*pcVar1)();
return uVar11;
}
うーん、ギブアップします。writeup を見てみます。Unity で作られていて、C# を解析してゲームの挙動を変えることで解けるらしいです。なるほど、Ghidra で見えるところは低いレイヤのところで、ちょっと見当違いでした。
dnSpy というツールでデコンパイルできるそうです。解析する対象も pico.exe ではなく、pico_Data/Managed/Assenbly-CSharp.dll は C# としてデコンパイル出来るようです。試しに、ILSpy で、このファイルをデコンパイルすると、見れました。Unity のディレクトリ構成を知る必要があったようです。普通では行けないはずの白のフラグのところに行けるように出来るようです。
調べてみると、ILSpy はデコンパイルは出来るけど、ソースの変更は出来ないようです。実際にやってみましたが、確かに変更は出来なかったです。dnSpy はソースの変更や、デバッグする機能などがあり、かなり高機能なようです。dnSpy を使ってみます。
dnSpy は、以下からダウンロードできます。右側にある Releases のリンクをクリックして、v6.1.8 の dnSpy-net-win64.zip をダウンロードします。
github.com
インストールは不要で、解凍したフォルダにある dnSpy.exe をダブルクリックして起動します。File → Open... をクリックして、pico_Data/Managed/Assenbly-CSharp.dll を開きます。EvolveGames に各クラスが見えるので、PlayerController を開きます。なんとなく、Updateメソッドが、何らかのコールバック関数(イベントハンドラの方が適切?)です。
先頭に重力の計算をしている感じのところがあるので、ここの - this.gravity * Time.deltaTime
を削除してみます。そのままでは編集できないので、右クリックして Edit Method (C#)... をクリックすると編集できるようになります。編集が終わったら、Compile を押すと、元の画面に戻って、編集したコートが反映されています。最後に、File → Save All でコードを保存して完了です。
- this.gravity * Time.deltaTime
を削除すると、クリアすることは可能なのですが、以下のように、上昇し続けてしまいます。
なので、削除するのではなく、- this.gravity * Time.deltaTime / 20
にして、大ジャンプできるようにした方が、フラグに到達できやすいと思います。
picoCTF{WELCOME_TO_UNITY!!}
でした。
Ready Gladiator 1(200ポイント)
Medium の問題です。インスタンスを起動すると、1つのファイル(imp.red)がダウンロードできます。
Core Wars というものらしいです。コンピュータとの対戦で、アセンブラのような書き方で、自分の動きを決めて、相手の命令を潰せれば勝ち、逆に潰されたら負けです。メモリ内を順番に実行していきます。メモリには上限が設定されていて、それを超えると先頭に戻ります。
相手のプログラムを潰すためのプログラムを書く必要があります。ダウンロードしたファイルは、そのプログラムのサンプルみたいなもので、これを改良していき、コンピュータに勝つことが目的のようです。
しかし、Core Wars には、いくつかバージョンがあるようで、ルール、定義がはっきりしませんし、これを一生懸命頑張って得られるものは無さそうなので、スキップします。
Virtual Machine 1(300ポイント)
Hard の問題です。Virtual Machine 0(歯車の問題)の続きのようです。インスタンスを起動すると、1つのファイル(Virtual-Machine-1.zip)がダウンロードできます。
ダウンロードしたファイルを解凍すると、VirtualMachine1.dae が得られます。Virtual Machine 0 と同様に、Blender で開いてみます。なかなかにハードな感じです。
サーバに接続してみます。なるほど、固定の入力ではなく、その場で答える必要があるようですね。Python で計算させる必要がありそうです。
$ nc saturn.picoctf.net 52483
If the input to the machine is 12136, what is the output?
Answer>
地道にやっていくしかない、と想定して進めていきます。
最初の赤の車軸の歯車は、24個の凹凸があります。左側の歯車は 8個であり、3倍回転するということになります。その後ろの歯車は 16個で、その左の 2つの歯車は両方とも 8個です。つまり、3×2 で、6倍回転します。棒をつたって、しばらくは同じ数の歯車が接続されているので、変化なしです。
それが棒をつたって、24個に繋がっていて、12個の歯車を回します。よって、12倍です。それに繋がっているのが 16個で、右隣りの 8個の歯車を動かすので、2倍で、計24倍です。次も 16個で 8個を回すので、計48倍です。忘れそうなので、キャプチャにメモしていきました。
ということで、最終的に 9216倍となりました。早速、インスタンスを起動して、9216倍した値を答えましたが、不正解でした。
$ nc saturn.picoctf.net 61658
If the input to the machine is 6941, what is the output?
Answer> 63968256
63968256
That's not correct.
You have to wait 360.0 seconds before trying again.
歯車が左右に分岐してる場合、何となく数えやすそうな方を選んでいましたが、これはうすうす感じてましたが、間違いがありそうです。最後の 2倍、2倍、2倍のところは、右側を進みましたが、左側を進むと 6倍になります。左右で倍数が違うところを吸収するとかしないとおかしくなります。
以下のように、左右の回転数を吸収する機能を持つ歯車はディファレンシャルギアと言うそうです。左右の歯車の回転数の差が真ん中のギアが回ることで差を吸収します。
というわけで、見直します。最初の 6倍は合ってますが、次は左を通る必要があります。左を通ると、18倍で、計108倍です。
次は、20倍なので、計2160倍です。あとは、最後の6倍で、計12960倍です。
では、インスタンスを起動してやってみます。うーん、ダメです。
$ nc saturn.picoctf.net 61054
If the input to the machine is 23034, what is the output?
Answer> 298520640
298520640
That's not correct.
You have to wait 360.0 seconds before trying again.
合ってると思うんですよねぇ、でも、違うってことは、何か理解できてない部分があるようです。
ここでギブアップです。writeup を探しましたが、解説がされてるものが、あまりありませんでした。2つのギア比の平均が出力になると解説されているところはあったのですが、今回のディファレンシャルギアは、片方のギアに出力のギアが接続されているので、その場合でも、そうなるのかが、ちょっと分かりませんでした。また、詳しい解説が見つかったら追記しようと思います。
Ready Gladiator 2(400ポイント)
Medium の問題です。Ready Gladiator 1 と同様にスキップします。
おわりに
今回は、picoCTF の picoCTF 2023 のうち、Reverse Engineering の全9問に挑戦しました。今回の問題は今までやってきたリバースエンジニアリングと少し雰囲気の違う問題が多かったです。まだまだだな、と思いました。
最後になりましたが、エンジニアグループのランキングに参加中です。
気楽にポチッとよろしくお願いいたします🙇
今回は以上です!
最後までお読みいただき、ありがとうございました。