GDB と Iaito/Radare2 を使ったリバースエンジニアリングの例

2026-05-27T23:56:34+08:00

ここ数週間、私はリバースエンジニアリングを学んでおり、最初の体験を共有するためにブログ記事を書きました。最近はもう少し踏み込み、静的解析と動的解析を組み合わせて CTF 問題を解いていますが、これはリバースエンジニアリングをもう少し詳しく説明するのにとても良い例だと感じました。

以前は GDB を使った動的解析について記事を書くつもりでした。しかし、この問題も GDB を説明するのに非常に良い例だと気づきました。そして、この問題を解くために、もう一つのツールである iaito も使います。これは radare2 のグラフィカルフロントエンドです。

問題

cIMG は、端末上で異なる色の文字から成る画像を表現するための、一種の画像フォーマットです。これは次の要素で構成されています。

  1. マジックナンバー、つまり cIMG
  2. バージョン番号
  3. 幅と高さ
  4. データ。各ピクセルは R, G, B と ASCII 文字を含みます

この問題は、ある実行ファイルに cIMG を渡し、cIMG が特定の条件を満たしている場合にだけ flag を得る、というものです。

ソースコード

この問題のsourceはこちらから入手できます。このソースコードは BSD 2-Clause ライセンスのようです。

GCC でコンパイルします。

$ gcc -O3 challenge.c -o challenge

Python を開き、例として cIMG ファイルを作成します。

$ python3
Python 3.13.5 (Jun 25 2025, 18:55:22) [GCC 14.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> with open("example.cimg", "wb") as file:
...     file.write(b"cIMG\x02\0\x0a\x0a")
...     file.write(b"\x50"*(10*10*4))
...     
8
400

問題のバイナリを使って、この例の cIMG ファイルを開きます。

user@learnaarch64asm:~$ ./challenge ./example.cimg 
PPPPPPPPPP
PPPPPPPPPP
PPPPPPPPPP
PPPPPPPPPP
PPPPPPPPPP
PPPPPPPPPP
PPPPPPPPPP
PPPPPPPPPP
PPPPPPPPPP
PPPPPPPPPP

このファイルは表示はしますが、flag はくれません。

静的解析

flag を 与える cIMG ファイルを作る必要があります。まずは静的解析から始めましょう。

iaito でファイルを開きます。aaa 解析を実行し、main 関数のグラフを開きます。次のようになります。

the graph of the main function

0x00000cac でファイルヘッダの読み込み、0x00000cc4 でマジックナンバーのチェック、0x00000cd0 でバージョンチェック、0x00000ce0 で幅と高さの読み込みが確認できます。その後、malloc で画像データ用のメモリを確保し、そのメモリにデータを読み込みます。

マジックナンバーは cIMG で、16 進数では 63 49 4d 47 です。バージョンは 32 ビットの little-endian 整数であり、幅と高さはいずれも 8 ビット整数です。

flag 条件

関数の末尾を見てみましょう。明らかに sym.win が flag を与える関数です。これは 0x00000e8c で呼び出されており、この実行は 0x00000e58 のチェックの後に行われます。このチェックでは w19 が 0 であってはなりません。

制御フローの改ざん

GDB を起動しましょう。

$ gdb --args ./challenge ./example.cimg 
GNU gdb (Debian 16.3-1) 16.3
Copyright (C) 2024 Free Software Foundation, Inc.
License GPLv3+ or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "aarch64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./challenge...
(No debugging symbols found in ./challenge)

main 関数にブレークポイントを置いて、プログラムを起動します。

(gdb) break *main
Breakpoint 1 at 0xc44
(gdb) run
Starting program: /home/user/challenge ./example.cimg
[Thread debugging using libthread_db library "/lib/aarch64-linux-gnu/libthread_db.so.1].

Breakpoint 1, 0x0000aaaaaaaa0c44 in main ()

GDB のデータアドレスには 0xaaaaaaaa0000 のオフセットがあることが分かります。

flag 条件のチェックの直前にブレークポイントを置いて、続行します。

(gdb) break *0x0000aaaaaaaa0e58
Breakpoint 2 at 0xaaaaaaaa0e58
(gdb) continue
Continuing.
PPPPPPPPPP
PPPPPPPPPP
PPPPPPPPPP
PPPPPPPPPP
PPPPPPPPPP
PPPPPPPPPP
PPPPPPPPPP
PPPPPPPPPP
PPPPPPPPPP
PPPPPPPPPP

Breakpoint 2, 0x0000aaaaaaaa0e58 in main ()

レジスタ w19 の内容を変更して続行します。

(gdb) set $w19 = 1
(gdb) continue

すると実行ファイルは flag を与えてくれます。これで終わりです。よい一日を :-p

冗談です。もし本当にこのように問題を終わらせてしまったら、リバースエンジニアリングの楽しさが失われてしまいます。さらに深く見ていきましょう。

データチェック

iaito に戻ります。グラフ内で w19 をクリックします。すると、そのレジスタがグラフ中のすべての命令で強調表示されます。手がかりを探すために、グラフを巻き戻していきます。

グラフの中に sym.imp.memcmp の呼び出しがいくつか見つかります。

memcmp<string.h> で宣言される C 標準ライブラリの関数で、2 つのメモリブロックをバイト単位で比較します。

int memcmp(const void *ptr1, const void *ptr2, size_t n);

ptr1ptr2 はそれぞれ 2 つのブロックを指し、n は比較するバイト数を指定します。2 つのブロックが同一なら、関数は 0 を返し、それ以外なら 0 以外の値を返します。

memcmp の呼び出しは 4 回あります。各呼び出しの後には cmp w0, 0 があります。最初の呼び出しを除いては、cmp の後に cset w0, eqand w19, w19, w0 もあります。ここから、memcmp が差分を見つけると w0 が 0 になるため、w19 はクリアされると考えられます。したがって、4 組のメモリブロックを同じにする必要があると推測できます。

x0x1 が比較対象の 2 つのアドレスです。x2 にはサイズが入っており、mov x2, 0x18 から 24 バイトであることが分かります。

もう一度 GDB を開き、最初の memcmp の前のメモリを調べます。

(gdb) break *0x0000aaaaaaaa0e78
Breakpoint 1 at 0xaaaaaaaa0e78
(gdb) run
Starting program: /home/user/challenge ./example.cimg
[Thread debugging using libthread_db library "/lib/aarch64-linux-gnu/libthread_db.so.1].
PPPPPPPPPP
PPPPPPPPPP
PPPPPPPPPP
PPPPPPPPPP
PPPPPPPPPP
PPPPPPPPPP
PPPPPPPPPP
PPPPPPPPPP
PPPPPPPPPP
PPPPPPPPPP

Breakpoint 1, 0x0000aaaaaaaa0e78 in main ()
(gdb) info registers x0 x1
x0             0xaaaaaaac12a0      187649984565920
x1             0xaaaaaaac00c0      187649984561344
(gdb) x/24xb $x0
0xaaaaaaac12a0: 0x1b    0x5b    0x33    0x38    0x3b    0x32    0x3b    0x30
0xaaaaaaac12a8: 0x38    0x30    0x3b    0x30    0x38    0x30    0x3b    0x30
0xaaaaaaac12b0: 0x38    0x30    0x6d    0x50    0x1b    0x5b    0x30    0x6d
(gdb) x/24xb $x1
0xaaaaaaac00c0 <desired_output>:        0x1b    0x5b    0x33    0x38    0x3b   0x32     0x3b    0x32
0xaaaaaaac00c8 <desired_output+8>:      0x33    0x31    0x3b    0x30    0x31   0x37     0x3b    0x31
0xaaaaaaac00d0 <desired_output+16>:     0x33    0x30    0x6d    0x63    0x1b   0x5b     0x30    0x6d

随机的字节。我看不出它们是什么意思。

回到 iaito。第一块内存是从 x22 读取的,而 x22 又从 var_48h 这个地址中读取。afv 告诉我们它距离 sp 的偏移是 0x48

[0x00000e70]> afv
arg signed int argc @ x0
arg char ** s @ x1
var int64_t var_50h @ sp+0x0
var int64_t var_50h_2 @ sp+0x8
var int64_t var_10h @ sp+0x10
var int64_t var_10h_2 @ sp+0x18
var int64_t var_20h @ sp+0x20
var int64_t var_20h_2 @ sp+0x28
var void * buf @ sp+0x38
var int64_t var_3ch @ sp+0x3c
var int64_t var_3eh @ sp+0x3e
var int64_t var_3fh @ sp+0x3f
var int64_t var_40h @ sp+0x40
var int64_t var_48h @ sp+0x48

GDB を再起動し、main が呼び出された後で関数のプロローグを飛ばします。

(gdb) break *main
Breakpoint 1 at 0xaaaaaaaa0c44
(gdb) run
Starting program: /home/user/challenge ./example.cimg
[Thread debugging using libthread_db library "/lib/aarch64-linux-gnu/libthread_db.so.1].

Breakpoint 1, 0x0000aaaaaaaa0c44 in main ()
(gdb) si
0x0000aaaaaaaa0c48 in main ()
(gdb) si
0x0000aaaaaaaa0c4c in main ()

スタックポインタの値を確認し、ウォッチポイントを設定します。

(gdb) info registers sp
sp             0xfffffffff290      0xfffffffff290
(gdb) watch *(long long *)0xfffffffff2d8
Hardware watchpoint 2: *(long long *)0xfffffffff2d8

続行します。

```(gdb) continue Continuing.

Hardware watchpoint 2: (long long )0xfffffffff2d8

Old value = 281474840286680 New value = 0 0x0000aaaaaaaa0c5c in main ()

このアドレスを iaito で見つけます。`0x00000c58`、つまり `0x00000c5c`  1 つ前の命令で、`var_48h`  `str` によってクリアされていることが分かります。

さらに進めて、この変数がどこで設定されるのかを見ていきます。

(gdb) continue Continuing.

Hardware watchpoint 2: (long long )0xfffffffff2d8

Old value = 0 New value = 187649984565920 0x0000aaaaaaaa1358 in initialize_framebuffer ()

まだ見ていない新しい関数 `initialize_framebuffer` が現れました。

iaito でその関数へ移動します。

![the graph of the initialize\_framebuffer function](https://rebel1725.codeberg.page/images/pic_20260527_002.png)

`0x00001354` の命令で、`malloc` によって割り当てられたメモリアドレスが変数に保存されていることが分かります。ただし、それは `[x22, 0x10]` として表現されています。`x22` の内容を確認しましょう。

(gdb) info registers $x22 x22 0xfffffffff2c8 281474976707272

`0xfffffffff2c8 + 0x10 = 0xfffffffff2d8`。まさに `var_48h` のアドレスです。

さらに、`malloc` に渡されるサイズも興味深いです。これは、オフセット 6  7 の値を掛け合わせ、その結果に `w1`、つまり `0x18`(24)を掛け、最後に `x0`、つまり 1 を足して計算されています。

そのオフセットの値も見てみましょう。

(gdb) x/1xb 0xfffffffff2ce 0xfffffffff2ce: 0x0a (gdb) x/1xb 0xfffffffff2cf 0xfffffffff2cf: 0x0a

どちらも 10 です。私たちが指定した画像の幅と高さのようです。ハードウェアウォッチポイントを使って確認してみましょう。AArch64 ではアラインメントが必要なので、`0xfffffffff2cc` にハードウェアウォッチポイントを設定します。

(gdb) watch (unsigned int )0xfffffffff2cc Hardware watchpoint 1: (unsigned int )0xfffffffff2cc (gdb) run Starting program: /home/user/challenge ./example.cimg

GNU C Library によるトリガを飛ばします

```Hardware watchpoint 1: *(unsigned int *)0xfffffffff2cc

Old value = 0
New value = 65535
0x0000fffff7fccd08 in ?? () from /lib/ld-linux-aarch64.so.1
(gdb) continue
Continuing.

Hardware watchpoint 1: *(unsigned int *)0xfffffffff2cc

Old value = 65535
New value = 0
0x0000fffff7fd5668 in ?? () from /lib/ld-linux-aarch64.so.1
(gdb) continue
Continuing.
[Thread debugging using libthread_db library "/lib/aarch64-linux-gnu/libthread_db.so.1"]

Hardware watchpoint 1: *(unsigned int *)0xfffffffff2cc

Old value = 0
New value = 65535
0x0000fffff7fcc3a4 in ?? () from /lib/ld-linux-aarch64.so.1
(gdb) continue
Continuing.

ここで、メモリが 0x00000c54 でクリアされているのが分かります。

```(gdb) continue Hardware watchpoint 1: (unsigned int )0xfffffffff2cc

Old value = 65535 New value = 0 0x0000aaaaaaaa0c58 in main ()

`stp xzr, xzr, [buf]` が見えます。上の `afv` から、`buf` はスタックポインタから `0x38` の位置にあることが分かります。

`0x00000cb8` では、`buf` からの `ldr` と、その後のマジックナンバーのチェックが見つかります。

`0x00000c9c` では、次のような興味深い箇所があります。

0x00000c9c add x21, sp, 0x38 0x00000ca0 mov x2, 8 ; size_t nbyte 0x00000ca4 mov x1, x21 ; void buf 0x00000ca8 mov w0, 0 0x00000cac bl sym.imp.read ; ssize_t read(int fildes, void buf, size_t nbyte) ; ssize_t read(0, 0x0000000000000000, 0x00000000)

これで、これらのオフセットにある 2 つの値が幅と高さであることを確認できました。

iaito  GDB  `initialize_framebuffer` に戻ります。ウォッチポイントを設定してメモリを調べます。

(gdb) break *0xaaaaaaaa1354 Breakpoint 1 at 0xaaaaaaaa1354 (gdb) run Starting program: /home/user/challenge ./example.cimg [Thread debugging using libthread_db library "/lib/aarch64-linux-gnu/libthread_db.so.1].

Breakpoint 1, 0x0000aaaaaaaa1354 in initialize_framebuffer () (gdb) info registers x0 x0 0xaaaaaaac12a0 187649984565920 (gdb) watch (char )0xaaaaaaac12a0 Hardware watchpoint 2: (char )0xaaaaaaac12a0 (gdb) continue Continuing.

Hardware watchpoint 2: (char )0xaaaaaaac12a0

Old value = 0 '\000' New value = 27 '\033' 0x0000aaaaaaaa13bc in initialize_framebuffer () (gdb) x/24xb 0xaaaaaaac12a0 0xaaaaaaac12a0: 0x1b 0x5b 0x33 0x38 0x3b 0x32 0x3b 0x32 0xaaaaaaac12a8: 0x35 0x35 0x3b 0x32 0x35 0x35 0x3b 0x32 0xaaaaaaac12b0: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00

上のデータと比較してみましょう。

(gdb) x/24xb $x0 0xaaaaaaac12a0: 0x1b 0x5b 0x33 0x38 0x3b 0x32 0x3b 0x30 0xaaaaaaac12a8: 0x38 0x30 0x3b 0x30 0x38 0x30 0x3b 0x30 0xaaaaaaac12b0: 0x38 0x30 0x6d 0x50 0x1b 0x5b 0x30 0x6d

`0xaaaaaaac12a7` が最初に異なるバイトです。ここにウォッチポイントを設定できます。

(gdb) watch (char )0xaaaaaaac12a7 Hardware watchpoint 3: (char )0xaaaaaaac12a7 (gdb) continue Continuing.

Hardware watchpoint 3: (char )0xaaaaaaac12a7

Old value = 50 '2' New value = 48 '0' 0x0000aaaaaaaa128c in display ()

新しい関数を調べる必要があります。iaito で開きます。

![the graph of the initialize\_framebuffer function](https://rebel1725.codeberg.page/images/pic_20260527_003.png)

データの変化は `0x00001288`  `stp` 命令で起きていることが分かります。`0x0000128c`  `str` 命令も興味深いので、ここでのメモリを見てみましょう。

(gdb) x/24xb 0xaaaaaaac12a0 0xaaaaaaac12a0: 0x1b 0x5b 0x33 0x38 0x3b 0x32 0x3b 0x30 0xaaaaaaac12a8: 0x38 0x30 0x3b 0x30 0x38 0x30 0x3b 0x30 0xaaaaaaac12b0: 0x35 0x35 0x6d 0x20 0x1b 0x5b 0x30 0x6d (gdb) si 0x0000aaaaaaaa1290 in display () (gdb) x/24xb 0xaaaaaaac12a0 0xaaaaaaac12a0: 0x1b 0x5b 0x33 0x38 0x3b 0x32 0x3b 0x30 0xaaaaaaac12a8: 0x38 0x30 0x3b 0x30 0x38 0x30 0x3b 0x30 0xaaaaaaac12b0: 0x38 0x30 0x6d 0x50 0x1b 0x5b 0x30 0x6d

`0xaaaaaaac12b0` からの 8 バイトのブロックは、`str` 命令によって変更されています。レジスタがその物語を示しています。

(gdb) info registers x2 x2 0xaaaaaaac12a0 187649984565920

`x2` についてはあまり気にしなくてかまいません。`x4``x5``x1` に注目しましょう。`0x00001268`  `0x00001270` では、非常に重要なレジスタ `x25` が見えます。これは、`x2` が指すメモリに書き込まれるデータを含むメモリのアドレスを保持しています。そのメモリを調べてみましょう。

(gdb) x/24xb $x25 0xfffffffff270: 0x1b 0x5b 0x33 0x38 0x3b 0x32 0x3b 0x30 0xfffffffff278: 0x38 0x30 0x3b 0x30 0x38 0x30 0x3b 0x30 0xfffffffff280: 0x38 0x30 0x6d 0x50 0x1b 0x5b 0x30 0x6d

`0x00001238` では、`x25` のアドレスが `x0` にコピーされます。その後、次のレジスタへいくつかのデータがコピーされます。そして `snprintf` が呼ばれます。`snprintf` は、書式付き出力を文字配列に書き込みます。

int snprintf(char str, size_t size, const char format, ...);

つまり、`x25` のメモリに書式付き文字列を書き込んでいるわけです。GDB を再起動し、`x2` が指すメモリを確認しましょう。

(gdb) break *0xaaaaaaaa1258 Breakpoint 1 at 0xaaaaaaaa1258 (gdb) run Starting program: /home/user/challenge ./example.cimg [Thread debugging using libthread_db library "/lib/aarch64-linux-gnu/libthread_db.so.1].

Breakpoint 1, 0x0000aaaaaaaa1258 in display () (gdb) x/s $x2 0xaaaaaaaa1510: "\033[38;2;%03d;%03d;%03dm%c\033[0m" (gdb) x/29xb $x2 0xaaaaaaaa1510: 0x1b 0x5b 0x33 0x38 0x3b 0x32 0x3b 0x25 0xaaaaaaaa1518: 0x30 0x33 0x64 0x3b 0x25 0x30 0x33 0x64 0xaaaaaaaa1520: 0x3b 0x25 0x30 0x33 0x64 0x6d 0x25 0x63 0xaaaaaaaa1528: 0x1b 0x5b 0x30 0x6d 0x00

これが書式文字列です。`x/s` の結果に違和感がある場合は、Python を使ってこの文字列を 1 バイトずつ表示できます。

print(b"\x1b\x5b\x33\x38\x3b\x32\x3b\x25\x30\x33\x64\x3b\x25\x30\x33\x64\x3\ b\x25\x30\x33\x64\x6d\x25\x63\x1b\x5b\x30\x6d\x00") b'\x1b[38;2;%03d;%03d;%03dm%c\x1b[0m\x00' blob = b'\x1b[38;2;%03d;%03d;%03dm%c\x1b[0m\x00' for i1 in range(len(blob)): ... blob[i1:i1+1] ...
b'\x1b' b'[' b'3' b'8' b';' b'2' b';' b'%' b'0' b'3' b'd' b';' b'%' b'0' b'3' b'd' b';' b'%' b'0' b'3' b'd' b'm' b'%' b'c' b'\x1b' b'[' b'0' b'm' b'\x00'

この書式文字列には 4 つのプレースホルダがあります。最初の 3 つは `%03d` で、整数を少なくとも 3 桁の 10 進数に整形します。たとえば 7 は `007`、15 は `015` になります。最後の 1 つは `%c` で、1 文字だけに対応します。

これで次のバイトパターンが分かりました。

0x1b 0x5b 0x33 0x38 0x3b 0x32 0x3b [N1] [N1] [N1] 0x3b [N2] [N2] [N2] 0x3b [N3] [N3] [N3] 0x6d [CH] 0x1b 0x5b 0x30 0x6d

では、プレースホルダに何が入っているのかを調べましょう。

(gdb) info registers x3 x4 x5 x6 x3 0x50 80 x4 0x50 80 x5 0x50 80 x6 0x50 80

すべて `0x50` です。これは例の cIMG で使ったデータのようです。ここを明確にするために、cIMG を再作成してみましょう。

with open("example.cimg", "wb") as file: ... file.write(b"cIMG\x02\0\x0a\x0a\x12\x34\x56\x78") ... file.write(b"\x50"(1010*4-4)) ...
12 396


(gdb) break *0xaaaaaaaa1258 Breakpoint 1 at 0xaaaaaaaa1258 (gdb) run Starting program: /home/user/challenge ./example.cimg [Thread debugging using libthread_db library "/lib/aarch64-linux-gnu/libthread_db.so.1].

Breakpoint 1, 0x0000aaaaaaaa1258 in display () (gdb) info registers x3 x4 x5 x6 x3 0x12 18 x4 0x34 52 x5 0x56 86 x6 0x78 120 ```

したがって、上の 16 進パターンにある N1, N2, N3 はそれぞれ R, G, B に対応し、CH は埋め込みたい文字そのものです。

これで GDB に戻り、各 memcmp の前でブレークし、x/24xb $x1 を実行して期待される framebuffer を調べます。そして、その framebuffer を使って期待される cIMG データを復元します。あとは十六進エディタでそのデータを書き込めばよいです。

サイズのチェック

しかし、期待されるデータを埋めても、実行ファイルはまだ flag を返しません。GDB でメモリを確認すると、memcmp は期待通り 0 を返していました。したがって、満たされていない別の条件があるはずです。

iaito に戻り、最初の memcmp から後ろへさかのぼります。0x00000d60cmp があり、var_40h と 4 を比較しています。initialize_framebuffer0x00001348 には、幅と高さの積を保存する str があります。

0x00000d60cmp が、その後の w0 の値を決めます。var_40h が 4 なら w0 は 1 になり、それ以外なら 0 になります。

0x00000d78 では w0 が 0 と比較され、0x00000d84ccmp 命令の動作に影響します。[x22, 0x13] の内容が w0 に読み込まれ、[x1, 0x13]w2 に読み込まれます。memcmp から分かるように、x22 は実際の framebuffer のアドレスで、x1 は期待される framebuffer のアドレスです。すでにデータは期待値と一致しており、オフセット 19 は ASCII 文字なので、w0w2 は同じであり、かつ 0 ではないはずです。

ここから、var_40h が 4 なら w2w0 と比較され、その結果 w19 は 1 に設定される、と結論できます。それ以外の場合、w2 は 0 と比較され、w19 は 0 になります。

最初の memcmp の後では、0x00000e7cw0 が 0 なので、w19 は 0 と比較されます。すると cset によって、等しくなければ w19 は 1、そうでなければ 0 に設定されます。

そこで、hex エディタを使って cIMG を修正し、幅と高さを掛け合わせると 4 になる値に変更します。たとえば 0202 にできます。これで実行ファイルは flag を返すはずです。

結論

上で説明した手順には次が含まれます。

  1. radare2 と iaito を使った静的解析
  2. GDB を使った動的解析。内容は次のとおりです。

  3. 制御フローの改ざん

  4. メモリの निरी視
  5. 整数の確認

これはリバースエンジニアリングではごく典型的な手順です。

ちなみに、この実行ファイルを逆向きに解析する方法は他にもあるかもしれません。しかし、どの方法を使うにしても、たいていは静的解析と動的解析の両方が関わります。

Tildeverse Banner Exchange