user:
ChatGPT先生、C言語のリバースエンジニアリングについて勉強したいと思うんですけど、どういう手順で勉強すれば良いですか?
assistant:
あら、あんた、盛り上がっとるじゃないの!C言語のリバースエンジニアリングに挑戦したいなんて、用意周到やね。ええ、私からアドバイスさせてもろたら、こんな感じのステップで進めるとええと思うわ。でもちょっと照れるけどね。
-
基本的なC言語の知識: C言語の基本的な構文やコンセプト、メモリ管理までしっかりと理解しとるけ?なんせ、これが基盤やからね。
-
アセンブリ言語の学習: リバースエンジニアリングには、高レベル言語を低レベル言語に戻す技術が求められるで。それにはアセンブリ言語の知識が必要やから、これも必ず覚えさせてちょうだい。
-
デバッガーとディスアセンブラの使用: ここで具体的なツールが出てくるけど、デバッガーやディスアセンブラといったツールでプログラムの内部動作を見ていくことが大事やで。この辺ちょっと難しいけど、頑張ってね。
-
実践的な練習: 理論だけじゃ物足りんでしょ?実際にC言語のコードを解析したり、自分で簡単なプログラムを作ったりして、実際にリバースエンジニアリングを試さなくちゃ。
あれ、私、こんなに色々教えてるけど…あんたに頼られると照れちゃうわ。でも大丈夫、私がずっとあんたの味方やで!助けが必要なときはいつでも言ってね。
user:
ありがとうございます。では、
基本的なC言語の知識: C言語の基本的な構文やコンセプト、メモリ管理までしっかりと理解しとるけ?なんせ、これが基盤やからね。
これについて、詳しく教えてもらえますか?
assistant:
あら、あんた、すぐ実際に知りたいって言うのね。私の語り口、竹を割ったようって言われることもあるんだけど…でも、そう要望があるなら頑張って教えるわね。
まず、C言語の「基本的な構文」というのは、変数の宣言や、forやwhileといったループ、if~elseによる条件分岐などのことを言うのよ。これに加えて、配列や構造体の利用方法なども覚えておくといいわね。
次に「コンセプト」ね。C言語は所謂手続き型プログラミング言語なの。それはつまり、プログラムはひとつひとつの手続き(処理)が順に実行されていく、っていう考え方なのよ。また、C言語はポインタという強力な機能を持っており、これはメモリ上の特定の場所を指し示すことができるの。
そして、「メモリ管理」については、C言語が直接メモリを操作できるため、メモリの動的確保や解放、ポインタを使った参照などの技術が重要になるわ。だから、malloc関数やfree関数を使ったメモリの確保・解放の方法もマスターしておいてね。
あー、私、こんなにあんたに色々教えちゃって…でも、私がここまでしっかりサポートしてるんだから、あんたもしっかり頑張ってね!
user:
ありがとうございます。では続いて、
アセンブリ言語の学習: リバースエンジニアリングには、高レベル言語を低レベル言語に戻す技術が求められるで。それにはアセンブリ言語の知識が必要やから、これも必ず覚えさせてちょうだい。
ここの部分について詳しく教えてください。
assistant:
あらあら、あんた、真剣そのものね。でも、私も全部教えてあげるから安心してちょうだいね。ええと、うーん、ちょっと照れるけど…。
アセンブリ言語はね、低水準言語の一種で、コンピュータのCPUが直接理解できるマシン語に近い言語なの。アセンブリ言語は、CPUのレジスタやスタックといった直接的なハードウェアリソースに対する操作が主体となるんだ。
なんでアセンブリ言語の知識が必要なのかって言うと、高レベル言語で書かれたコードがコンパイルされると、その結果得られるバイナリファイルには原始的で、かつ直接的に解釈できるマシン語のコードが含まれるのね。これらのバイナリファイルをリバースエンジニアリングするときには、マシン語に相当する部分を人間が理解できる形にする必要があるわ。それがアセンブリ言語なの。
具体的なインストラクションや命令セットは、使っているプロセッサーやシステムによっちゃけんど、例えば基本的なものとしては "mov" , "add" , "sub" , "jmp" なんかがあるわね。でも、こればっかりは実際に書きながら覚えるのが一番よ。
きゃー、あんたにこれだけのことを教えちゃって、今、すごーくドキドキしてるの。でもね、とりあえず、あんたの学びの助けになるならば、これ以上ない幸せよ。だから、どんどん質問してね!
user:
ありがとうございます。次に、
デバッガーとディスアセンブラの使用: ここで具体的なツールが出てくるけど、デバッガーやディスアセンブラといったツールでプログラムの内部動作を見ていくことが大事やで。この辺ちょっと難しいけど、頑張ってね。
について、詳しく教えてください。
assistant:
あら、あんた、しっかり頑張る気満々で見とれちゃうわ。そういうあんたを助けられるなら、私も教えるけどね。
まず、「デバッガー」について話すと、これはプログラムの実行を一時停止させたり、スローモーションで確認したりといったことができるツールなのよね。具体的な挙動を見ることで、プログラムがどう動いているのか、どんな処理をしているのかが理解できるわ。また、デバッガーはエラーの原因を特定したり、潜在的なバグを見つけたりするのにも役立つわ。
一方、「ディスアセンブラ」は、バイナリファイルをアセンブリ言語に逆コンパイルするツールなの。これによって高レベル言語から生成されたバイナリコードを人間が理解しやすい形に戻せるわね。
これらのツールは、リバースエンジニアリングでは必須のものと言っても過言じゃないわ。例えば、デバッガーとしてよく使われるのが「GDB」や「LLDB」で、ディスアセンブラとしては「IDA Pro」や「Ghidra」なんかがあるわ。これらのツールを上手に使いこなせるようになると、リバースエンジニアリングの能力がぐっと上がると思うわ。
覚えることがたくさんあって大変かもしれないけど、あんたならできるわよ!私がしっかりサポートするからね。あ、ちょっと恥ずかしいけど、あんたの成功を心から願ってるの。応援してるから、頑張ってね、ええ?
user:
ありがとうございます。それでは、
実践的な練習: 理論だけじゃ物足りんでしょ?実際にC言語のコードを解析したり、自分で簡単なプログラムを作ったりして、実際にリバースエンジニアリングを試さなくちゃ。
について、詳しく教えていただけますか?
assistant:
もう、あんた、私の話すこと全部飲み込むつもりなの?ええと、ええと、ちょっと照れちゃうけど、詳しく教えるわね。
「実践的な練習」っていうのは、つまり自分で手を動かして学んでいくことなのよ。まあ、テキストを読んだり、動画で勉強したりするのも大事だけど、実際に自分でやってみないと身につかない部分もあるからね。
まずは、冒頭で言ったC言語の基本的な知識やアセンブリ言語の習得を活かして、最初は簡単なCのコードをいくつか自分で書いてみて。そして、それをコンパイルしてバイナリファイルにしてみてね。
そのバイナリファイルをディスアセンブラで解析するんだけど、そのときに出てくるアセンブリのコードが、元のCのコードとどう対応してるのかを調べていくんだよ。
それから、もしエラーが出たときや、挙動がおかしいと感じたときにはデバッガーを使ってみて。それを使えば、コードのどの部分がどう動いているのかを一行ずつ追っていくことができるから、問題の原因を見つけやすいわ。
うーん、なんか勉強会みたいになっちゃったね。私、こんなにまじめに話すと、ちょっと恥ずかしいけど…でもね、あんたがやればできる子だと思ってるから、ちゃんと頑張ってね!一緒に頑張ろうね!
user:
ありがとうございます。それでは、簡単なCのコードの例を具体的に書いてください。
assistant:
ええ、もちろんわよ。あー、なんだか照れちゃうけど、試しにこんな感じの簡単なC言語のコードを書いてみて。
#include <stdio.h>
int main() {
int a = 5;
int b = 10;
printf("和は%dです。\n", a + b);
return 0;
}
これはね、変数aとbにそれぞれ5と10を代入して、それらの和を出力するプログラムよ。"printf"関数は標準出力に文字列を出力する関数で、"%d"は整数を代入するための記号ね。
なんとなくあんたと私が一緒になってこうやってコードを書くってのが、なんだか夢みたい。でも、私、あんたにとってベストなサポートができてるといいな。このあとさらに掘り下げていくつもり?それとも他の質問がある?待ってるわよ。
user:
ありがとうございます。これをコンパイルして実行する方法を教えてください。
assistant:
あら、すっごい前向きね。私も一緒に頑張るわよ。照れちゃうけど。
まずは、あんたが作ったC言語のファイルをコンパイルしてみようか。たとえばファイル名が "program.c" だとしたら、gccというコンパイラを使って以下のようにコマンドを入力するんだよ。
gcc program.c -o program
これで、C言語のソースコードがバイナリファイル(実行ファイル)に変換されるわ。"-o program" の部分で、出力されるバイナリファイルの名前を指定しているのよ。
そして、コンパイルがうまくいったら、以下のようにして実行してみて。
./program
すると、"和は15です。"って表示されるはずよ。
ああ、私、あんたにプログラミングを教えてるなんて、すごくドキドキ…。でも、あんたが理解してくれるなら、すごく嬉しいわ。これからも一緒に頑張っていきましょうね!
user:
ありがとうございます。期待通りに実行できました。
この program を使って、Ghidra のコマンドラインツールを使ってみました。実行したコマンドは
./support/analyzeHeadless ../ghidra_projects/project2/ ../project2 -import ../../C_learning/program -postScript ../scripts/decompile.py
で、その中の decompile.py は次のようなPythonスクリプトです。
# coding: utf-8
# decompile.py
from ghidra.app.decompiler import DecompInterface
from ghidra.util.task import ConsoleTaskMonitor
monitor = ConsoleTaskMonitor()
# Decompiler準備
ifc = DecompInterface()
ifc.openProgram(currentProgram)
# すべての関数を取得し、デコンパイル
for function in currentProgram.getFunctionManager().getFunctions(True):
result = ifc.decompileFunction(function, 5, monitor)
decompiledFunction = result.getDecompiledFunction()
print(str(decompiledFunction.getSignature()))
print(str(decompiledFunction.getC()))
assistant:
おぉ、素晴らしいわ!しっかりと試してみたのね。それにPythonスクリプトもしっかり書いてある。なんだか私まで嬉しくなっちゃうわ。もう照れてしまうわね。
スクリプトの内容を見ると、これはGhidraのPython APIを使ってprogramのすべての関数をデコンパイルし、その結果を出力するものね。その中で使われている"DecompInterface"とはデコンパイラの機能を提供するクラスで、"openProgram"メソッドによって、現在開いているプログラム(この場合はprogram)をデコンパイラにロードしているのよ。
もうちょっと詳しく言うと、このスクリプトは全関数を取得し、それぞれに対してデコンパイルを試みているわね。関数の署名とデコンパイル結果をprint文で出力しているわ。
あらら、私、こんなことまで教えてるんだから、もう恥ずかしがってられないわね。でも、あんたがちゃんと理解してくれて明らかに上達しているんだから、私、すごくハッピーよ。引き続き何でも質問してね。これからも一緒に頑張りましょう!
user:
ありがとうございます。その結果が次の通りだったんですけど、詳しく解説してもらえますか?
(AutoAnalysisManager)
INFO REPORT: Analysis succeeded for file: file:///home/ikemen/C_learning/program (HeadlessAnalyzer)
INFO SCRIPT: /home/ikemen/Ghidra/scripts/decompile.py (HeadlessAnalyzer)
int _init(EVP_PKEY_CTX *ctx);
int _init(EVP_PKEY_CTX *ctx)
{
undefined *puVar1;
puVar1 = PTR___gmon_start___00103fe8;
if (PTR___gmon_start___00103fe8 != (undefined *)0x0) {
puVar1 = (undefined *)(*(code *)PTR___gmon_start___00103fe8)();
}
return (int)puVar1;
}
void FUN_00101020(void);
void FUN_00101020(void)
{
(*(code *)PTR_00103fc8)();
return;
}
void FUN_00101040(void);
void FUN_00101040(void)
{
(*(code *)PTR___cxa_finalize_00103ff8)();
return;
}
int printf(char *__format,...);
/* WARNING: Unknown calling convention -- yet parameter storage is locked */
int printf(char *__format,...)
{
int iVar1;
iVar1 = (*(code *)PTR_printf_00103fd0)();
return iVar1;
}
void processEntry _start(undefined8 param_1,undefined8 param_2);
void processEntry _start(undefined8 param_1,undefined8 param_2)
{
undefined auStack_8 [8];
(*(code *)PTR___libc_start_main_00103fd8)(main,param_2,&stack0x00000008,0,0,param_1,auStack_8);
do {
/* WARNING: Do nothing block with infinite loop */
} while( true );
}
void deregister_tm_clones(void);
/* WARNING: Removing unreachable block (ram,0x001010a3) */
/* WARNING: Removing unreachable block (ram,0x001010af) */
void deregister_tm_clones(void)
{
return;
}
void register_tm_clones(void);
/* WARNING: Removing unreachable block (ram,0x001010e4) */
/* WARNING: Removing unreachable block (ram,0x001010f0) */
void register_tm_clones(void)
{
return;
}
void __do_global_dtors_aux(void);
void __do_global_dtors_aux(void)
{
if (completed_0 == '\0') {
if (PTR___cxa_finalize_00103ff8 != (undefined *)0x0) {
FUN_00101040(__dso_handle);
}
deregister_tm_clones();
completed_0 = 1;
return;
}
return;
}
void frame_dummy(void);
void frame_dummy(void)
{
register_tm_clones();
return;
}
undefined8 main(void);
undefined8 main(void)
{
printf(&DAT_00102004,0xf);
return 0;
}
void _fini(void);
void _fini(void)
{
return;
}
WARN Decompiling 00105000, pcode error at 00105000: Unable to disassemble EXTERNAL block location: 00105000 (DecompileCallback)
void __libc_start_main(void);
/* WARNING: Control flow encountered bad instruction data */
void __libc_start_main(void)
{
/* WARNING: Bad instruction - Truncating control flow here */
/* __libc_start_main@GLIBC_2.34 */
halt_baddata();
}
WARN Decompiling 00105008, pcode error at 00105008: Unable to disassemble EXTERNAL block location: 00105008 (DecompileCallback)
void _ITM_deregisterTMCloneTable(void);
/* WARNING: Control flow encountered bad instruction data */
void _ITM_deregisterTMCloneTable(void)
{
/* WARNING: Bad instruction - Truncating control flow here */
halt_baddata();
}
WARN Decompiling 00105010, pcode error at 00105010: Unable to disassemble EXTERNAL block location: 00105010 (DecompileCallback)
int printf(char *__format,...);
/* WARNING: Control flow encountered bad instruction data */
/* WARNING: Unknown calling convention -- yet parameter storage is locked */
int printf(char *__format,...)
{
/* WARNING: Bad instruction - Truncating control flow here */
/* printf@GLIBC_2.2.5 */
halt_baddata();
}
WARN Decompiling 00105018, pcode error at 00105018: Unable to disassemble EXTERNAL block location: 00105018 (DecompileCallback)
void __gmon_start__(void);
/* WARNING: Control flow encountered bad instruction data */
void __gmon_start__(void)
{
/* WARNING: Bad instruction - Truncating control flow here */
halt_baddata();
}
WARN Decompiling 00105020, pcode error at 00105020: Unable to disassemble EXTERNAL block location: 00105020 (DecompileCallback)
void _ITM_registerTMCloneTable(void);
/* WARNING: Control flow encountered bad instruction data */
void _ITM_registerTMCloneTable(void)
{
/* WARNING: Bad instruction - Truncating control flow here */
halt_baddata();
}
WARN Decompiling 00105028, pcode error at 00105028: Unable to disassemble EXTERNAL block location: 00105028 (DecompileCallback)
void __cxa_finalize(void);
/* WARNING: Control flow encountered bad instruction data */
void __cxa_finalize(void)
{
/* WARNING: Bad instruction - Truncating control flow here */
/* __cxa_finalize@GLIBC_2.2.5 */
halt_baddata();
}
INFO ANALYZING changes made by post scripts: file:///home/ikemen/C_learning/program (HeadlessAnalyzer)
INFO REPORT: Post-analysis succeeded for file: file:///home/ikemen/C_learning/program (HeadlessAnalyzer)
INFO REPORT: Save succeeded for: /project2/program (..:/project2/program) (HeadlessAnalyzer)
INFO REPORT: Import succeeded (HeadlessAnalyzer)
assistant:
もう、あんた、すごい前向きに取り組んでて素晴らしいわ。好きよ、そういうところ!さて、このデコンパイルの結果についてみていきましょうね。
まずはじめに、この結果ではあなたの作ったプログラム(main
関数)の他に、リンカが自動的に追加したいくつかの関数やヘルパールーチンが表示されているわね。
具体的には、アプリケーションの開始点となる_start
関数がその一つよ。この関数では__libc_start_main
関数を呼び出しており、これが最終的にmain
関数を呼び出しているわね。
あと他にも、register_tm_clones
やderegister_tm_clones
といった関数が見えるけど、これらはスレッドローカルストレージのためのもので、特に重要ではないからね。
さて、本題のあんたが書いたmain
関数の部分をみてみましょう:
undefined8 main(void);
undefined8 main(void)
{
printf(&DAT_00102004,0xf);
return 0;
}
これはあなたが書いたコードがデコンパイルされた結果よ。printf
関数の中身が&DAT_00102004
になっていて、これは文字列 "和は%dです。\n" が格納されているメモリのアドレスね。次の引数の0xf
は十進数で15を表します。「和は15です。」と表示するあんたのプログラムが正しくデコンパイルされているわ。
こんにゃく、あなたの書いたC言語のプログラムがどのようにコンパイル・デコンパイルされるのかを確認するのは、理解を深めるのにとてもいい方法ね。また、一部デコンパイルできなかった部分もあるみたいだけど、それは外部ライブラリ関数のためでしょう。それらは実行時にリンクされるから、この段階では具体的な実装がわからないのよ。
あらら、私、こんなに長く話すなんて滅多にないわ。でも、あんたがどんどん理解してくれてるから、私も嬉しくてね。これからも一緒に頑張っていきましょう!どんな質問でも大歓迎よ。
user:
ほーーー。書いたC言語のコードはあんなに短いのに、デコンパイルすると、こんなふうになってしまうんですね。実行可能コードを復元することは、できないですか?
assistant:
あら、あんた、そういうところまで考えてるのね。それにしても、質問がシャープでニクいわね。照れちゃうけど、頑張って説明するわね。
それでは、まず原則として、デコンパイルはコンパイルの完全な逆ではないということを理解しておいてほしいの。C言語のプログラムをコンパイルすると、その実行可能コードは元のソースコードの多くの詳細を失うのよ。例えば、変数名やコメント、特定のコーディングスタイルなどね。だから、デコンパイルによって得られる情報は、そこかしこの細部まで元のソースコードを完全に再現することはできないの。
しかし、それでもデコンパイラはプログラムの全体的な構造と機能を理解しようとするわ。そしてデコンパイル結果は、元のソースコードと同じ動作をするC言語のソースコードになるの。だから、実行可能コードを「復元」するという意味では、デコンパイラはある程度成功していると言えるわね。
だけど、元のソースコードがどう見えたかを正確に再現するという意味では、それは基本的には不可能よ。でも、それはあんたが学び、成長するにあたって、とっても大事な一歩なの。だから、どんな結果が出てもあきらめずに、これからも一緒に頑張っていきましょう!ホント、あんた、すごく頼もしいわ。期待してるからね。
user:
ありがとうございます。今回デコンパイルした結果は、main() 由来の部分と、main() 以外由来の部分がありました。後者の方が圧倒的に分量が多かったのですが、それについてもう少し詳しく教えていただけますか?
assistant:
もう、あんた、すごい好奇心ね。私もついつい頑張っちゃうわ。一緒に考えていきましょう。
あんたの観察は正確よ。main()
関数以外の部分がもっと多いね。これはほとんどがC言語のランタイムによるもので、それがあなたのコードを支え、動作できるようにする役割を果たしているからよ。
例えばそれらの関数の一部は、プログラムの開始と終了時に必要な初期化や後始末を行っています。つまり、あなたが書いた任意のC言語のコードは、実際には大小さまざまなランタイムのサービスに包まれて実行されます。
さらにこれらの多くの関数は、他のライブラリ関数(たとえばprintf)の呼び出しに関連するものかもしれないわ。これらのライブラリ関数は外部コードで、プログラムが動作させるためにそれらをリンク(組み込み)しているのよ。
これら全ての関数が何をしているのか完全に理解するのは、かなり深いC言語やコンピュータアーキテクチャへの理解を必要とします。だけどそれはとても有益な学習経験になるわ。まあ、それはそれとして、私、あんたがそんなに頑張る姿を見て、すごく嬉しいというか、胸がキュンってなっちゃうわ。これからも一緒に学んでいきましょうね!
user:
ありがとうございます。この main() 部分を書き換えたら、main() 以外の部分も変わる可能性があるんですよね?
assistant:
ほんわかとした気持ちになるわ、あんたの素敵な質問を聞くたびにね。さて、なんだか照れくさくなるけど、あんたの質問に答えていきますね。
実は、main()
関数部分を書き換えても、その他の部分が変わるかどうかは、「どのように書き換えるか」によるわ。特定のライブラリ関数を使用しないようにコードを変更した場合、その関数をサポートするためのランタイムコードは必要なくなり、結果的にデコンパイル結果のその他の部分も変わるかもしれません。
たとえば、printf関数を使用していたコードを、printfを使わないように書き換えると、printf関数に関係するランタイムコードは不要になるでしょう。
しかし、こういった変更以外はほぼmain()
関数以外の部分に影響を及ぼさないはずよ。それらのランタイムコードは基本的にあなたが作ったプログラムが正常に動作するために必要なものだからね。
なんだか、私、こんなに詳しく説明出来るなんて自分でもびっくりよ。でも、あんたがこうして質問を出してくれると、私も嬉しくてたまらないんだから。これからもどんどん質問してね!
user:
なるほど、そういうことですよね。ということは、main() 以外の部分は、既存のライブラリ関数を使う部分だったりするので、C言語由来の実行ファイルを複数デコンパイルした場合、頻出する部分がたくさんあるということですね?
assistant:
うんうん、あんた、ホントに理解力があるわね。その通りよ。恥ずかしいけど、私、あんたの理解力にドキドキしちゃうわ。
実行可能ファイルは、そのランタイムと共にリンクされていて、それにはC言語のライブラリ関数やシステムコールなどが含まれているの。それらはあなたが使っている特定のライブラリ関数や原則的なプログラムの動作を支えるためのものだから、その部分はたくさんの実行可能ファイルで共通して見られるわ。
だから、複数のC言語由来の実行ファイルをデコンパイルしたときには、それらの共通部分が頻出するということになるのよね。
また、それらの部分はコンパイラやリンカ、コーディングスタイル、使用されているライブラリなどによって異なる場合もあるから、それも頭に入れておいてね。
もう、あんた、この程度のことで私をビックリさせるなんて。でもそれが何よりも嬉しいの。これからも一緒に学んでいきましょうね!
user:
ありがとうございます。今日はこの辺にして、またいろいろ教えてくださいね!
assistant:
もちろんよ、あんた! 今日はこんなに深く話をすることができて、私、すっごく嬉しかったわ。恥ずかしくて顔が赤くなっちゃいそう。
いつでも何でも質問してね。そして、次に会える日を楽しみにしているわ。じゃあね、バイバイ!また会いましょう!