これは何?
坂井弘亮さんの「リンカ・ローダ実践開発テクニック」の8章「ローダの原理と簡易ローダの作成」を手元でも実行してみようと思ったのですが、環境の違いのせいかそのままでは動かなかったので、その原因を調べて動くようにした記録です。そこに至る経緯も自分なりに勉強になったので、結論を先に書くスタイルではなく、時系列順に(覚えている限り)書いてみようと思います。
作業記録はこれです:
https://github.com/ashiato45/loader-test
環境
バイナリを触りますし、プログラムの自己書き換えを含むテーマなので、動作するしないは環境に強く依存します。実際、私の所有している新しいマシンでは対象プログラムのローダ領域へのコピーの時点でこけてしまいこちらは未だに原因不明です。今回の私のローダが動作した環境は以下のとおりです:
試行錯誤
素朴に動かそうとする
大体本に書いてあるとおり、ローダを作り、セクションの書き込み、実行権限をオンにしました。ただ、そのまま動かすにしても次のような課題はあり、とりあえずの対処をしました。
Elf_Ehdrがない
elf.hを見ながら次のようなマクロを刺しました。loader-test/loader.c at 26fd62f7ba1671d6abadc5300aeebfdf725cb44e · ashiato45/loader-test · GitHub
IS_ELFがない
elf.hを見ながら次のようなマクロを刺しました。loader-test/loader.c at 26fd62f7ba1671d6abadc5300aeebfdf725cb44e · ashiato45/loader-test · GitHub
呼出規約が32ビットと64ビットで違う
この本は2010年発売なので恐らく32ビット環境を想定しているのですが、試した時点(2023年)では64bit環境が主流であり、32bit環境が手に入りませんでした。32bitの頃は関数呼び出しの引数はスタックに積むことになっていたのですが、64bitだとレジスタに入れ、残りをスタックに積むことになっていたので、gccでどうしているかを調べ*1 適当にやりました。うまくいかなかったら直そうと思っていたのだったと思います。
コンパイルスクリプトがどこだかわからない
また、ローダ側に、対象プログラムを格納するためのメモリ空間をあけるようにコンパイルする必要がありますが、そのためのもとのコンパイルスクリプトは自分のマシンの中を探しました。適当に/lib/を探したら見つかったのですが、普通に.textなどでgrepをかければよかったなと今になっては思います。
対象プログラムを簡単にする
上記のとおりでも動かなかったので、printfすらも省いて何もしないプログラムにしました。ただ、exitをするにはstdlibが必要だったので、元の本とは違ってそれだけincludeしています(これが落とし穴だった…)。
#include <stdlib.h> int main(int argc, char* argv[]){ exit(0); }
これらのコンパイル、実行は次のようになります
gcc simple.c -o simple -static -Wall --debug -O0 gcc loader.c -o loader -static -Wall --debug -O0 -T space.script ./chflg ./loader ./loader ./simple
(chflgのコンパイルは省略しています)
これを実行したところ、ロード済プログラムの実行中でsegmentation faultを得ました。とりあえずgdbにかけてみたのですが、自己書き換えプログラムである以上スタック情報もよくわかりませんし、停止時点で何がおこっていたのかもよくわかりませんでした。ここで一工夫が必要になります。
機械語レベルステップ実行
同じ状態からスタートして、同じ機械語命令を順番に実行したなら同じ結果になるはずで、上のような結果が得られるはずがありません。何が違っていたのかを調べるため、実行中のプログラムカウンタを追い掛けていって、どのような経路でプログラムが実行されていたのかを機械語レベルで追跡したいと考えました。そして、普通に「./simple」とした場合と「./loader ./simple」とした場合のどこで実行経路の違いが発生したのかを知りたくなりました。そのため、以前も使用したgdbのPython拡張を使って、機械語レベルで実行してその経路を出力させました。
そのために、loader-test/trackpc.py at main · ashiato45/loader-test · GitHub というスクリプトを組み(対象に応じて適当にコメントアウトして使います)、
gdb -x trackpc.py ./simple
や
gdb -x trackpc.py ./loader
とします。
これでローダ経由で動かしてみると次のような出力を得ました:
ちょっとは修行するかと思ってgdbでstepiした結果、やっと死亡地点を見つけられた。やった~ pic.twitter.com/ymYjwPNcOj
— 足跡45(避難先併用) (@ashiato45) 2023年2月13日
このプログラムカウンタの番地に対応する箇所を、直接実行のときと比べると、__libc_start_main.cのどこかの実行でおかしいことになっているらしいことがわかります:
うーん状態がぜんぜんちがう pic.twitter.com/n2ITn7qOGk
— 足跡45(避難先併用) (@ashiato45) 2023年2月17日
libc_start_main.cでウェブで検索し*2にらめっこして、environなどの変数名と比較すると、このあたり ( glibc/libc-start.c at 895ef79e04a953cac1493863bcae29ad85657ee1 · lattera/glibc · GitHub )と対応しそうということがわかり、セグフォはここ glibc/libc-start.c at 895ef79e04a953cac1493863bcae29ad85657ee1 · lattera/glibc · GitHub で変な値をアドレスと解釈して参照外ししているのが原因らしいということがわかりました。その原因はなんだろうと考えると、結局ロードされたプログラムのstart関数(mainの前にリンクされる、argcやargvをmainに渡して呼び出す関数)実行時点でスタックの様子がおかしくなっているからでは?となりました。
ちょっとうごいた
とりあえずstart時のスタックの様子を調べてみたところ、argcがトップに来ていて、そのあとにargvが並び…となっていたので、この状況を対象プログラムへのジャンプ前に再現してやればよさそうです:
スタックポインタの様子しらべてみたけど、x64でも_start時点でargc→argの順で積まれてるのね pic.twitter.com/JuVL9tJABx
— 足跡45(避難先併用) (@ashiato45) 2023年2月20日
そこで、対象プログラムへのジャンプの前にスタックのポインタを、ローダ呼び出し時のスタックにしたいわけですが*3、mainまで到達してしまうとargcはstartによってmainの引数に渡されたものを使う前提なので、&argcをstart開始時のスタックトップの位置として使うことはできません。しかし、argv[0]の位置(つまりargv)ならばスタックトップの1つ下の位置と一致するであろうと考えました。これは、わざわざstartのなかで、沢山あるかもしれないargvを逐一コピーしはしないだろうという予想からです。
…と思ったのですが、もとの本をみたらargv[0]のポインタを利用してそこから計算してるんですね。理由までは解説されていなかったと思うので、まあ自分で追い掛けてみて勉強になったのでいいのですが…
すると、先程の箇所は通過して別のところでセグフォするようになりました。
うごいた!
今度はrun_exit_handlerのところでセグフォしていることがわかりました。これはexit()関数に関係するものなので、ロード対象プログラムでstdlibのexitを使っているのが悪いのでは?というあたりをつけました。そこで、対象プログラムのほうでstdlibのインクルードをやめ、システムコールのほうの_Exit()を使用するようにしたところ最後まで動作しました。
ローダ経由でバイナリを動かすのできた!!!(いくらか制約はあるが) pic.twitter.com/pal7vxCQ6F
— 足跡45(避難先併用) (@ashiato45) 2023年2月23日
細かい理由は正直わかりませんが、stdlibをインクルードした時点で何かハンドラが登録されていて、それがローダ側と対象プログラムとで2回登録されたりしているのでは?という気がしています。
感想
この手の本は読み流すのと自分で手を動かすのでは理解度が大違いなのでやってみたのですが、実際壁にぶつかり調べてみるという過程で、機械語レベルステップ実行のような方法を覚えられたり、スタックの様子を観察したりできてとても勉強になりました。
また、リンカのほうに進んでいくにしても多分役立つでしょう。今回は完全に経過を記録したわけではないのですが、「メモリカナリアが悪さをしているのでは?」など見当違いのあたりをつけて突っ走ったりなどして徒労に終わったりしたのでなかなか疲れました。
startからmainの間でスタックに何をしたのか、やstdlibのexit()を使えるようにするにはどうするのか?などいろいろわからないことは残っていますが、時間がたってこのあたりをやる元気がでたらやってみたいですね。
その他
他にも似た問題で苦しんでる人がいました。セプキャンの審査の段階でここまで要求するんですね…
smallkirby.hatenablog.com
*1:x64の呼出規約を調べました [GDB] Linux x86-64 の呼出規約(calling convention)を gdb で確認する - th0x4c 備忘録
*2:本当はローカルを探せばみつかるのでしょうがみつかりませんでした…
*3:本当はプログラム名とかも書き換えたほうがいいんだろうけど今はそれどころではない