0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

FWE CTF - NativeKotlian Writeup (フロー解析)

0
Last updated at Posted at 2026-05-09

はじめに

2025/8に開催された、Full Weak Engineer CTFの問題のWriteupです。

以前にもWriteupを作成したのですが、あまりにもぐしゃぐしゃだったため、再度書くことにしました💦
ついでに、Upsolveも兼ねての記事です!

一つの記事にするには、あまりにも文やメディアの量が多すぎたので、4つの記事にわけて公開しています ↓

問題概要 (公式Repoから引用)

ジャンル 問題名 作問者 タグ 最終スコア Solve数
Rev NativeKotlian こーふ Hard 497 2

内容としては、Kotlin/Nativeでビルドされた、ネイティブで動くKotlinアプリケーションを解析する問題です。

添付ファイルには、実行可能ELF形式 (x86-64) のファイルが含まれています。
コマンドラインから与えられたファイルを暗号化して保存します。

babar@mypc:/mnt/c/Ctf/FWE-CTF/NativeKotlian$ ./chall
Usage: chall <input>
babar@mypc:/mnt/c/Ctf/FWE-CTF/NativeKotlian$ ./chall data.txt

image.png

そして、この問題は、暗号化されたファイルのみが与えられ、それを復号化する問題です。

image.png

フロー解析

エントリーポイント付近をしばらく漁ると、kfun__mainから始まるメイン関数を見つけることができます。

しかし、あまりにも大規模な関数となってしまっているので、最初にどのようなフローでこのアプリケーションが処理を行うかの大筋の予測を行います。

image.png
関数がデカすぎて、もはや見えない...

引数チェック・使用方法出力

最初の方に出てくる、if (*(args + 8) != 1)で始まる処理を見ます。

image.png

args + 8 には、int32で引数の数が入っています (0x7ffff6bbe028がargsへのアドレス)
つまり、./chall 引数という使用方法を要求していることがわかります。

image.png

そして、もし引数の数が1個ではない場合は、/proc/self/cmdlineからBufferedSourceを作成し、読み取っています。

image.png

そして読み取ったものを加工し、最後にUsageを出力します。

image.png

暗号化キー生成

続いて、暗号化キー生成のフローを確認します。

上の方ではすでに、ファイルパスを引数から取得する処理などをすでに行っていますが、一旦、キー生成の関数のみに着目して進めます。

image.png

以下の部分が関数呼び出しになっているように見えます。

 v246[0] = (**(*(*(*genKey_lambda & 0xFFFFFFFFFFFFFFFCLL) + 64LL)
              + 16LL * (*(*(*genKey_lambda & 0xFFFFFFFFFFFFFFFCLL) + 60LL) & 0x79)
              + 8))(
              genKey_lambda,
              v246);

デバッガーで該当箇所にブレークポイントを置き、Step Instructionをすると、kfun__genKey_lambda_3_lambda_2_FUNCTION_REFERENCE_4_invoke_internalへcallしていることがわかります。

────────────────────────────────────────────────────────[ LAST SIGNAL ]────────────────────────────────────────────────────────
Breakpoint hit at 0x25a8f0
...
─────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────────────────────────────
 ► 0x25a8f0 <kfun:#main(kotlin.Array<kotlin.String>){}+480>    call   qword ptr [rcx]           
...
pwndbg> si
0x000000000025d110 in kfun:$genKey$lambda$3$lambda$2$FUNCTION_REFERENCE$4.invoke#internal ()
─────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────────────────────────────
 ► 0x25d110 <kfun:$genKey$lambda$3$lambda$2$FUNCTION_REFERENCE$4.invoke#internal>       push   rbp
   0x25d111 <kfun:$genKey$lambda$3$lambda$2$FUNCTION_REFERENCE$4.invoke#internal+1>     push   r15
   0x25d113 <kfun:$genKey$lambda$3$lambda$2$FUNCTION_REFERENCE$4.invoke#internal+3>     push   r14
   0x25d115 <kfun:$genKey$lambda$3$lambda$2$FUNCTION_REFERENCE$4.invoke#internal+5>     push   r13
   0x25d117 <kfun:$genKey$lambda$3$lambda$2$FUNCTION_REFERENCE$4.invoke#internal+7>     push   r12
...
pwndbg>

見つかった関数の最後のあたりを見ると、何かのPairを戻り値としているようです。

image.png

v54, v56をxrefし、代入されている周辺を見てみると、最初の方で、pair相当(128bit)の変数に空の文字列と、配列を代入していることがわかります。

このことから、genKeyの戻り値が Pair<String, List<?>> のようなものであることが見えます。

image.png

今回の目的はフローを知ることなので、厳密な解析は一旦後回しにします。

出力先パス構築

先ほど一旦スルーした、パス取得後の処理を追います。
パスを引数から取得し、String化していることが流れでわかります。

image.png

その直後に、String化したものをsplitしている箇所があります。

image.png

しかし、何を区切り (delimiter) としているかがわかりにくいため、splitの箇所でブレークポイントを設定し、動的解析を行います。

pwndbg> context
...
*RSI  0x289c70 (__unnamed_62) —▸ 0x27faf1 (kclass:kotlin[String]+1) ◂— 0x27fa
...
─────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────────────────────────────
 ► 0x25aa3d <kfun:#main(kotlin.Array<kotlin.String>){}+813>     call   kfun:kotlin.text.split#internal <kfun:kotlin.text.split#internal>
        rdi: 0x7ffff6b7e438 —▸ 0x27faf0 (kclass:kotlin[String]) ◂— 0x27faf0 (kclass:kotlin[String])
        rsi: 0x289c70 (__unnamed_62) —▸ 0x27faf1 (kclass:kotlin[String]+1) ◂— 0x27fa
        rdx: 2
        rcx: 0x7fffffffd960 ◂— 0
...
pwndbg> hexdump 0x289c70
+0000 0x289c70  f1 fa 27 00 00 00 00 00  01 00 00 00 00 00 00 00  │..'.....│........│
+0010 0x289c80  2e 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  │........│........│
+0020 0x289c90  f1 fa 27 00 00 00 00 00  0f 00 00 00 00 00 00 00  │..'.....│........│
+0030 0x289ca0  41 00 72 00 72 00 61 00  79 00 20 00 69 00 73 00  │A.r.r.a.│y...i.s.│
pwndbg>

出力されたアドレスをIDAで見てみると、.単体の文字であることがわかりました。

image.png

splitをしたあと、配列から特定の要素を抽出し、そこから新しいStringのインスタンスを作成している処理が見つかります。

image.png

同じように、string_from_array = var_140;にブレークポイントを置き、何に該当するStringなのかを特定します。

そうすると、拡張子を格納している文字列であることがわかります。

...
*R12  0x7ffff6bbe688 —▸ 0x27faf0 (kclass:kotlin[String]) ◂— 0x27faf0 (kclass:kotlin[String])
...
─────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────────────────────────────
 ► 0x25b558 <kfun:#main(kotlin.Array<kotlin.String>){}+3656>    mov    qword ptr [rsp + 0x208], r12     [0x7fffffffd968] <= 0x7ffff6bbe688 —▸ 0x27faf0 (kclass:kotlin[String]) ◂— 0x27faf0
 ...
pwndbg> hexdump 0x7ffff6bbe688
+0000 0x7ffff6bbe688  f0 fa 27 00 00 00 00 00  03 00 00 00 00 00 00 00  │..'.....│........│
+0010 0x7ffff6bbe698  74 00 78 00 74 00 00 00  00 00 00 00 00 00 00 00  │t.x.t...│........│
+0020 0x7ffff6bbe6a8  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  │........│........│
+0030 0x7ffff6bbe6b8  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  │........│........│
pwndbg>

もう少し整備すると、こんな感じになりそうです。

image.png

そうすると、これまでの変数を利用し、出力するファイル名をビルドする処理にたどり着くことができます。

 file_name_array_2 = kotlin::alloc::CustomAllocator::Allocate(*(*v8 + 224LL), 6u);
  *file_name_array_2 = 0;
  *(file_name_array_2 + 8) = &kclass_kotlin_CharArray;
...
  memcpy((file_name_array_1 + 2LL * v220 + 16), key_string_1 + 16, 2 * key_string_len);// 生成したkey文字列をコピー
...
  *(file_name_array_1 + 2LL * v220 + 16) = '.'; // .を追加
...
  memcpy((file_name_array_1 + 2LL * v220 + 16), extension_string + 2, 2 * v138);// 拡張子をコピー
...
  *(file_name_array_1 + 2LL * v220 + 16) = *&encryted_str;// .encryptedを末尾に追加
  *(v141 + 2 * v140 + 32) = 100;

実際に中身を見てみても、出力されるファイル名が構築されていることがわかります。

pwndbg> hexdump 0x7ffff69be038
+0000 0x7ffff69be038  36 00 77 00 58 00 6a 00  4d 00 46 00 51 00 6d 00  │6.w.X.j.│M.F.Q.m.│
+0010 0x7ffff69be048  2e 00 74 00 78 00 74 00  2e 00 65 00 6e 00 63 00  │..t.x.t.│..e.n.c.│
+0020 0x7ffff69be058  72 00 79 00 70 00 74 00  65 00 64 00 00 00 00 00  │r.y.p.t.│e.d.....│
+0030 0x7ffff69be068  01 00 00 00 00 00 00 00  f0 fa 27 00 00 00 00 00  │........│..'.....│
pwndbg>

暗号化前準備

ここから、いよいよ暗号化周辺の処理になります。
最初に元ファイルからの読み取り準備及び、出力先ファイルへのSink作成を行っています。

image.png

その後ループへ入り、ファイルの中身を読み取った後に、更に8バイトずつ処理していることがわかります。

image.png

また、読んだバイト数が8バイト未満の場合、読んだデータを8バイトにパディングしています。
なおパディングをする際は、読んだバイト数をそのまま入れる値としているようです。

image.png

image.png

var_278について

file_content_bufferとは別にvar_278 変数にてパディングを設定しており、 Xrefをみても代入箇所が見当たらず、何の変数なのかが不明瞭でした。

image.png

そこで、該当のスタックアドレスにwatchpointを設定し、どこから書き込んでいるかを調査します。

関数のはじめ、つまりスタック確保後にブレークポイントを置きます。

pwndbg> b *0x25A721
Breakpoint 10 at 0x25a721

そして、var_278が格納されている、$rsp + 0xd0にwatchpointを置くために、rspのアドレスを取得します。


────────────────────────────────────────────────────────────────────────[ LAST SIGNAL ]─────────────────────────────────────────────────────────────────────────
Breakpoint hit at 0x25a721
─────────────────────────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]─────────────────────────────────────────────────────
...
*RSP  0x7fffffffd7f0 ◂— 0
...
──────────────────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────────────────────────────────────────────
   0x25a713 <kfun:#main(kotlin.Array<kotlin.String>){}+3>     push   r14
   0x25a715 <kfun:#main(kotlin.Array<kotlin.String>){}+5>     push   r13
   0x25a717 <kfun:#main(kotlin.Array<kotlin.String>){}+7>     push   r12
   0x25a719 <kfun:#main(kotlin.Array<kotlin.String>){}+9>     push   rbx
   0x25a71a <kfun:#main(kotlin.Array<kotlin.String>){}+10>    sub    rsp, 0x318
 ► 0x25a721 <kfun:#main(kotlin.Array<kotlin.String>){}+17>    mov    rbx, rdi               RBX => 0x7ffff6bbe028 —▸ 0x27e8a0 (kclass:kotlin[Array]) ◂— 0x27e8a0

そのままwatchpointを設置します。

pwndbg> watch *0x7FFFFFFFD8C0
Hardware watchpoint 17: *0x7FFFFFFFD8C0

そうすると、アクセスしている命令でブレークします。

pwndbg> c
Continuing.
Downloading source file ./string/../sysdeps/x86_64/multiarch/memmove-vec-unaligned-erms.S
Download failed: Invalid argument.  Continuing without source file ./string/../sysdeps/x86_64/multiarch/memmove-vec-unaligned-erms.S.

Thread 1 "chall" hit Hardware watchpoint 16: *(0x7fffffffd7f0 + 0xD0)

Old value = 0
New value = 2181376

Thread 1 "chall" hit Hardware watchpoint 17: *0x7FFFFFFFD8C0

Old value = 0
New value = 2181376
__memcpy_avx_unaligned_erms () at ../sysdeps/x86_64/multiarch/memmove-vec-unaligned-erms.S:323
⚠️ warning: 323 ../sysdeps/x86_64/multiarch/memmove-vec-unaligned-erms.S: No such file or directory
Downloading source file ./string/../sysdeps/x86_64/multiarch/memmove-vec-unaligned-erms.S
Download failed: Invalid argument.  Continuing without source file ./string/../sysdeps/x86_64/multiarch/memmove-vec-unaligned-erms.S.
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
─────────────────────────────────────────────────────────────────────────────────────────────────[ LAST SIGNAL ]─────────────────────────────────────────────────────────────────────────────────────────────────
Breakpoint hit at 0x7ffff7d88ad2
─────────────────────────────────────────────────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]──────────────────────────────────────────────────────────────────────────────
*RAX  0x7fffffffd8c0 —▸ 0x214900 ◂— 'd::istream'
*RBX  0x7ffff7e3c5e0 —▸ 0x2c0c20 ◂— 0
*RCX  0x7ffff6b66e48 ◂— 0
*RDX  3
*RDI  0x7fffffffd8c0 —▸ 0x214900 ◂— 'd::istream'
*RSI  0x2149
*R8   0x800000000
*R9   0x210
*R10  0x7ffff7c11c68 ◂— 0x11002200002fe2
*R11  0x7ffff7c8e720 (feof) ◂— endbr64
*R12  0
*R13  0x7ffff6abe3a8 —▸ 0x2839e0 (kclass:okio[Segment]) ◂— 0x2839e0 (kclass:okio[Segment])
*R14  3
*R15  0x7fffffffd8b0 —▸ 0x27e9c3 (kclass:kotlin[ByteArray]+3) ◂— 0
*RBP  3
*RSP  0x7fffffffd738 —▸ 0x254c0f (kfun:okio.Buffer#read(kotlin.ByteArray;kotlin.Int;kotlin.Int){}kotlin.Int+287) ◂— mov qword ptr [rsp + 0x20], r15
*RIP  0x7ffff7d88ad2 (__memmove_avx_unaligned_erms+82) ◂— mov byte ptr [rdi], cl
──────────────────────────────────────────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]───────────────────────────────────────────────────────────────────────────────────────
 ► 0x7ffff7d88ad2 <__memmove_avx_unaligned_erms+82>                                                  mov    byte ptr [rdi], cl     [0x7fffffffd8c0] <= 0x48
   0x7ffff7d88ad4 <__memmove_avx_unaligned_erms+84>                                                  ret                                <kfun:okio.Buffer#read(kotlin.ByteArray;kotlin.Int;kotlin.Int){}kotlin.Int+287>
   0x254c0f       <kfun:okio.Buffer#read(kotlin.ByteArray;kotlin.Int;kotlin.Int){}kotlin.Int+287>    mov    qword ptr [rsp + 0x20], r15      [0x7fffffffd760] <= 0x7fffffffd8b0 —▸ 0x27e9c3 (kclass:kotlin[ByteArray]+3) ◂— 0
   0x254c14       <kfun:okio.Buffer#read(kotlin.ByteArray;kotlin.Int;kotlin.Int){}kotlin.Int+292>    add    dword ptr [r13 + 0x20], r14d     [0x7ffff6abe3c8] <= 3 (0 + 3)
   0x254c18       <kfun:okio.Buffer#read(kotlin.ByteArray;kotlin.Int;kotlin.Int){}kotlin.Int+296>    movsxd rax, r14d                        RAX => 3
   0x254c1b       <kfun:okio.Buffer#read(kotlin.ByteArray;kotlin.Int;kotlin.Int){}kotlin.Int+299>    mov    rcx, qword ptr [rsp]             RCX, [0x7fffffffd740] => 0x7ffff6bbe848 —▸ 0x284130 (kclass:okio[Buffer]) ◂— 0x284130 /* '0A(' */
   0x254c1f       <kfun:okio.Buffer#read(kotlin.ByteArray;kotlin.Int;kotlin.Int){}kotlin.Int+303>    sub    qword ptr [rcx + 0x10], rax      [0x7ffff6bbe858] <= 0 (3 - 3)
   0x254c23       <kfun:okio.Buffer#read(kotlin.ByteArray;kotlin.Int;kotlin.Int){}kotlin.Int+307>    mov    eax, dword ptr [r13 + 0x20]      EAX, [0x7ffff6abe3c8] => 3
   0x254c27       <kfun:okio.Buffer#read(kotlin.ByteArray;kotlin.Int;kotlin.Int){}kotlin.Int+311>    cmp    eax, dword ptr [r13 + 0x24]      3 - 3     EFLAGS => 0x246 [ cf PF af ZF sf IF df of ac ]
   0x254c2b       <kfun:okio.Buffer#read(kotlin.ByteArray;kotlin.Int;kotlin.Int){}kotlin.Int+315>  ✘ jne    kfun:okio.Buffer#read(kotlin.ByteArray;kotlin.Int;kotlin.Int){}kotlin.Int+566 <kfun:okio.Buffer#read(kotlin.ByteArray;kotlin.Int;kotlin.Int){}kotlin.Int+566>

   0x254c31       <kfun:okio.Buffer#read(kotlin.ByteArray;kotlin.Int;kotlin.Int){}kotlin.Int+321>    xorps  xmm0, xmm0
────────────────────────────────────────────────────────────────────────────────────────────────────[ STACK ]────────────────────────────────────────────────────────────────────────────────────────────────────
00:0000│ rsp 0x7fffffffd738 —▸ 0x254c0f (kfun:okio.Buffer#read(kotlin.ByteArray;kotlin.Int;kotlin.Int){}kotlin.Int+287) ◂— mov qword ptr [rsp + 0x20], r15
01:0008│     0x7fffffffd740 —▸ 0x7ffff6bbe848 —▸ 0x284130 (kclass:okio[Buffer]) ◂— 0x284130 /* '0A(' */
02:0010│     0x7fffffffd748 —▸ 0x7fffffffd8f0 —▸ 0x7fffffffdb40 —▸ 0x7fffffffdb60 ◂— 0
03:0018│     0x7fffffffd750 ◂— 0x600000000
04:0020│     0x7fffffffd758 —▸ 0x7ffff6abe3a8 —▸ 0x2839e0 (kclass:okio[Segment]) ◂— 0x2839e0 (kclass:okio[Segment])
05:0028│     0x7fffffffd760 ◂— 0
... ↓        2 skipped
──────────────────────────────────────────────────────────────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────────────────────────────────────────────────────────────
 ► 0   0x7ffff7d88ad2 __memmove_avx_unaligned_erms+82
   1         0x254c0f kfun:okio.Buffer#read(kotlin.ByteArray;kotlin.Int;kotlin.Int){}kotlin.Int+287
   2         0x25c315 kfun:#main(kotlin.Array<kotlin.String>){}+7173
   3         0x26c141 Init_and_run_start+497
   4         0x26c23b main+11
   5   0x7ffff7c2a1ca __libc_start_call_main+122
   6   0x7ffff7c2a28b __libc_start_main+139
   7         0x22aa89 _start+41
...

Step Instructionをしhexdumpをしてみると、実際に配列へのアドレスが書き込まれています。

pwndbg> si

Thread 1 "chall" hit Hardware watchpoint 16: *(0x7fffffffd7f0 + 0xD0)

Old value = 2181376
New value = 2181448

Thread 1 "chall" hit Hardware watchpoint 17: *0x7FFFFFFFD8C0

...
pwndbg> hexdump 0x7FFFFFFFD8C0
+0000 0x7fffffffd8c0  48 49 21 00 00 00 00 00  30 e1 af f6 ff 7f 00 00  │HI!.....│0.......│
+0010 0x7fffffffd8d0  70 ff ff ff ff ff ff ff  d0 6e 2a 00 00 00 00 00  │p.......│.n*.....│
+0020 0x7fffffffd8e0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  │........│........│
+0030 0x7fffffffd8f0  40 db ff ff ff 7f 00 00  00 00 00 00 43 00 00 00  │@.......│....C...│
pwndbg>

ここでBack Traceを見てみると、kfun:#main関数の途中である0x25c315が、このアクセスした処理の呼び出し元のようです。(Return先)

 ► 0   0x7ffff7d88ad2 __memmove_avx_unaligned_erms+82
   1         0x254c0f kfun:okio.Buffer#read(kotlin.ByteArray;kotlin.Int;kotlin.Int){}kotlin.Int+287
   2         0x25c315 kfun:#main(kotlin.Array<kotlin.String>){}+7173
   3         0x26c141 Init_and_run_start+497
   4         0x26c23b main+11
   5   0x7ffff7c2a1ca __libc_start_call_main+122
   6   0x7ffff7c2a28b __libc_start_main+139
   7         0x22aa89 _start+41

ここを見てみると、ファイル読み出しの処理での書き込みであることが判明します。
しかし直接 var_278 を渡しているわけではないようです。

image.png

ここでスタックの構造を見てみると、file_content_buffer, eight_contast_val, var_278が連続して並んでいることがわかります。

image.png

また、初期化時も並んでいることがわかります。

image.png

ここからこれらの変数はそれぞれ独立したものではなく、file_content_bufferから始まる一つの構造体であることが想像できます。

それぞれ、

  • 配列の型データ
  • 配列の長さ
  • 配列のデータ

という形になっていると考えることができます。

しかし、このままだと依然として分かりづらいので、IDAの機能であるLocal Typesを用います。
Array という名前の構造体を作成し、デコンパイラにも適用します。

image.png

これにより、データの流れがわかりやすくなりました。(変数名も合わせて変えています)

image.png

暗号化

パディングの処理が完了した後は、encrypt 関数を2回呼び出しファイルから読み出したデータを暗号化しています。
その後、暗号化した8バイト分のファイルを実際に書き込んでいます。

image.png

その後、8回分ループをして何かをしています。

image.png

キー配列関連の関数呼び出しがあるので、キー更新の処理じゃないかと予想をし、詳しい解析は後に回します。

image.png

その後、読んだバイト数が8バイトではない(8未満)の場合、ファイルを閉じて暗号化のプロセスが終了します。

image.png

まとめ

今までのフローをチャートにし、視覚化するとこのような感じになります。

ここまでで、処理全体は「キー生成」「出力ファイル名構築」「8バイト単位の暗号化処理」「キー更新」に分けられることが分かりました。
次の記事では、このうち暗号鍵を生成している genKey 周辺を詳しく解析し、ファイル名から初期鍵を復元する方法を見ていきます。

FWE CTF - NativeKotlian Writeup (暗号キー生成関数)

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?