プログラム系統備忘録ブログ

記事中のコードは自己責任の下でご自由にどうぞ。

EXP-301受講記 & OSED合格記

OffSec社のEXP-301コースを受講し、OSED試験に合格できました。コースや試験の概要、これから取り組む方へのアドバイス、備忘録等の記事です。

なお、レポート作成方法はPEN-200-2022受講記 & OSCP合格記の時とほぼ同じです。よろしければそちらの記事もご参照ください。

分かる人向けの結果概要

  • EXP-301コース関連に合計210時間ほど取り組みました。
    • ExerciseやExtra Mileは一部を除いて完了しました。一部は挫折しました。
    • Labは3台全て完了しました。
  • 試験では3つの課題すべてを完了できました。レポートはコードや画像込みで103ページ、それら抜きで38ページになりました。

EXP-301コースとは

EXP-301: Windows User Mode Exploit Development(略称WUMED)コースとは、OffSec社が提供する学習コースです。OffSec社の他コースと同様に、Course Material(=教科書や説明動画)とLab(=自習環境)が与えられて、アクセス可能な期間内に自習していくコースです。「先生」は付きません。

EXP-301コースでは、C言語やC++言語製のWindows x86プロセスを標的としてネットワーク経由で攻撃する、Python3を使ったbasicなエクスプロイト開発手法を学べます。EXP-301コース受講者の必要条件(Prerequisites)として、以下の項目が挙げられています:

  • ImmunityDBG, OllyDBG等、Debuggerに馴染みがあること。
  • 32-bitのエクスプロイトのコンセプトの基本について馴染みがあること。
  • Python3コードを書くことに馴染みがあること。
    • 個人的には、strbytesの区別がついて、かつ関数の自作や呼び出しができれば、コース内容のコードは理解できるように思いました。
      • とはいえ「バグらせにくいコードを書けるかどうか」「バグらせた時にデバッグできるか」は、また別の話だと思います。
  • 基本的なC言語コードを読めて理解できること。
  • 基本的な32-bitアセンブリ言語コードを読めて理解できること。

EXP-301コースでは以下の事柄等を学べます。

  • x86アセンブリ言語の基礎
    • 前述したように必要条件で基本的なアセンブリ言語の知識を求られているため、本コース内容だけでx86アセンブリ言語を理解するのは少し難しいかもしれません。
  • WinDbgやIDA Freeの使い方と、それらを使ったバグの見つけ方
  • スタックバッファオーバーフローを利用してWindows x86プロセスでRCEするエクスプロイトを、Python3で作成する方法
    • 最も単純な手法として、/GS(=Stack Canary等)もDEPもASLRも無いプロセスを標的に、戻りアドレスをjmp espに改ざんしてRCEにつなげる手法から始まります。
  • SEHの原理やエクスプロイトで利用する方法と、Windowsの追加緩和策
  • DEPの原理と、ROPを使ってバイパスする方法
  • ASLRの原理と、アドレスをする脆弱性を使ってバイパスする方法
  • Egghunterや、Format String Specifier Attack等の、エクスプロイト開発に有用な手法の紹介
  • x86アセンブリ言語でのリバースシェルの自作方法
  • その他、エクスプロイト開発に有用なツールの紹介等

詳細はシラバス(Course Materialテキストの目次抜粋版のようです)をご参照ください。

反対に、EXP-301コースでは以下の事柄を扱いません。

  • ELF等の*nix環境を標的としたエクスプロイト開発
  • x64アセンブリ言語や、Windowsのx64プロセス
  • ヒープオーバーフローを利用したRCE

なお、上位コースであるEXP-401: Advanced Windows Exploitationコースでは、Windows x64プロセスのヒープを使ったエクスプロイトやカーネルエクスプロイトを学べるようです。

サポート関係

  • メールサポート
    • Contact support – OffSec Support Portalに説明がある通り、各種問い合わせ窓口用のメールアドレスが紹介されています。困ったらぜひ質問してみましょう。
    • 私は、以下の部門にメールを送りました:
      • Help部門に「Student Forum(後述します)のEXP-301用板へのアクセス権をください」と依頼しました。
      • Proctoring部門に「監視ツール動作確認用のクレデンシャルをください」と依頼しました。詳細は後述します。
      • Challenge部門に「IDAの逆コンパイル機能は使えますか?」や、OSED試験のレポート要件等を質問しました。
  • OffSec Forums(=Student Forum)
    • Course Materialの章ごとや、Labのマシンごとに板がある掲示板です。私の場合、前述したようにHelp部門へメールを送ることでアクセス可能になりました。
    • PEN-200コースの板と比べると、書き込み数は少なめでした。とはいえその分、すべての書き込みに目を通すことも容易いでしょう。とても役立つ書き込みもありました。
  • Discord
    • OffSec Community Chat User Guide – OffSec Support Portalに記載があるように、OffSec社は公式Discordサーバーを運営しています。
      • なお、上記のページでは当該Discordサーバーへの参加方法に「ポータルにログインして~」とありますが、少なくとも本記事執筆時点ではOffSecトップページ上部メニューのKali & CommunityOffSec Discordリンクから参加できます。
    • Discordサーバーへ参加した直後はPublicチャンネルのみ参加できます。コース固有のPrivateチャンネルへ参加するには、#role-requestsチャンネルへ行き、指示に従ってください。
    • EXP-301コースのチャンネルでは、1日に複数人が質問していることがあるほど活発です。しかし少ないチャンネルで複数の質問が飛び交うことがよくあります。「書き込んで何かヒントや助言をもらえたらラッキー」精神で書き込むのがいいのかもしれません。
    • 特定のExerciseやLabで詰まった場合は、過去ログ検索でいいものが引っかかるかもしれません。私の場合はとあるLabの攻略で行き詰まった末に検索してみると、打開策が得られました。

IDAの逆コンパイル機能は使えないので注意

EXP-301コースでは、受講者のKali LinuxにIDA Freeをインストールして使用します。Course Materialの作成時点ではIDA Freeの最新バージョンが7.0系列だったようでして、そのバージョンではクラウドベースの逆コンパイラはx64バイナリのみ対応していました。一方で本記事執筆時点ではIDA Freeのバージョン8.3が登場しており、x86バイナリも逆コンパイルできます。「そうなるとEXP-301コースやOSED試験で使うバイナリを逆コンパイルできるのでリバーシングが楽になるのでは?」と思ったのですが、OffSec社にメールで聞いてみると「クラウドベースの逆コンパイラを使うということはバイナリをクラウドへアップロードすることであり、それはAcademic Policyに反する。そのため使用不可。」という回答がありました。

そのためコース受講時に使用するIDAでは、通常ではF5キーに割り当てられている逆コンパイルショートカットを、まず押さない配置へ変更しました。メニューのOptionsShortcutsから、hx:GetPseudoのキー割当を変更しましょう:

IDAの逆コンパイル操作ショートカットは誤操作しかねないF5から変更しましょう

なお、IDAの逆アセンブル表示画面で自動的に振られるコメントは、ジャンプテーブル箇所などではIDA 7.0よりもIDA 8.3のほうがより賢いコメントを表示してくれるようでした。そのためIDA Freeの最新バージョンをインストールすることをおすすめします。

また、FAQにある通りOSED試験では使用可能なプログラミング言語やツールに制限があります:

EXP-301コース受講中では別のプログラミング言語やツールを使えるかもしれませんが、OSED試験に備えるためにコースで紹介されているツールのみを使って手に馴染ませたほうが良いと思います。ついでに言及すると、Python3でのエクスプロイト開発でpwntoolsライブラリは使えます。受信が必要な場合等で便利なことはあるかもしれません。

Lab環境のWinDbgバージョンが古くて一部辛い

少し前にWinDbgの旧バージョンではfnstenv結果のFIPが0固定になるらしいという記事を書きました。なぜこの現象に気付いたかというと、EXP-301コースのWinDbgがちょうどそのバグに引っかかるバージョンだったからです!エクスプロイト開発の最後の最後で悩むことになるので注意が必要です。前述のStudent ForumやDiscordでも、そのバグに引っかかって悩んでいる人が大勢いました。

なお、本現象も含めて、試験終了後に届いたSurveyで色々書いておきました。今後の受講者様が取り掛かる頃には改善されていることを祈っています。

私の事前知識

EXP-301コース受講時の私の知識状況です:

  • 英語の技術文書は、適宜単語を調べながらで読めました。そのためCourse MaterialやStudent Forum、Discordで話されている内容は、多少時間がかかりますが理解できました。
  • CTFのpwn/revジャンルで身につけた知識はありました:
    • x86アセンブリの読解はある程度できました。「C言語のstrcpy関数やmemcpy関数を素直にアセンブルしたらこうなる」なども想像がついていました。
    • IDAの使い方は身についていました。とはいえ普段は「クラウドベースの逆コンパイラ万歳」だったので、久々に逆アセンブル結果とじっくり向き合うことにはなりました。
    • WinDbgは使ったことがありませんでした。一方でx64dbg(=Windows環境向けデバッガー)や、gdb(=CUIデバッガー)は使用経験がありました。
    • x64 *nix環境を標的としたROPの実装経験はありました。一方でWindows x86プロセス向けのROPは組んだことがありませんでした。そのため「ROPで何を狙うのだろう」という興味が、受講開始時の最大の関心事でした。
  • SEHは聞いたことがある程度で、実際にどのように実現されているのかは知りませんでした。
  • Egghunterも初耳でした。

EXP-301コース受講記

私は90日間の期間でEXP-301コースを受講しました。予定のある日や、すぐに床に就きたかった日などは除いて、それなりの可処分時間を費やしたと思います。以下の流れで進めました:

  • コース期間開始直後に、Course Materialのダウンロードリクエストを出しました。押した直後にはもう、Course Materialをダウンロードできる状況になっていました。「Text」はPDFとして、「Videos」はmp4ファイルやアクセス用のhtml群としてダウンロードできました。PDFは約12MiB、約600ページでした。PDFをKindleに入れて、外出中でも一応読み進められるようにしました。
  • Textを読み進めつつ、実際に手を動かしました。各種Exerciseや、一部の章の最後に存在するExtra Mileにも取り組みました。Text最後に到達するまで160時間ほどかかりました。
    • 基本的にはExerciseとExtra Mileはすべて取り組みましたが、「DEP有効プロセスについてSEH OverwriteでRCEせよ」というExtra Mileは挫折しました。あれこれを一発でやるROP Gadgetが、探しても探しても見つかりませんでした……。
    • なお、Videosは序盤だけ聞いて、残りは全く聞きませんでした。「WinDbgでデバッグ中のプロセスを一度終了して、WinDbgも再起動して、WinDbgをアタッチし直す」までの約1分間が、何回も出てきたのが多分大きな理由です。とはいえ、「Lab VMへのRDP接続にはそのコマンドを使うのか」「ROPは「ろっぷ」、DEPは「でっぷ」、ASLRは「えーえすえるあーる」と読むんだ!」といった学びはありました。
  • OSED試験受験の予定を立てて、約1か月前に日程を予約しました。詳細は後述します。
  • Course MaterialのText完了後に、3台あるLabマシンを攻略しました。35時間ほどかかりました。
    • 脆弱性が見つからずに悩んでいる時間が長かったです。最後のChallengeは数日かけても脆弱性が見つからず、最終的にDiscordでヒントを探しました。
    • なお、使用するマシンはすべて各々の受講者専用マシンです。PEN-200コースでは他の人と同じマシンを同時に触っている状況がありましたが、EXP-301コースではそのようなことはありません。
    • PEN-200コースでは「Course Materialが終わった後のLabが本番」という風潮がありました。一方でEXP-301コースでは「LabはCourse Materialの延長線」という印象でした。Labマシンの台数の違いによるものかもしれません。
    • ところでStudent Forumの板構成はLabにマシンが4台あるかのような構成でした。以前はもう1台あったのかもしれません。
  • OSED試験時のレポート作成の練習として、Labマシンの攻略手順をレポートにまとめました。15時間ほどかかりました。
    • 前回のOSCP受験時と同様に、noraj/OSCP-Exam-Report-Template-Markdownを使いました。テンプレート中のOSED-exam-report-template_epi_v1でMarkdownファイルを生成して、OffSec社公式提供テンプレートと見比べて適宜調整しました。
    • 「High-Level summaryには何を書けばいいの?」や「エクスプロイト開発でのstep-by-stepな記述とはどのくらいの粒度?」などに大分悩みましたが、このくらいは必要だろうという項目はどうにか洗い出せました。
  • 監視ツールの動作確認等、試験の準備をしました。詳細は後述します。
  • EXP-301コース完了後すぐにOSED試験を受験しました。詳細は後述します。

今回も、EXP-301コースを進める途中で得られた情報は「チートシート」としてテキストファイルへひたすら書いていきました。WinDbgのコマンドや、セキュリティ機構の種類や突破方法などを書きました。そのファイルを見たら700行ほどありました。

なお、Discordを眺めていると「OSED試験の準備、対策、練習に役立つ」として以下の練習用バイナリが紹介されていました。私は余裕があれば取り組もうと思っていましたが、最終的にEXP-301コースだけでいっぱいいっぱいで練習用バイナリには取り組みませんでした。そのため練習用バイナリの難易度等は不明です。

OSED試験受験記

私の理解だと、OSED(OffSec Exploit Developer)試験とはEXP-301コースの内容を習得したことを証明するための試験です。FAQでもLabの知識が問われるといったことが書かれています。より具体的には、OSED Exam GuideOSED Exam FAQにある通り、OSED試験は以下の構成です:

試験前に上述のExam GuideやFAQを熟読することをおすすめします。なお、Exam Guideではtaskassignmentという表現が混在している点に注意してください。FAQではassignmentの表現で統一されています。レポート提出時に一緒に含めるPython3コードのファイル名でも、assignment1.py等を使用します。

OSED試験の申込み

  • OSED試験の開始日時は、ポータルページから予約を入れて指定します。空きがあれば、1時間単位で開始時間を設定できます。
    • 私は試験1か月前に予約しました。その時の「1か月後」の予約可能状況は1か月中10日ほどのみ選択可能で、かつ選択可能な日も1時間単位で選択肢が数個程度のみ、という大分埋まった状況でした。
      • 先の予定を決められる方は、早めに予約したほうが良さそうです。

試験の準備

試験の準備として以下のことをしました:

  • 監視ツールで使用するWebカメラの動作確認をしました。以前と同じく720pビデオを実現するロジクールC270 HDウェブカメラ(ノイズリダクションマイク搭載)を使いました。
  • 監視ツールの動作確認をしました。Proctoring Tool Manual – OffSec Support Portalにあるように、試験では監視ツールを使用して、Webカメラで受験者を映し、受講者が使用するモニターを画面共有をします。
    • 前述したようにProctoring部門へメールを出して、動作確認用のクレデンシャルをもらって動作確認しました。
    • なお、動作確認用のクレデンシャルは失効しやすいので注意してください。一度ログインに使用した時点で失効する上に、日本時間午前1時を超えても失効するようです。私はクレデンシャル発行を合計3回依頼しました……。
  • 試験時に使う部屋から、余計なものを運び出して部屋の外へ追いやりました。試験開始前の事前確認で、Webカメラで部屋を一周映すことになるためです。その際に変なものがあるとトラブルになるかもしれません。
  • 事前確認時で提示するパスポートについて、念のためスキャンを撮って、すぐに提示できるようにしました。実際に役立ちました。
  • メールボックスの整理をしました。これは、試験本番時に使用するVPN用ファイルがメールで届くためで、かつその間も監視セッション中(=画面共有中)であるためです。

OSED試験本番

私は、日本時間で昼の12時からの試験を予約しました。方針として、以下のことを考えていました:

  • 脆弱性を探すのは大変なので、脆弱性探しが不要な課題(脆弱性のPoCが提供されているもの?)から手を付ける。
  • 1時間くらいごとに休憩して、甘いものを食べたりする。
  • 食事や睡眠はちゃんと取る。
  • 前回のOSCP試験同様、Can I take breaks during the exam? – OffSec Support Portalに記載されているように、離席時や着席時は、チャットで連絡する。
    • 私は「I'm going to leave my seat and take a break.」「I have returned to my seat to resume the exam.」と書いていました。通じてくれていたみたいです。

実際の試験時は以下の流れになりました(細かい休憩や食事時間等は省略しています):

時刻(時分) 試験開始からの時分 内容
11:39 -00:21 proctoringセッションへログインしました。Webカメラの有効化や、画面共有の設定をしました。
11:45 -00:15 チャットで試験監督官から事前確認の指示が届き始めました。パスポートの提示時にWebカメラ越しでは読めないと言われたので、事前にスキャンしておいた内容を提示しました。その他は問題なく終わりました。
11:58 -00:02 事前確認が完了しました。試験開始時刻の12時まで待ちました。
12:00 00:00 メールが届きました。そのメールに、VPN接続用ファイルのDLリンクやクレデンシャル、試験で使用するコントロールパネルのURL等が含まれていました。
12:03 00:03 VPN接続に成功し、コントロールパネルへアクセスできました。
12:09 00:09 コントロールパネルに記載されている課題内容を一通り読み終えました。一番手を付けやすそうな課題から始めました。
17:27 05:27 1つ目の課題を完了しました。6時間かからずに1つ目を終えられたことに少し安心します。2つ目の課題に取り掛かりました。
22:45 10:45 2つ目の課題で行き詰まったので、一旦スキップして3つ目の課題を始めました。
02:52 14:52 3つ目の課題を完了しました。とりあえず合格圏内に入れたことに安堵しました。一旦眠ることにしました。
11:22 23:22 起きて、朝食等を済ませて、物理的に準備運動をしました。監視セッションが途切れていたようなので、試験監督官にどうすればいいかチャットで聞きました。
11:29 23:29 無事監視セッションを復旧できました。とりあえず達成できている課題2つについてざっくりレポートを書き始めました。これはスクリーンショットやコマンド等の取り忘れがあれば気付きたいと思ったためです。
16:27 28:27 完了した課題1つ目分のレポートをとりあえず書き終わりました。細かい文章表現等では山程TODOが残っていますが、大まかな流れや、レポートに要求されているものは書けていそうでした。完了した課題2つ目分のレポートを書き始めました。
22:27 34:27 完了した課題2つ目分についてもレポートをとりあえず書き終わりました。残りの、行き詰まっていた課題を再開しました。
02:54 38:54 行き詰まっていた箇所の突破口を見つけました!一旦眠るともう昼になりそうなので、寝ずに続けることにしました。
07:40 43:40 行き詰まっていた課題も最後まで完了しました!これで無事、3つすべての課題を達成できました。朝日が輝いて見えました。最後に完了した課題のレポートを書き始めました。
10:38 46:38 最後の課題分もレポートをとりあえず書き終わりました。各マシンをrevertして、レポートに書いてあるとおりの手順やコードでエクスプロイトできることの確認を始めました。
10:50 46:50 問題なくエクスプロイトできることを確認しました。要求されているスクリーンショットを念のため再撮影しました。残る時間は、念のため各課題の指示事項を数回読み直して見落としがないことを確認していました。
11:45 47:45 試験用VPNが切断され、監視セッションも終了しました。ここから24時間以内にレポートを提出する必要があります。
12:06 48:06 寝る準備をして床に就きました。体力が限界に近づいているのか、足元がおぼつかない感じがしました。
21:56 57:56 起きて、諸々を済ませました。とりあえずレポートの課題1から、細かい表現含めて確認を始めました。このときは「6時頃にはレポート提出できるだろうか」と思っていました。
01:28 61:28 レポートの課題1部分が完成しました。思ったよりも時間がかかっていて驚きます。
04:58 64:58 レポートの課題2部分が完成しました。
08:39 68:39 レポートの課題3部分が完成しました。レポート全体の体裁を整え始めました。
10:51 70:51 レポートが完成したということにしました。レポート提出期限まで残り1時間を切っているので、提出作業を始めました。
11:02 71:02 レポートを提出しました。レポート提出を受け付けたというメールがすぐに届きました。これでOSED試験全体が完了しました。
11:45 71:45 (このタイミングがレポート提出締切でした)

なお、前述した通りレポートはMarkdownで書きました。レポートはコードや画像込みで103ページ、それら抜きで38ページになりました。

ちなみにレポート提出にまたすぐに床に就いたのですが、お昼の温かい状況に適した格好で眠りについた結果、翌日明け方に寒さで目が覚めました。それが祟ったのか風邪を引き、翌1週間ほどひたすら寝て過ごす羽目になりました。皆様は体調管理にご注意ください。

合格通知

レポート提出から約5日後、We are happy to informから始まるメール、合格通知が届きました!デジタル合格証明書をAccredibleサービスに追加しました:

OSED合格証明書の一部

感想

脆弱性を探す部分が一番大変でした!Course Materialでは脆弱性の場所があらかじめ判明していたり、「このようにすると脆弱性があることが分かります」などのガイドがあったりしました。しかしLab以降では自分で脆弱性を探す必要がありました。「Import Tableに危険な関数があるか、さらにそれらの関数を危険な呼び出し方をしている箇所があるか」等の観点で探しましたが、到達する条件が複雑だったり、rep movsb等のアセンブリレベルの処理に脆弱性があって関数は未使用であるため探すのが大変だったりしました。

「脆弱性が分かって後はエクスプロイトを書けば完成」という段階になっても、ROPを組むのが大変なことがありました。ROP gadgetが少なく、どのレジスタを何の用途で使えば目的が達成できるのか悩むこともありました。最終的にROPを組み上げられたときは少し感激しました。

その他、ROPを何度も組んでいるうちに、「自分用コーディング規約」のようなものができてきました。ROPもプログラミングですね!なお、ROP gadgetの命名等はIUPAP Nomenclature of Pwnable - CTFするぞに従いました。ジョークがそれなりにある記事ですが、命名規約は本当に参考になります。

WinDbgはやっぱり独特でした:

  • 歴史的経緯が理由だと思いますが、私が思う「通常の用語」ではなく別の用語を使っていることがあり、ドキュメントを読むのも大変でした。
    • 「Step Into」ではなく「Trace」、「Step Over」ではなく「Step」。
    • 「Disassemble」ではなく「Unassemble」。
    • 「Hardware Breakpoint」ではなく「Processor Breakpoint」。
  • 組み込みコマンドは1~3文字であることがほとんどでかつ長い名称も無いので、覚えるのが大変でした。拡張コマンドでも!vprotなど省略形であることが多くて、それはそれで覚えるのが大変でした。
    • 例えば「今の関数からreturnするまで実行」(=gdbにおけるfinコマンド)はptコマンドです。
    • とはいえ何度も何度も実行しているうちに、ある程度指が覚えてくれました。
    • ちなみにgdbの場合は、短いコマンドでも入力できますが、長いコマンドでも入力できるので正式名称を覚えやすい気がしました。例えばgdbのcコマンドはcontinueの省略形です。
  • 数値リテラルがデフォルトで16進数として扱われるのが独特でした。最終的にはコマンドで使うあらゆるリテラルに0x0n接頭辞をつけることで対抗していました。nコマンドは切り替え忘れが怖いので使いませんでした。
  • コース期間中、別件でgdbを触る機会がありました。うっかり手癖でWinDbgの方のコマンドを入力、実行してしまうことがあり、面白くも大変でした。
  • 上でも書きましたが、Labのマシンに入っているWinDbgのバージョンが古いためfnstenv命令でEIPを取得できず、その後にAccessViolationでクラッシュする現象でドツボにはまっていました。そこまで到達できていればエクスプロイト開発は完了しているようなものなので、後はもうWinDbgを使わずに直接実行すればいい、ということに気付くまでしばらくかかりました。

コース中、Python3でx86アセンブリ言語コードをアセンブルするために、Keystone Engineというライブラリを使いました。ただそのライブラリの使い勝手が非常に悪かったです:

  • アセンブル結果がlist[int]であり、bytesではありません。単にbytesへ変換すればいいのであまり問題にはなりませんが、扱いづらさは感じました。
  • アセンブリコード中にうっかり\x00のバイトを含めてしまうとそこがコードの終端になります。コメント中などにうっかり含めてしまわないよう注意しましょう。もしくはraw stringsを使いましょう。
  • 構文エラーなどがあった場合でも、keystone.keystone.KsError: Invalid operand (KS_ERR_ASM_INVALIDOPERAND)keystone.keystone.KsError: Cannot find a symbol (KS_ERR_ASM_SYMBOL_MISSING)などのエラーメッセージにとどまります。どの行どの場所でエラーになったのかという情報が一切何もありません。多少変更するたびに再実行してエラーが出ないことを確認するくらいしか対応策はなさそうです……。
  • ラベル名箇所でコロンをつけ忘れたら、続く1行もろとも無視されたらしい結果になりました。アセンブル結果をobjdump等で逆アセンブルして検証することが必要かもしれません……。
  • 未対応の命令があるようです。試した範囲では、add [ebp+4], eaxmov dword [eax+ecx], 0xdeadbeaf等がKeystoneではエラーになりました。msf-nasm_shellではアセンブルできるのに……。
  • 今後もし自分で自作のx86アセンブリ言語コードをアセンブルすることになるなら、Keystoneは使わずに別の外部ツールを使うと思います……。

その他、細かい感想です:

  • 「このWin32APIにそういう用途あったんだ!」という驚きが2つありました。一方はMSDNのドキュメントをよく読めば「確かにできなくはなさそう……?」となりました。けれどももう一方はMSDNをよく読んでもなぜその用途で使えるのか分かりませんでした、まさに驚きです。
  • コース中で言及されますが、Windowsでもシステムコールを直接呼び出すことは一応できます。ただシステムコール番号がそんなにコロコロ変わっているとは知りませんでした。通りでシステムコール直接呼び出しを見ないわけです!
  • *nix系統システムだと、システムコールを簡単に呼び出せるのでROP組むのも楽なんだろうなあと思いました。Win32APIを呼び出す必要があると、スタック等で呼び出す内容を準備する必要があって大変でした。
  • リバースシェルを実行するアセンブリ言語コードを自作実装する章は面白かったです。ハンドルの継承ってこういう場面で使うんですね、かつあのハンドルメンバーにあの種類のハンドルを指定できたんですね、型からでは想像できませんでした!その他、PEBを巡るところで「この前提を置いていますが、ドキュメントにない挙動に依存していますし将来的に動作しなくなる可能性があるのでは?」と思う処理がありましたが、少なくとも現状では動きますし、将来変わったらその時に話題になるのでしょう……。
  • Extra Mileの1つに、自作アセンブリコードのアセンブル結果のバイト数をなるべく減らそうというものがありました。工夫しがいがあって面白かったです!コードゴルフに熱中する人の気持が少しわかった気がしました。
    • 例えばsub al, 1の場合は2C ibという専用命令があるので2バイト長になりますが、sub cl, 1等他のレジスタの場合は80 E9 01などの3バイト命令になります。eax/ecx系はしばしば専用命令あるんですね。
  • PDBファイルなしに、EXEファイル単体をKali Linuxへ持っていってIDAに読み込ませるだけで、関数名等のシンボル情報が表示されることがあって驚きました。PE構造を調べると、Debug DirectoryのTypeが1(=IMAGE_DEBUG_TYPE_COFF)でした。EXEにデバッグ情報が埋め込まれていることがあるんですね!
  • コース中でしばしば言及されますが、Windows XP以前などの古い環境ではWin32APIが常に同一アドレスに配置されていたため、エクスプロイトが簡単だったらしいです。DEPにしろASLRにしろ、OSのセキュリティ面での進化を感じます。
    • ROP対策のShadow Stackの普及状況って現在どういう状況でしたっけ、と調べると、/CETCOMPAT (CET Shadow Stack compatible)スイッチがVisual Studio 2019以降で搭載されているようです。そのスイッチが広まればエクスプロイトは困難になるんでしょうか。
  • コースで扱う某アプリケーションが、正規処理として「ペイロード内容をもとにCreateProcessAする」という脆弱とかいうレベルを超えた機能を実装していました。リモートからOSコマンドを実行し放題です!一体どうしてそんな実装になったのでしょう……。
  • 何はともあれ、C言語やC++言語を使ってプログラム全体を安全に実装するのは人類一般には早いのでは、Rust言語などが流行るのも自然の道理なのでは、というのが一番の感想でした。
    • 各種セキュリティ機構を簡単にコンパイル時付与できる現在では、stack buffer overflow + jmp espだけでRCEできることなんて早々無いと思っていたのですが、CVE-2023-32560のPoCなどで今なお時折存在するようです……。

OffSec社が提供する他のコースを眺めていると、EXP-401コースが面白そうです。ただそのコースだけはオンライン上では受講できず、世界各所のイベント開催時でのみ受講できるようで、受講すること自体のハードルが相当高そうです……。他の300番台コースはオンラインで受講できますし面白そうなので、そのうち挑戦してみたいです。

おまけ: 検証用コードの紹介

エクスプロイトを開発する際、確認するべきことが色々あります。人力確認だと見逃しが発生しやすく、結果ドツボにはまりかねないので、機械的に検証できることはそうしました。検証に使ったコードを紹介します。

bad Characters確認用コード

「Bad Characters」という概念があります。入力を終端させたりプログラムに異なる挙動を引き起こしたりするため、入力に使えないバイトを指します。中には、サーバー側の処理の都合で異なるバイトに変化させられるためBad Charactersになるバイトも存在します。例えばサーバーが入力をURLデコードする場合は、0x2b0x20へ変化させます。そのような変化が起こる場合は人力での目視確認では見逃しやすいので、機械的に確認するためのコード片です:

badchars = b"\x00"
testchars = bytes(set(range(256)).difference(badchars))

def check_bad_chars(testchars):
    db_command_output = """
    上記testcharsを含んだペイロードを送信して、WinDbgのdbコマンド確認した結果を、ここに貼り付ける
    """
    appeared = set()
    for line in db_command_output.split("\n"):
        line = line.strip()
        if len(line) == 0: continue
        line = line.split("  ")[1].strip()
        for v in line.replace("-", " ").split(" "):
            appeared.add(int(v, 16))
    ok = True
    for c in testchars:
        if c not in appeared:
            ok = False
            print(f"[-] \\x{c:02X} may be badchar!")
    if ok: print("[+] All bad characters are detected!")
check_bad_chars(testchars)

assert all(map(lambda b: b not in buffer, badchars))

ROPチェーン構築用クラス

ROPチェーン構築時でもBad Charactersはついて回ります。うっかりBad Charactersを含んだペイロードを送信してしまうと、厄介なデバッグをするハメになります。一般にバグの検知は早ければ早いほどよく、ペイロードの構築時に気付くことができれば原因も分かりやすいです。そういう理由で作ったROPチェーン構築用補助クラスです:

import struct
def p32(x:int)->bytes: return struct.pack("<I", (x + 0x1_0000_0000) % 0x1_0000_0000)
def to_hex(value:bytes)->str: return "".join(map(lambda b: f"\\x{b:02x}", value))
class RopChain:
    def __init__(self, badchars):
        self._badchars = bytes(badchars)
        self._buffer = bytearray()
        self._alignment = 4
    def append(self, value):
        append_bytes = p32(value) if isinstance(value, int) else bytes(value)
        if any(map(lambda b: b in self._badchars, append_bytes)):
            raise ValueError(f"{to_hex(append_bytes)} contains one of badchars {to_hex(self._badchars)}")
        if len(append_bytes) % self._alignment != 0:
            raise ValueError(f"{len(append_bytes) = }, which is not align of {self._alignment}")
        self._buffer.extend(append_bytes)
        return self
    def as_bytes(self)->bytes: return bytes(self._buffer)
    def __str__(self)->str: return str(self._buffer)
    def __len__(self)->int: return len(self._buffer)

以下のように使います:

rop_pop_eax = 0xdeadbeef
ropchain = RopChain(b"\x00")
ropchain.append(0x12345678) # 4バイト整数を追加できます
ropchain.append(b"xxxx") # bytesも追加できます、pop単位に合わせるため4バイト単位に限定しています
ropchain.append(rop_pop_eax).append(-1) # p32関数で負の数も処理できるようにしています
# ropchain.append(0) # これはbad charactersを含みますが例外が出るので気付けます
payload = ropchain.as_bytes()
# あとはpayloadを含めて送信します