どうも。
全国大会のCクラスに参加、修了したので応募課題での提出物を晒させていただきます。
その前にいくつか重要なことを。
- 課題は全部むずい
- 最後の一問だけやってラスボスを倒した気になっていたら負けます
- 問題文はちゃんと読みましょう
- 言われた事と若干違うことをやっている...
- この晒しには間違いも含まれている
- 仕方がないね
- 実際に全国大会に参加して間違いに気付けた分もあります
それ以外の事は他の晒しや問題文自体に書いてあるので、まぁ頑張ってください。
課題
応募課題は此処に在ります。
https://www.ipa.go.jp/jinzai/security-camp/2024/camp/zenkoku/m42obm0000002mzw-att/classC.txt
1 2 3
<こういうのは自分で考えるんだよ>
1に関しては、講師の方々について調べて、発表資料とかを読みウケがよさそうな話題を探して取り入れました。
効果があったかは不明です。
4
選んだ記事
https://pentest-tools.com/blog/xz-utils-backdoor-cve-2024-3094
選んだ理由
XZ Utilsは、Linuxディストリビューションの多くでデフォルトでインストールされている。今回発見された脆弱性は、「Jia Tan」と呼ばれる一人のOSSコントリビュータによって埋め込まれたバックドアであり、OSSへの攻撃という観点から全世界に衝撃を与えた。また、手口の巧妙さと粘り強さ、活動の秘匿性から国家の支援を受けたグループ又は国家自身によって作成されたペルソナであるという意見も存在する。Linuxは一般的なユーザーだけでなく、サーバーや組み込みシステムなど、インターネットのインフラを支える多くのシステムで使用されている。したがって、Linux上で動作するソフトウェアの一部に脆弱性が存在すると、その影響はインターネット全体に及ぶ可能性がある。これは、情報通信インフラの安全性を脅かすだけでなく、経済活動や社会生活全体に影響を及ぼす可能性がある。
この記事にはJia Tanの活動やXZ Utils Backdoorについて詳細な情報が記載されている。今回の攻撃を理解するのに必要な情報が十分に含まれていると感じたため選んだ。
技術的詳細
このバックドアはCVE-2024-3094というCVE番号が割り当てられた。リモートからコードを実行するための複数段階のマルウェアを使用する。
- まず、ビルド時に実行されるコマンドに
trコマンドを挿入することで、テスト用の破損したアーカイブファイルを正常なファイルに復元する。これによってbashスクリプトがビルド環境にdropされる。 - 次にbashスクリプトは別のテスト用ファイルから次のbashスクリプト抽出しdropする。
- このbashスクリプトもテスト用データから特定のシグニチャを持つファイルを探索し、
grepとcutを用いてデータを抽出し、アーカイブファイルとして再び抽出する。また、sedとawkを用いて特定のデータをRC4で復号する。最終的にそのデータを再び圧縮ファイルとして抽出し、liblzma_la-crc64-fast.oとしてdropする。
このオブジェクトファイルは最終的にビルドされるxz utilsに含まれることになる。
このコードはglibcの機能で、間接的な関数呼び出しを可能にするGNU_IFUNCを利用して、RSA_decrypt関数をフックする。これはOpenSSHの認証ルーチンの実行時のフック/リダイレクトを実行するのに使われる。
このフックを介して、バイナリはRSA構造体内で与えられた公開鍵の公開余剰(N)を調べ、ハードコードされた復号キーでデコードし、ED448楕円曲線署名アルゴリズムを使用してデータの有効性をチェックする。この公開鍵が攻撃者(Jia Tan)のものと一致すれば、SSHの認証ペイロードに指定されたデータがsystem()を通して実行される。
つまりこれはリモートコード実行の脆弱性である。
被害規模又は影響範囲
このバックドアは、オープンソースプロジェクトのサプライチェーンを通じて配布され、多数のシステムに影響を及ぼす可能性があった。
幸いにも各ディストリビューションの安定版リリースに組み込まれる前に発見された。そのため、ソースコードから直接ビルドしたり早期リリースを利用しているユーザーのみに影響している
対策
プロダクト環境や重要な拠点では、安定版リリースの使用する。
また、オープンソースプロジェクトのメンテナンスに対する資金提供が考えられる。オープンソースプロジェクトは、その開発とメンテナンスがボランティアによって行われていることが多く、その資金や人材の不足は、プロジェクトのセキュリティを脅かす可能性がある。実際にOpenSSLでは資金不足などが原因でheartbleed脆弱性が発生したと言われている。今回の件とは少し違うが、それでもコードを監視しメンテナンスを行うには人も金も必要なのが現実である。
しかし、近年では技術の発展によりAIがソースコードを理解できるようになっている。コミット内容の監査し脆弱になりうるコードを自動で発見できる時代が必ず来る。また、コミットの監査も含めてユーザーの属性やアクティビティを元に信用度を計算できるようになれば、その情報を元に他のメンテナが危険なユーザーに意識を向けることも出来る。
5
問5-1
TCP SYNスキャン
まず、目的ポートに対してSYNパケットを送信する。ポートが開いている場合、ターゲットはSYN/ACKパケットを返すので、これで判断することができる。スキャナーはこのパケットを受け取ったら、通常はACKパケットを送信するが、代わりにRSTパケットを送信して接続をリセットする。ポートが閉じている場合はRSTパケットが返される。ファイアウォールなどによりfilterされている場合は何も帰ってこない。これにより、完全な接続が確立されることなくポートの状態を判断できる。実際にnmapではscan_engine_raw.ccのsendIPScanProbe関数内のbuild_tcp_rawの呼び出し時にフラグを設定することでそれぞれの通信を実現している。
TCP SYNスキャンではハンドシェイクを完了しないため高速で、対象のサーバーアプリケーションとは接続を確立しないのでアプリケーションのログには残らない。ただし、ファイアウォールやIDS/IPSなどのネットワークセキュリティデバイスによって検出される可能性がある。
TCP Connectスキャン
一般的なTCPのハンドシェイクと同じ方法で接続を行う。具体的には、目的ポートに対してSYNパケットを送信し、ターゲットがSYN/ACKパケットを返すと、スキャナーはACKパケットを送信してハンドシェイクを完了する。その後、スキャナーはFIN/ACKパケットを送信してFIN/ACKパケットを受信し、ACKパケットを送信して接続を終了する。ポートが閉じている場合はRSTパケットが返される。ファイアウォールなどによりfilterされている場合は何も帰ってこない。nmapではscan_engine_connect.ccのsendConnectScanProbe関数内で一般的なTCP接続を確立することで実現している。
接続が完了したことが条件になるので、擬陽性が少ない。その反面ログが残りやすく、検出されやすい。また、TCP SYNスキャンよりも多くの時間がかかる。
実験
実際にnmapを用いてTCP SYNスキャンとTCP Connectスキャンを比較する。
Azureを利用してnmapを実行するクライアント(kali)とSSH Apache telnetdを動作させるサーバー(Ubuntu)を用意する。
クライアントは日本で、サーバーはアメリカを指定する。
SSHはTCP22 telnetdはTCP23 ApacheはTCP80番のポートを利用し、インターネットからUbuntuに対して転送するようにする。また、追加でポート443をインターネットから転送するようにする。
TCP SYNスキャンのコマンド(結果は一部省略)
$ nmap -sS -p1-65535 -Pn --max-scan-delay 0 -v Serverのアドレス
PORT STATE SERVICE
22/tcp open ssh
23/tcp open telnet
80/tcp open http
443/tcp closed https
scanned in 159.04 seconds
サーバー側のrsyslogには何も追加されなかった。
TCP Connectスキャンのコマンド(結果は一部省略)
nmap -sT -p1-65535 -Pn --max-scan-delay 0 -v Serverのアドレス
PORT STATE SERVICE
22/tcp open ssh
23/tcp open telnet
80/tcp open http
443/tcp closed https
scanned in 160.44 seconds
サーバーのログには次の内容が追加された。
May 19 16:04:17 server inetd[4837]: could not getpeername
May 19 16:04:17 server inetd[4837]: could not getpeername
May 19 16:04:27 server inetd[4837]: could not getpeername
また、時間をより正確に計測するために追加でそれぞれ2回スキャンを行う。
TCP SYNスキャンにかかった時間(秒)
- 159.04
- 159.21
- 159.15
TCP Connectスキャンにかかった時間(秒)
- 160.44
- 162.11
- 161.72
以上の結果からTCP SYNスキャンとTCP Connectスキャンでは同じ結果が得られることが分かった。また、TCP Connectスキャンではサーバーにcould not getpeernameという奇妙なログが残された。このことから検知されやすくなることが分かる。
ただし、予想と違い実行速度には1、2秒の差しか見られなかった。実行速度に変化がなかったのは、動作の違いがターゲットからSYN/ACKパケットが返された後にあるからである。ポートが開いている場合はTCP SYNスキャンではRSTを送信し、TCP Connectスキャンでは通常のハンドシェイクを行う。しかし、ポートが閉じていたりファイアウォールでフィルターされている場合は、TCP SYNスキャンもTCP Connectスキャンもどちらも同じ動作をすることになる。つまり何もしない。
まとめると、速度の変化が現れるのはポートが開いている状態の場合のみである。なので全てのポートをスキャンするような場合には、スキャン速度には影響が少ない。
問5-2
短時間に大量の接続を行うということは、内部から外部に向けて大量のSYNパケットを送信することである。大量のTCPセッションを確保することによって、サーバー側に対してSYN Flood状態を引き起こし、処理速度の低下や新規接続が行われなくなる。
また、内部ネットワークではSYNパケットの到着とともにNATエントリを作成する必要がある。その結果、内部ネットワークのルーターのNATが枯渇し、輻輳によってネットワーク全体の速度が低下したり、新規接続が行われなくなる。
もしこれが社内や学内で発生すれば、内部ネットワークが麻痺するので他のユーザーが通信できなくなってしまう。
この仮説を検証するためにGNS3を用いてルーターとローカルネットワークをエミュレートする。
Client(Ubuntu)からRouterを通して、インターネットを模したSwitch経由でServer(Ubuntu)に対してポートスキャンを行うことにする。
[Ubuntu(Client)] -FastEthernet0/0- [Router] -FastEthernet0/1- [Switch] --- [Ubuntu(Server)]
まず相互の通信を可能にするために、インターフェースとNATを設定する。Router内で次のコマンドを実行する。
enable
configure terminal
interface FastEthernet0/0
ip address 192.168.1.1 255.255.255.0
ip nat inside
no shutdown
exit
interface FastEthernet0/1
ip address 10.0.0.1 255.255.255.0
ip nat outside
no shutdown
exit
ip nat inside source list 1 interface FastEthernet0/1 overload
access-list 1 permit 192.168.1.0 0.0.0.255
end
実験を容易にするために追加でNATテーブルのエントリ数上限を変更する。ここでは最大10エントリにする。
ip nat translation max-entries 10
R1# show ip nat statistics
(省略)
max entry: max allowed 10, used 0, missed 0
次に、ClientとServer側でもインターフェースとルーティングの設定を行い、ルーターと通信できるようにする。
Client(Ubuntu)内で次のコマンドを実行する。
ifconfig eth0 192.168.1.2 netmask 255.255.255.0
route add default gw 192.168.1.1
Server(Ubuntu)内で次のコマンドを実行する。
ifconfig eth0 10.0.0.2 netmask 255.255.255.0
route add default gw 10.0.0.1
これで環境構築は完了である。
次に実験を開始する。
まず、ClientからServerに対してpingを送信する。
$ ping 10.0.0.2
PING 10.0.0.2 (10.0.0.2) 56(84) bytes of data.
64 bytes from 10.0.0.2: icmp_seq=1 ttl=63 time=14.3 ms
64 bytes from 10.0.0.2: icmp_seq=2 ttl=63 time=24.5 ms
...
この状態でルーター内でのNATテーブルは次のようになっている。
R1# show ip nat translations
Pro Inside global Inside local Outside local Outside global
icmp 10.0.0.1:1024 192.168.1.2:46842 10.0.0.2:46842 10.0.0.2:1024
このことから適切にネットワークが設定され通信が行われていることが分かる。
次にClientからServerに対してポートスキャンを行う。
これはServerに対して1から65535までのポートを1秒間隔でスキャンする。低速ではあるがNATがポートスキャンによって枯渇することを証明するためなので問題ない。
$ nmap -v 10.0.0.2 -p 1-65535 --scan-delay 1s &
この状態でしばらく放置する。
その後のルーター内のNATテーブルは次のようになっている。
R1#show ip nat statistics
Total active translations: 10 (0 static, 10 dynamic; 10 extended)
(省略)
R1# show ip nat translations
Pro Inside global Inside local Outside local Outside global
tcp 10.0.0.1:4104 192.168.1.2:52694 10.0.0.2:2055 10.0.0.2:2055
tcp 10.0.0.1:4096 192.168.1.2:52694 10.0.0.2:15264 10.0.0.2:15264
(省略)
tcp 10.0.0.1:4099 192.168.1.2:52694 10.0.0.2:58709 10.0.0.2:58709
tcp 10.0.0.1:4097 192.168.1.2:52694 10.0.0.2:65176 10.0.0.2:65176
このように最大NATエントリ数までNATエントリが存在しており、NATが枯渇している。
この状態でClientからServerにpingを送信する。
$ ping 10.0.0.2
PING 10.0.0.2 (10.0.0.2) 56(84) bytes of data.
From 192.168.1.1 icmp_seq=1 Destination Host Unreachable
From 192.168.1.1 icmp_seq=2 Destination Host Unreachable
...
通信が行われなくなった。
nmapを停止したあとにルーターでclear ip nat translation *を実行するか暫く待つかをするとNATテーブルかクリアされる。
その状態だとまたpingが送信できるようになる。
$ ping 10.0.0.2
PING 10.0.0.2 (10.0.0.2) 56(84) bytes of data.
64 bytes from 10.0.0.2: icmp_seq=1 ttl=63 time=18.2 ms
64 bytes from 10.0.0.2: icmp_seq=2 ttl=63 time=18.2 ms
...
実験より、高速なポートスキャンによってNATが枯渇し、内部から外部へ接続できなくなることが判明した。
6
問6-1
イメージファイルimage.ddに存在するファイルシステムをmountコマンドまたは同等の機能を持つユーティリティでマウントできるようにしてください。
その過程でイメージファイル内のどの部分をどのように変更したのか、なぜそれでマウントできるようになるのかを簡潔に解答してください。
まず、任意のバイナリエディタを用いて0x00100438と0x00100439に0x53 0xEFを上書きする。
0010 0430: AF C8 EE 65 01 00 FF FF 53 EF 01 00 01 00 00 00
次に、管理者権限で次のコマンドを順に実行する。
losetup -fP image.dd
losetup
BACK-FILEがimage.ddのloopデバイスを覚えておく。
ここでは/dev/loop0だとする。
次に、管理者権限で次のコマンドを順に実行する。
mount /dev/loop0p1 /mnt/dir_name
これでマウントできた。
❱ ls
here.txt lost+found rootd
❱ cat here.txt
Here is my private directory
解説
まずfileコマンドを用いてファイルが何なのか調べる。
❱ file image.dd
image.dd: DOS/MBR boot sector; partition 1 : ID=0xee, start-CHS (0x0,0,2), end-CHS (0x3ff,255,63), startsector 1, 2097151 sectors, extended partition table (last)
何らかのパーティションが含まれていることが分かるが、それ以上はわからない。
次にblkidコマンドで調べる。
❱ blkid image.dd
image_true.dd: PTUUID="e83a394f-0bfe-47f9-9837-763df6404f58" PTTYPE="gpt"
GPT形式であることがわかる。
また、testdiskコマンドのログを確認する。
Filesystem created: Mon Mar 11 17:44:14 2024
Last mount time: Mon Mar 11 17:46:52 2024
Linux 524424 2617479 2093056
ext4 blocksize=4096 Large_file Sparse_SB Recover, 1071 MB / 1022 MiB
This partition ends after the disk limits. (start=524424, size=2093056, end=2617479, disk end=2093056)
パーティションの位置がずれているように思える、がここでは関係ない。このパーティションのファイルシステムの形式はext4だと考えられる。
しかし普通にマウントしようとしても
wrong fs type, bad option, bad superblock on /dev/loop0p1, missing codepage or helper program, or other error.
のように表示される。
そこでこのパーティションのバイナリデータをExt4 Disk Layout - Ext4 (kernel.org)を参考に確認したところ、The Super Blockのs_magicが0x00に上書きされていることが判明した。なのでこれを元に戻すとマウントできるようになる。
問6-2
問6-1でマウントしたファイルシステム形式の名称(例:FAT16)と、そう判断した理由を簡潔に解答してください。
ext4
解説
問6-1の解説の内容と同じ。
データの形式がext4の物であることからもext4なのは確定である。
問6-3
イメージファイルimage.ddに存在するファイルシステムをマウントしてもそのままでは通常の方法で中身を参照(マウントしたOS上でファイルをダブルクリックするなどして、ファイルのフォーマットに対応したアプリケーションで中身を表示)できないファイルが存在します。そのファイルを、マウントしたOSから通常の方法で参照できるようにしてください(※ファイルを抽出するのではなく、参照ができる状態に戻してください)。
その過程でイメージファイル内のどの部分をどのように変更したのか、なぜそれで参照できるようになるのかを簡潔に解答してください。
/rootd/websites/jp-cr-deloitte-cyber-trends-and-intelligence-report-2023.pdfに対して操作をしようとしてもBad message (os error 74)のようなエラーが発生する。
パーティション1の0x9233Cの位置に0x00 0x84 0x00を上書きする。
0x0009 2330: 00 00 00 00 00 00 00 00 C0 01 00 00 00 84 00 00
この状態でマウントすることでエラーが発生し無くなる。
解説
まずfsck.ext4コマンドでパーティションをチェックする。
❱ fsck.ext4 -c /dev/loop0p1
e2fsck 1.46.5 (30-Dec-2021)
badblocks: invalid last block - -
/dev/loop0p1: Updating bad block inode.
Pass 1: Checking inodes, blocks, and sizes
Inode 20 has an invalid extent
(logical block 0, invalid physical block 4473154, len 448)
Clear<y>?
その結果inode20が不正になっていることが判明する。
該当のpdfファイルのinodeも20である。
❱ find -inum 20
./jp-cr-deloitte-cyber-trends-and-intelligence-report-2023.pdf
debugfs: imap <20>
Inode 20 is part of block group 0
located at block 146, offset 0x0300
であり、ブロックサイズは4096byteであるから4096*146 + 0x300 = 0x92300の位置にこのinodeデータ構造が存在する。
このinodeのi_blockの情報を確認すると、Extent pointsが0x42 0x41 0x44 0x00(ASCIIでBAD)になっている。
この部分を修正する必要がある。
ここで、既にわかっている情報として0x9237Cと0x9238Cにある、このinodeのチェックサム(0x9FCFA7F0)がある。
この情報をもとにExtent pointsを総当たりしチェックサムに合致するデータを探索することで、元の情報を復元できると考えた。
inodeのチェックサムはFS UUID、inode番号、inode構造体自身から算出されるCRC32である。
https://github.com/tytso/e2fsprogs をもとにdebugfsのソースコードを変更する。
csum.c内のext2fs_inode_csum_verify関数内に次のコードを挿入して実行したところi=5の時にBADが現れた。
for (i = 0; i < EXT2_N_BLOCKS; i++) {
printf("block %x\n", inode->i_block[i]);
}
よってi_block[5]を総当たりするようにext2fs_inode_csum_verify関数を書き変える。
...
provided = ext2fs_le16_to_cpu(inode->i_checksum_lo);
for (i = 0; i < 0xffffffff; i++) {
inode->i_block[5] = i;
retval = ext2fs_inode_csum(fs, inum, inode, &calculated, has_hi);
if (retval)
return 0;
if (has_hi) {
__u32 hi = ext2fs_le16_to_cpu(inode->i_checksum_hi);
provided |= hi << 16;
} else
calculated &= 0xFFFF;
printf("i: 0x%08x\n", inode->i_block[5]);
printf("provided 0x%08x\n", provided);
printf("calculated 0x%08x\n", calculated);
if (provided == calculated)
return 1;
}
/*
* If the checksum didn't match, it's possible it was due to
...
iが0x8400の時にチェックサムが一致した。
よって0x00 0x84 0x00 0x00を書き込むことでinode 20に対して正常にアクセスできるようになる。
実際にパーティション内の0x8400 * 4096 = 0x8400000の位置にはPDFファイルの実体が存在している。
7
問7-1 選んだ技術はどんなものですか?
今後発展し、攻撃の対象としても注目される技術として、AR(拡張現実)やMR(複合現実)デバイスを挙げる。特にMR技術は近年急速に進化しており、Appleから発売されているVision Proは特に話題になっている。これらのデバイスは、ユーザーの視界にデジタル情報を重ねることで、現実と仮想の境界を曖昧にする。将来的にスマートフォンのように普及し、日常的に利用されることが予想される。
問7-2 その技術が脆弱になり得るのはどんなときだと思いますか?
ARやMRデバイスが脆弱になる場面は多岐にわたる。特に、人間の認知内容を操作することに繋がるので、コンピュータやシステムへの攻撃というよりも人間の認知自体が攻撃対象となり得る点が特徴的である。
一つの攻撃シナリオとして、悪意のあるアクターがデバイスに侵入して、ユーザーに見せる情報を改竄するケースが考えられる。これによってユーザーは攻撃者が用意した状態をそのままの形で認識することになる。例えば、車の運転中に災害や事故が発生したかのような映像・音を出力させることで、ドライバーに誤った行動を取らせ、実際の事故を誘発することが可能である。
また、ARやMR機器というのは多数のセンサーを搭載している。これらのセンサー類、例えばカメラなどに対して、特殊な光や音波を照射することで誤作動を起こせると考える。Apple Vision Proでは視線と指の動作を元に操作を行う。そこで、視線や指の位置を誤認させることで、操作を乗っ取る攻撃が考えられる。また、現実の環境に偽の情報をプロジェクションしたり、カメラに直接投影したりするケースも考えられる。これによって偽のUIを表示し、ユーザーに意図しない操作を行わせられる。これはMR版クリックジャッキングだと言える。
問7-3 その技術に対して攻撃することを考えたときに、思いつく障壁は何ですか?
ARやMRは基本的には、一般的な無線規格や有線接続でPCやインターネットに接続される。一般的なIoTデバイスやサーバーと同様に、ファイアウォールや暗号化プロトコル(TLS/SSL)で保護されている。そのため、IoTデバイスなどへの攻撃と同様に脆弱性やmisconfigurationを探して侵入する必要がある。また、センサーへの攻撃も複雑であり、特定の条件下でのみ成功する可能性がある。
例えば、任意の情報を表示させる攻撃は難易度が高く、特にメニュー表示中に周囲の映像が消える設計であれば、MR版クリックジャッキングのような攻撃も防ぎやすい。特に、操作時にユーザーが意識的に確認するためのフィードバックメカニズムが組み込まれている場合、クリックジャッキングのような攻撃は困難になる。
8
若干変更しています。
利用されたトリック
コンパイル時にgccのオプションで-Wl,-z,lazyを指定してlazy bindingを有効化したうえで、実行ファイルの.rela.pltのstrncmpとstrcmpのr_offsetを入れ替えている。
解答に至るまでの調査
状況確認と静的解析
まず、与えられたバイナリをCutterを用いてデコンパイルする。
main関数のデコンパイル結果は次のようになった。
undefined8 main(void)
{
int32_t iVar1;
char *s1;
char *s2;
iVar1 = strcmp(0x8f4, 0x907, 0xd);
if (iVar1 == 0) {
iVar1 = strncmp(0x8f4, 0x907);
if (iVar1 == 0) {
puts(0x919);
} else {
puts(0x91f);
}
} else {
puts(0x939);
}
return 0;
}
なぜかstrcmpが3引数になっている。また、strncmpが2引数になっている。このことから、strcmpとstrncmpが入れ替わっているという予想を立てた。
逆アセンブリ結果を抜粋する。
lea rax, str.Reversing_is_cool. ; 0x8f4
lea rax, str.Reversing_is_fun. ; 0x907
lea rdi, str.Welcome_to_Security_Camp ; 0x91f ; const char *s
0x8f4がReversing is cool.で0x907がReversing is fun.である。たしかにstrncmp(0x8f4, 0x907, 0xd)は0xd文字の比較で結果が0になり、strcmp(0x8f4, 0x907)は0以外になってWelcome to Security Camp!が表示されることになる。
strcmpが3引数として認識されたのは、strcmpの呼び出しが
mov edx, 0xd
mov rsi, rcx
mov rdi, rax
call strcmp
となっているからである。x86_64の呼び出し規則によると、rsi、rdi、rdx(edx)はそれぞれ第1、第2、第3引数と指定されている。(System V Application Binary Interface: AMD64 Architecture Processor Supplement (With LP64 and ILP32 Programming Models) Version 1.0より)
これが原因であると考えた。
表層解析
checksecを用いてhackファイルを調査する。
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
Partial RELROで、canaryが無いことが判明した。これらが今回の問題に対する要因となっていると考えた。
例えば、canaryが無いことから、内部で意図的にバッファオーバーフローなどを起こして実行する命令を変更していると考えた。しかしstrcmpの呼び出しが元から3引数で行われていることもあり、可能性は低い。また、デコンパイル結果にもそのようなコードは見られなかった。(もちろんデコンパイラを完全に信用してはならない。)
動的解析
ltraceコマンドを用いて動的解析を行う。
ltrace ./hack
__libc_start_main(0x559a3e0007f0, 1, 0x7ffcd0102fa8, 0x559a3e000870 <unfinished ...>
strncmp("Reversing is cool.", "Reversing is fun.", 13) = 0
strcmp("Reversing is cool.", "Reversing is fun.") = -3
puts("Welcome to Security Camp!"Welcome to Security Camp!
) = 26
+++ exited (status 0) +++
strncmpが先に実行され、その次にstrcmpが実行されている。このことから、実際の動作もstrncmpとstrcmpが入れ替わっていることが確認できた。(これは誤りである事が後に判明する。)
次にgdb(pwndbg)を用いて解析を行う。gdb上でもstrcmpとstrncmpが逆になっている状態であった。
ステップ実行で動作を確認したところ、main関数のstrcmpを呼び出し部分
0x555555400821 <main+49>: call 0x555555400670 <strcmp@plt>
の呼び出し先
0x7ffff7f2d3d0 <__strncmp_avx2>: cmp rdx,0x1
とstrncmpになっていることが判明した。通常のstrncmpではなくavx2になっているのは、SIMD命令によって高速化を行うためだと思われる。
これ以上は自分の知識だけではどうにもならないので調べることにする。
その結果、呼び出される命令がアセンブリの結果と異なるのはPLTやGOTの書き換えの可能性があることが判明した。
readelfとobjdumpコマンドを利用して、これらのセクション情報を確認する。(出力は省略)
readelf -r hack
objdump -d -j .plt hack
ここで致命的な問題に遭遇する。知識が足りず出力が読めない。
そこで文献を当たり、関連していそうな知識を身に着けることにする。gotやpltの書き換えだと考えているので次のような資料を読んだ。
- https://book.hacktricks.xyz/binary-exploitation/arbitrary-write-2-exec/aw2exec-got-plt
- https://zenn.dev/ri5255/articles/f61dcc5c7ffd9f
- https://systemoverlord.com/2017/03/19/got-and-plt-for-pwning.html
- https://syst3mfailure.io/ret2dl_resolve/
- 大学で配られた講義資料
この段階でようやく、動的再配置によって.rela.pltのオフセットから関数の名前を調べ、関数アドレスが検索されGOT(.got.plt)内の該当箇所にアドレスを書き込むという仕様がなんとなく理解できた。そして2回目以降は関数アドレスは検索されずにGOTからアドレスを取得することも。
また、https://news.ycombinator.com/item?id=39911311 ではGNU_IFUNCという再配置時の間接的な関数の仕組みによって、関数の処理が変更されるという話があった。これは今話題になっているxz backdoorに用いられている手法であり、応募課題として出される可能性が十分ある。だが、まずはPLTとGOTについて深く調べることにする。
再びCutterを用いて逆アセンブリを確認する。PLTが怪しいということで、pltセクションでどのように再配置のアドレスが取得されるかを調べる。
Cutterでステップ実行を行いplt内でのstrcmpの呼び出し時の処理を追う。strcmpが実行されるときはpltセクション内でgotを参照し、またpltに戻って0をpushしてからplt0に飛ぶ。この時に解決されるアドレスはstrcmpではなくstrncmp(strncmp_avx2)のものになっている。
strncmpが実行されるときは、strncmpを呼び出すのは見た目上では最初なのにもかかわらず、GOT内にすでにstrncmpのアドレスが格納されているので、そのままstrncmpが実行される。
int strcmp(const char * s1, const char * s2);
0x5625f4800670 jmp qword [strcmp] ; 0x5625f4a01018ここが0x5625f4a01018->0x5625f4800676になる つまり下のpush
0x5625f4800676 push 0
0x5625f480067b jmp .plt ; section..plt このジャンプ先でまた飛んで__ strncmp_avx2のアドレスが算出されてさらにジャンプする
int strncmp(const char * s1, const char * s2, size_t n);
0x5625f48006a0 jmp qword [strncmp] ; 0x5625f4a01030 ここが0x5625f4a01030->__strncmp_avx2になる 下は呼び出されない
0x5625f48006a6 push 3 ; 3
0x5625f48006ab jmp .plt ; section..plt
つまり、実際の動作ではlibc内のstrcmpは一度も呼び出されていない。
実際にmain関数の終了時のGOTの内容をCutterで確認したところ、strcmpのアドレスはpltセクション内を指しており、strncmpのアドレスはstrncmp_avx2のアドレスを指していた。
次に、なぜstrcmpのアドレスを解決しようとしてstrncmpのアドレスが得られたのかを調査する。Cutterが絶妙に使いにくかったのでここからはgdbのみを利用する。
まず、strcmpのアドレスを解決する場合の処理を追う。_dl_fixup内でstrcmp@pltを呼び出してるにもかかわらず、 _dl_lookup_symbol_xへの引数がstrncmpになっている。これは0x555555400418 + 0xbに位置する文字列であり、.dynstrセクションの文字列である。
そこで、.dynstrセクションの書き換えを疑った。そこで、実行ファイルの.dynstrセクションの文字列を書き変えたところ、デコンパイル結果もstrcmpとstrncmpの値が入れ替わったが、実際の動作まで入れ替わりWhat's wrongを表示されてしまう。つまりこれではない。
また、.dynsymの書き換えも疑った。.dynsymセクションのst_name(.dynstrのベースからのオフセット)を書き換えたら関数名を入れ替えることで同様に正常なデコンパイル結果を得られたが、これもWhat's wrongを表示する。
つまり、.dynstrと.dynsymの書き換え以外の方法でstrncmpとstrcmpの解決先が変更されていることが判明した。strcmpを呼び出そうとしているのにst_name=9のstrncmpという文字列が読み込まれている。
さらに_dl_fixupの処理を確認する。
0x7ffff7fd5ecb <_dl_fixup+91> add r12, rax
時点において、raxは 0x555555400000であり.got.pltセクションのベースアドレスになっている。 このr12はstrcmp<0x201018>のアドレスになっているはずがstrncmp<0x201030>のアドレスになっている。
ためしに実行ファイル内から0x201018を探してみる。リトルエンディアンなのでファイル上は0x181020である。すると、0x620あたりに0x201018と0x201030が存在した。これを入れ替えたところ、動作を変更しないままデコンパイル結果を入れ替えることができた。
xelfviewerを用いて0x620が何なのかを調べると.rela.pltセクションであることが判明した。実際に、.rela.pltのr_offset情報を見ると先に0x201030があり後に0x201018が存在するという、直感的に不可解なデータになっている。
そこで次の仮定を立てた。
-
.pltでのアドレス解決時にpush 0によって.rela.pltの0番目strcmpのアドレスを解決しようとする。しかしr_offsetが書き換わっているのでstrcmpへのアドレスを得ようとしてstrncmpのアドレスを解決してしまう。 - そのまま
strncmpへのアドレスとしてstrncmpのアドレスを書き込み、実行する。 - 次の
strncmp実行時には、すでにstrncmpのアドレスが解決されているのでそのまま利用する。 - デコンパイラは
r_offsetの位置に解決されたアドレスを配置すると仮定する。 - しかし、
gotの最初の位置から順にpushの0 1 2 3...番目としてしまう。 -
.rela.pltではインデックスが0のstrncmpのr_offsetが一番下なため、r_offsetが一番若いstrcmpがインデックス0、つまりpush 0の時に呼び出される値として認識される。 - よってデコンパイル結果にも影響を及ぼす。
そこで、同様のソースコードをgccでオプションを指定せずにコンパイルし、そのr_offsetを入れ替えた。しかし、再現できなかった。
この違いを調べるため、さらに_dl_fixupの処理を確認する。ここからはどのようにしてstrncmp文字列が得られたかを、_dl_lookup_symbol_xの実行までを遡って調べることにする。(こういうのはrrを使うべきなのだろうがpwndbgのままで進める。)
_dl_lookup_symbol_xの第一引数である関数名rdiは、その前に元から存在した0x555555400000にrdxである0xbが加算される。
0x555555400000は.dynstrのベースアドレスであり、0xbはそのオフセットなのでこれでstrncmpになる。
そのオフセットであり0xbは次のようにして取得される。
RDX 0x5555554002f8 ◂— or eax, dword ptr [rax] /* '\x0b' */
<_dl_fixup+222> mov edx, dword ptr [rdx]
rdx 0x5555554002f8は次のようにして求められる。
RDX 0x6
R10 0x5555554002c8
<_dl_fixup+105> lea rdx, [r10 + rdx*8]
2c8は.dynsymのベースアドレスである。
RDXは3の倍数のように思える。
この命令でdynsym上のstrncmpのアドレスが求められる。
rdx 0x6は次のようにして求められる。
RCX 0x0
RDX 0x2
<_dl_fixup+98> lea rcx, [rdx + rdx]
<_dl_fixup+102> add rdx, rcx
RDX 0x6
これはrdxを3倍する命令である。
dynsym上の0から数えて2番目にstrncmpがあるから元のrdxはそれであると考えられる。
RDX 0x200000007
<_dl_fixup+94> shr rdx, 0x20
RDX 0x2
0x200000007は見覚えがある。これは.rela.pltの0番目のr_infoと一致してる。やはりindex(Sym)を求める命令である。
しかし、r_offset = 0x201030であるから、なぜこれが選ばれたのかを知る必要がある。
<_dl_fixup+88> mov rdx, r8
単純にr8からの移動。
RSI 0x5555554005d8
<_dl_fixup+81> mov r8, qword ptr [rsi + 8] <initial+16>
R8 0x200000007
0x5555554005d8は.rela.pltのベースアドレスである。
+8したところに最初のstrncmpの情報がある
r9 0x0
RCX 0x0
RSI 0x0
<_dl_fixup+67> mov rdx, qword ptr [rdx + 8]
<_dl_fixup+71> add rdi, r9
<_dl_fixup+74> lea rsi, [rdx + rcx*8]
<_dl_fixup+78> add rsi, r9
RSI 0x5555554005d8
_dl_fixup+67でベースアドレスを求めている 。rcxとr9は0であるためその後の命令で変化はない。
<_dl_fixup+46> mov rdx,QWORD PTR [rbp+0x68]
rdxはスタック内のデータであることが分かる。
また、
<_dl_fixup+50> mov ebx,esi
<_dl_fixup+52> lea rcx, [rbx + rbx*2]
rcxはrsiの3倍である。
ここまでの流れを整理すると、(rsi * 3 * 8 + rela_plt_base_addr)を計算していることがわかる。8 * 3 = 24はpltの一つの情報の長さなのでrsiは.rela.plt内のindexであると予想できる。実際に.rela.pltの0番目はstrncmpのものである。
_dl_fixup内にはrsiを操作する命令はこれ以上は見つからない。つまり、rsiは_dl_fixupの引数である。
_dl_runtime_resolve_xsavec内の_dl_fixup呼び出し時の状況を調べる。
RBX 0x7fffffffd530
<_dl_runtime_resolve_xsavec+113> mov rsi, qword ptr [rbx + 0x10]
rbx + 0x10 = 0x7fffffffd540であり、rsp 0x7fffffffd178 < 0x7fffffffd540 < rbp 0x7fffffffd560であるから0x7fffffffd540はスタック領域である。そしてこの値はもちろん0である。
plt0から_dl_runtime_resolve_xsavecが呼ばれるときにはすでに0x7fffffffd540に0x00が入ってる。
00:0000│ rsp 0x7fffffffd538 —▸ 0x7ffff7ffe2e0 —▸ 0x555555400000 ◂— jg 0x555555400047
01:0008│-020 0x7fffffffd540 ◂— 0x0
02:0010│-018 0x7fffffffd548 —▸ 0x555555400826 (main+54) ◂— test eax, eax
03:0018│-010 0x7fffffffd550 —▸ 0x5555554008f4 ◂— push rdx /* 'Reversing is cool.' */
04:0020│-008 0x7fffffffd558 —▸ 0x555555400907 ◂— push rdx /* 'Reversing is fun.' */
05:0028│ rbp 0x7fffffffd560 ◂— 0x1
0x7ffff7ffe2e0はGOTのアドレスである。そして、この0x00は
<strcmp@plt+6> push 0
<strcmp@plt+11> jmp 0x555555400660 <0x555555400660>
↓
0x555555400660 push qword ptr [rip + 0x2009a2] <_GLOBAL_OFFSET_TABLE_+8>
0x555555400666 jmp qword ptr [rip + 0x2009a4] <_dl_runtime_resolve_xsavec>
.plt内にあったpush 0である。
これでようやく理解できた。つまり、先の仮説は正しかった。なぜ自身でコンパイルした実行ファイルではうまくいかなかったのかを調べる。
gdbを利用して同様にステップ実行する。しかし、mainからstrcmpを呼び出したところ、既にgot.pltに__strncmp_avx2のアドレスが存在していた。 つまり、実行したときにはすでにアドレスが解決されている。
これは、何もオプションをつけないでコンパイルするとBIND_NOWが有効化されるためだと考えられる。これによってプログラムコードが実行される前に全てのアドレスが解決される。しかし、問題の実行ファイルはLazy bindingなのでBIND_NOWが無効化されており、後からGOTを書き変えるためPartial RELROになっている。
実際に次のコンパイルオプションを利用し、その後にxelfviewerを用いてr_offsetを書き変えたところ、デコンパイラやディスアセンブラをだますことができた。
gcc -fno-stack-protector -Wl,-z,lazy hack.c -o my_hack
undefined8 main(void)
{
int32_t iVar1;
char *s2;
char *s1;
iVar1 = strcmp("Reversing is cool.", "Reversing is fun.", 0xd);
if (iVar1 == 0) {
iVar1 = strncmp("Reversing is cool.", "Reversing is fun.");
if (iVar1 == 0) {
puts("Nope.");
} else {
puts("Welcome to Security Camp!");
}
} else {
puts("What\'s wrong?");
}
return 0;
}
<main+53>: call 0x1070 <strcmp@plt>
<main+76>: call 0x1090 <strncmp@plt>
pwndbg> r
(デバッガのメッセージは省略)
Welcome to Security Camp!
ためしにstrcmpとstrncmp以外も書き変えてみる。この時、命令を適切に実行するために、解決される順序に気を付けないといけない。
例えば、strncmpを解決してputsのGOTに入れたら、putsを呼び出すときにstrncmpが呼び出されてプログラムが停止する。
そこで
-
strncmpがputs -
strcmpがstrncmp -
putsがstrcmp
になるように置き換える。
最初に解決されるのはstrncmpで、解決アドレスはstrcmpのGOTに
次に解決されるのはstrcmpだがすでに解決されているstrncmpに
次にputsを解決してstrncmpの場所にアドレスを入れる。
これによって.rela.plt内のr_offsetの値は上から0x201030 0x201018 0x201028 0x201020になる。
デコンパイル結果
undefined8 main(void)
{
int32_t iVar1;
char *s1;
char *s2;
iVar1 = puts(0x8f4, 0x907, 0xd);
if (iVar1 == 0) {
iVar1 = strncmp(0x8f4, 0x907);
if (iVar1 == 0) {
strcmp(0x919);
} else {
strcmp(0x91f);
}
} else {
strcmp(0x939);
}
return 0;
}
実行結果
Welcome to Security Camp!
なぜデコンパイラやltraceが騙されるのか
デコンパイラはlazy bindingが無効の場合と同じように、シンボル解決をエミュレートしているのだと思う。つまり.plt内のpushする値(_dl_fixupへの引数、.rela.plt内のindex)をもとに、.dynstrのシンボル名を表示しているのだと考えた。
類似する手法を調べる
どのような手法を用いているかを確認できたので、論文などがないかを検索する。結果、同じ方法は見つけられなかったが似たような物をいくつか見つけた。
-
https://blog.ret2.io/2017/11/16/dangers-of-the-decompiler/
plt内の_dl_fixupへの引数(pushする値)を交換している。これによりデコンパイル結果が変化する。 -
https://web.archive.org/web/20201111202708/https://h4des.org/blog/index.php?/archives/346-ELF-obfuscation-let-analysis-tools-show-wrong-external-symbol-calls.html
.dynstrを複製して、セクションテーブルのアドレス書き変える。動的ローダーはセクションテーブルを利用しないためデコンパイル結果が変わる。
検出プログラムを作る
シンボル解決をエミュレートして、r_offsetの値と比較することで検出することが可能だと考えた。
逆アセンブルで命令を取得し、エミュレートするためCapstoneを利用する。実装言語はPythonである。
単純に値を取得するだけでなく、エミュレートすることによってpushする値を置き換える類似手法にも対応できる。
このプログラムでは、最初にlazy bindingが有効になっているかを調べる。そうでなければ今回の手法は利用できない。
get_plt_target関数では.pltセクションの内容を読み取り、逆アセンブルされた命令をもとにアドレス解決をエミュレートしている。
私が利用しているgcc version 11.4.0では.plt.secセクションが存在しており、若干動作が変更されているためget_plt_sec_target関数で対応する。
そして、.rela.pltセクション内のr_offsetの値と比較し、結果を表示する。見やすくするためにrichを利用している。
実行例(問題ファイル)
❱ python3 checkplt.py hack
╭─────────────────────── .plt ───────────────────────╮
│ 0x660 push qword ptr [rip + 0x2009a2] │
│ 0x666 jmp qword ptr [rip + 0x2009a4] │
│ 0x66c nop dword ptr [rax] │
│ 0x670 jmp qword ptr [rip + 0x2009a2] -> [0x201018] │
│ 0x676 push 0 │
│ 0x67b jmp 0x660 │
│ 0x680 jmp qword ptr [rip + 0x20099a] -> [0x201020] │
│ 0x686 push 1 │
│ 0x68b jmp 0x660 │
│ 0x690 jmp qword ptr [rip + 0x200992] -> [0x201028] │
│ 0x696 push 2 │
│ 0x69b jmp 0x660 │
│ 0x6a0 jmp qword ptr [rip + 0x20098a] -> [0x201030] │
╰────────────────────────────────────────────────────╯
.rela.plt
┏━━━┳━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━┓
┃ ┃ symbol ┃ r_offset ┃ r_info ┃ type ┃
┡━━━╇━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━┩
│ 0 │ 2(strncmp) │ 0x201030 │ 0x200000007 │ 0x7 │
│ 1 │ 4(puts) │ 0x201020 │ 0x400000007 │ 0x7 │
│ 2 │ 5(__libc_start_main) │ 0x201028 │ 0x500000007 │ 0x7 │
│ 3 │ 6(strcmp) │ 0x201018 │ 0x600000007 │ 0x7 │
└───┴──────────────────────┴──────────┴─────────────┴──────┘
Result
┏━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━┳━━━━━━━━━━━━━━━━━━━┓
┃ ┃ symbol ┃ dynamic ┃ ┃ r_offset ┃
┡━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━╇━━━━━━━━━━━━━━━━━━━┩
│ 0 │ strncmp │ 0x201018 │ -> │ 0x201030(strcmp) │
│ 1 │ puts │ 0x201020 │ -> │ 0x201020 │
│ 2 │ __libc_start_main │ 0x201028 │ -> │ 0x201028 │
│ 3 │ strcmp │ 0x201030 │ -> │ 0x201018(strncmp) │
└───┴───────────────────┴──────────┴────┴───────────────────┘
Resultが0 │ strncmp │ 0x201018 │ -> │ 0x201030(strcmp)となっており、入れ替わりを検出できている。
実行例(gcc version 11.4.0デコンパイルしたファイル)
❱ python3 checkplt.py my_hack_hacked
╭─────────────────────────────────────────── .plt.sec ────────────────────────────────────────────╮
│ 0x1070 endbr64 │
│ 0x1074 bnd jmp qword ptr [rip + 0x2f9d] -> [0x4018] -> (.plt)0x1030 endbr64 (.plt)0x1034 push 0 │
│ 0x107b nop dword ptr [rax + rax] │
│ 0x1080 endbr64 │
│ 0x1084 bnd jmp qword ptr [rip + 0x2f95] -> [0x4020] -> (.plt)0x1040 endbr64 (.plt)0x1044 push 1 │
│ 0x108b nop dword ptr [rax + rax] │
│ 0x1090 endbr64 │
│ 0x1094 bnd jmp qword ptr [rip + 0x2f8d] -> [0x4028] -> (.plt)0x1050 endbr64 (.plt)0x1054 push 2 │
│ 0x109b nop dword ptr [rax + rax] │
╰─────────────────────────────────────────────────────────────────────────────────────────────────╯
.rela.plt
┏━━━┳━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━┓
┃ ┃ symbol ┃ r_offset ┃ r_info ┃ type ┃
┡━━━╇━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━┩
│ 0 │ 2(strncmp) │ 0x4028 │ 0x200000007 │ 0x7 │
│ 1 │ 4(puts) │ 0x4020 │ 0x400000007 │ 0x7 │
│ 2 │ 5(strcmp) │ 0x4018 │ 0x500000007 │ 0x7 │
└───┴────────────┴──────────┴─────────────┴──────┘
Result
┏━━━┳━━━━━━━━━┳━━━━━━━━━┳━━━━┳━━━━━━━━━━━━━━━━━┓
┃ ┃ symbol ┃ dynamic ┃ ┃ r_offset ┃
┡━━━╇━━━━━━━━━╇━━━━━━━━━╇━━━━╇━━━━━━━━━━━━━━━━━┩
│ 0 │ strncmp │ 0x4018 │ -> │ 0x4028(strcmp) │
│ 1 │ puts │ 0x4020 │ -> │ 0x4020 │
│ 2 │ strcmp │ 0x4028 │ -> │ 0x4018(strncmp) │
└───┴─────────┴─────────┴────┴─────────────────┘
.plt.secにも対応していることがわかる。
lazy bindingでない実行ファイルに対しては次の警告を出力に追加する。
no .got.plt section
maybe not lazy binding?
本来の出力は色分けもされており分かりやすいので是非実行してもらいたい。
感想
elfファイルの構造やアドレス解決について深い理解を必要とする難問であった。でも新しい技術を身につけられて嬉しかった。また、gdbやltraceのような低レイヤの解析に使える、歴史あるツールでも騙されてしまうことに驚いた。今後は自身の利用する武器の特性を適切に理解する必要があると感じた。そして、今回調べなかったGNU_IFUNCや類似手法についてもいつか理解を深めたい。
終わり
晒しと言いながら1 2 3を晒せてない。
本当に申し訳ない。
この晒しを参考にして通った人は応募課題を晒してください。