リバースエンジニアリング能力向上とWinDbgのプロになるため、自作のモジュールをリバーシングしてます。
今回は、C言語で実装したWindowsのソケット通信プログラムを解析していきます。
※検証目的に作成したプログラムでエラー処理などは実装していないため実用性は担保しません。
もくじ
今回作成したプログラム
今回作成したプログラムは以下のリポジトリに置いてあります。
次の2つの機能を実装してます。
- TCPコネクションを確立してPOSTリクエストを送信する
- UDPでデータを送信する
ヘッダファイルは以下のものを使用してます。
#include <winsock2.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#pragma comment(lib, "ws2_32.lib")
// IPアドレスを扱う場合などに利用
// #include <ws2tcpip.h>
// #include <iphlpapi.h>
// #pragma comment(lib, "iphlpapi.lib")
// winsock2.h と併用して使用する場合は、必ずwinsock2.hより後に定義する
// #include <windows.h>
winsock2
winsock2.h
は、Windows Sockets 2を含むいくつかの実装を行う際に使用するヘッダファイルです。
Windows環境におけるソケット通信に使用するAPI関数などが含まれています。
参考:Winsock2.h header - Win32 apps | Microsoft Docs
参考:Windows Sockets 2 - Win32 apps | Microsoft Docs
Windowsでソケットプログラミングを行う際の最初の一歩は、このwinsock2.h
で定義されているWSAStartup
関数を使ってWSADATA
構造体の初期化を行うことです。
参考:WSADATA (winsock.h) - Win32 apps | Microsoft Docs
参考:WSAStartup function (winsock2.h) - Win32 apps | Microsoft Docs
WSAStartup
関数は使用するWindows Socketsのバージョンと、初期化するWSADATA
構造体の2つを引数に取ります。
少々ややこしい点として、WSAStartup
関数の第一引数は、16bit符号なし整数(WORD型)を取りますが、下位8bitにWindows Socketsのメジャーバージョン、上位8bitにマイナーバージョンを指定する必要があります。
そのため、引数に与える値はMAKEWORD
マクロを使用して作成しています。
参考:MAKEWORD macro (Windows) | Microsoft Docs
また、初期化に成功した場合、WSAStartup
関数は戻り値0を返し、失敗した場合は定義されているエラーコードのいずれかを返します。
そのため、今回の実装には含めていませんがWSAStartup
関数の戻り値でエラー処理を実装します。
pragma comment()
winsock2.h
などのヘッダファイルをインクルードしてコンパイルしようとすると、error LNK2019: 未解決の外部シンボル
のようなエラーが返ってくる場合があります。
win_tcp_udp.obj : error LNK2019: 未解決の外部シンボル __imp__closesocket@4 が関数 _send_http_post で参照されました
win_tcp_udp.obj : error LNK2019: 未解決の外部シンボル __imp__connect@12 が関数 _send_http_post で参照されました
win_tcp_udp.obj : error LNK2019: 未解決の外部シンボル __imp__htons@4 が関数 _send_http_post で参照されました
win_tcp_udp.obj : error LNK2019: 未解決の外部シンボル __imp__inet_addr@4 が関数 _send_http_post で参照されました
win_tcp_udp.obj : error LNK2019: 未解決の外部シンボル __imp__send@16 が関数 _send_http_post で参照されました
win_tcp_udp.obj : error LNK2019: 未解決の外部シンボル __imp__sendto@24 が関数 _send_udp で参照されました
win_tcp_udp.obj : error LNK2019: 未解決の外部シンボル __imp__socket@12 が関数 _send_http_post で参照されました
win_tcp_udp.obj : error LNK2019: 未解決の外部シンボル __imp__WSAStartup@8 が関数 _send_http_post で参照されました
win_tcp_udp.obj : error LNK2019: 未解決の外部シンボル __imp__WSACleanup@0 が関数 _send_http_post で参照されました
.\build\bin\win_tcp_udp.exe : fatal error LNK1120: 9 件の未解決の外部参照
これは、コンパイラがライブラリをリンクできないことで発生します。
解決方法として、次のどちらかを実施します。
- VisualStudioで開発している場合は、プロジェクトのプロパティの[構成プロパティ]>[リンカー]>[入力]から[追加の依存ファイル]に参照エラーの発生しているライブラリを追加します
#pragma comment(lib, "<ライブラリ名>.lib")
を追記します。
今回はプロジェクトは作成せずCファイルをcl.exeでコンパイルしたいので、pragma
を使っていきます。
pragma
とは、コンパイル時に特定のアクションを指示する命令です。
特に、comment
のlib
構文を使用することで、コンパイル時にリンクするライブラリをソースコード側から指定することができます。
参考:comment pragma | Microsoft Docs
そのため、#pragma comment(lib, "ws2_32.lib")
の行を追加することでwinsock2.h
のリンクエラーを回避することができるようになります。
ws2tcpip.h
今回の実装では不要ですが、ws2tcpip.h
はIPアドレスの取得に関連する構造体などが定義された、TCP/IPを行うために必要なヘッダファイルです。
参考:Creating a Basic Winsock Application - Win32 apps | Microsoft Docs
参考:Ws2Tcpip.h header - Win32 apps | Microsoft Docs
POSTリクエストを送信する関数
任意のアドレスに対してPOSTリクエストを送信する関数は以下の通りです。
int send_http_post(unsigned char *senddata){
char destination[] = RSERVER;
unsigned short port = 80;
unsigned char httppath[20] = "/upload";
char httphost[] = LSERVER;
int dstSocket;
int result;
char toSendText[MAXBUF];
char postdata[MAXBUF];
int read_size;
// WSADATAの初期化
WSADATA data;
WSAStartup(MAKEWORD(2, 0), &data);
// AF_INETを設定
struct sockaddr_in dstAddr;
memset(&dstAddr, 0, sizeof(dstAddr));
dstAddr.sin_port = htons(port);
dstAddr.sin_family = AF_INET;
dstAddr.sin_addr.s_addr = inet_addr(destination);
// Socket通信の開始(SOCK_STREAMを指定)
printf("\t==>Creating socket...\n");
dstSocket = socket(AF_INET, SOCK_STREAM, 0);
if (dstSocket < 0){
printf("\t==>Creating socket failed!!\n");
return 0;
}
printf("\t==>Creating socket succeeded!!\n");
// 通信の開始
printf("\t==>Connecting...\n");
result = connect(dstSocket, (struct sockaddr *) &dstAddr, sizeof(dstAddr));
if (result < 0){
printf("\t==>Binding failed!!\n");
return 0;
}
printf("\t==>Connecting succeeded!!\n");
// HTTPリクエストの作成
printf("\t==>Creating HTTP request...\n");
sprintf(toSendText, "POST %s HTTP/1.1\r\n", httppath);
send(dstSocket, toSendText, strlen(toSendText) , 0);
sprintf(toSendText, "Host: %s:%d\r\n", httphost, port);
send(dstSocket, toSendText, strlen(toSendText), 0);
sprintf(postdata, "%s\r\n", senddata);
sprintf(toSendText, "Content-Length: %d\r\n", strlen(postdata));
send(dstSocket, toSendText, strlen(toSendText), 0);
sprintf(toSendText, "\r\n");
send(dstSocket, toSendText, strlen(toSendText), 0);
// HTTPリクエストの送信
printf("\t==>Sending HTTP request...\n");
send(dstSocket, postdata, strlen(postdata), 0);
// 通信のクローズ
printf("\t==>HTTP request is sent!!\n");
closesocket(dstSocket);
WSACleanup();
return 0;
}
WSADATAの初期化
まずはソケット通信に必要なWSADATA
構造体をWSAStartup
関数で初期化します。
Windows Socketsのバージョンは2.0を指定しています。
// WSADATAの初期化
WSADATA data;
WSAStartup(MAKEWORD(2, 0), &data);
通信先の指定
// AF_INETを設定
struct sockaddr_in dstAddr;
memset(&dstAddr, 0, sizeof(dstAddr));
dstAddr.sin_port = htons(port);
dstAddr.sin_family = AF_INET;
dstAddr.sin_addr.s_addr = inet_addr(destination);
WSADATA
構造体の初期化後、sockaddr_in
構造体を作成してアドレスファミリと通信先のアドレス、ポート番号を定義します。
公式ドキュメントではaddrinfo
構造体を使用する方法が紹介されていますが、今回はsockaddr_in
構造体を使用してます。
参考:Creating a Socket for the Client - Win32 apps | Microsoft Docs
sockaddr_in
構造体はIPv4の通信にのみ利用でき、IPv6を使用する場合はsockaddr_in6
構造体を使用します。
公式ドキュメントで使用しているaddrinfo
構造体はIPv4とIPv6の両方にソケットを使用する場合に利用します。
特に理由がなければaddrinfo
構造体を使っておけば問題なさそうですね。
参考:c - What is the difference between struct addrinfo and struct sockaddr - Stack Overflow
アドレスファミリにはAF_INET
を指定しています。
AF_INET
はIPv4の通信を行う場合に定義するアドレスファミリです。
参考:sockets - What is AF_INET, and why do I need it? - Stack Overflow
Socketの作成
socket
関数を使ってソケットを作成します。
// Socket通信の開始(SOCK_STREAMを指定)
printf("\t==>Creating socket...\n");
dstSocket = socket(AF_INET, SOCK_STREAM, 0);
if (dstSocket < 0){
printf("\t==>Creating socket failed!!\n");
return 0;
}
printf("\t==>Creating socket succeeded!!\n");
今回はTCP接続を行うため、第2引数にはSOCK_STREAM
を指定しています。
参考:socket function (winsock2.h) - Win32 apps | Microsoft Docs
第3引数にはプロトコルパラメータであり、使用するプロトコルを定義できます。
今回はTCPを明示的に指定しておらず、0を渡しています。
これは、プロトコルの指定を呼び出し元が行わないことを意味します。
参考:c - what does 0 indicate in socket() system call? - Stack Overflow
コネクションの確立
次はconnect
関数でソケット接続を確立します。
// 通信の開始
printf("\t==>Connecting...\n");
result = connect(dstSocket, (struct sockaddr *) &dstAddr, sizeof(dstAddr));
if (result < 0){
printf("\t==>Binding failed!!\n");
return 0;
}
printf("\t==>Connecting succeeded!!\n");
connect
関数はSocket通信におけるクライアント側の接続を行います。
参考:Connecting to a Socket - Win32 apps | Microsoft Docs
この関数によってbindされているSocketサーバとのTCPコネクションを確立します。
画像引用元:Unix Socket - Client Server Model
HTTPリクエストの送信
事前に作成したPOSTリクエストのデータをsend
関数でサーバに送信します。
// HTTPリクエストの送信
printf("\t==>Sending HTTP request...\n");
send(dstSocket, postdata, strlen(postdata), 0);
今回は実装していませんが、レスポンスを受信するにはrecv
関数を使います。
参考:Sending and Receiving Data on the Client - Win32 apps | Microsoft Docs
UDP通信を行う関数
UDP通信を行う関数は以下の通りです。
int send_udp(unsigned char *senddata)
{
char destination[] = RSERVER;
unsigned short port = 80;
char httphost[] = LSERVER;
int dstSocket;
int result;
char toSendText[MAXBUF];
int read_size;
// WSADATAの初期化
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 0), &wsaData);
struct sockaddr_in dstAddr;
memset(&dstAddr, 0, sizeof(dstAddr));
dstAddr.sin_port = htons(port);
dstAddr.sin_family = AF_INET;
dstAddr.sin_addr.s_addr = inet_addr(destination);
// Socket通信の開始(SOCK_DGRAMを指定)
printf("\t==>Creating socket...\n");
dstSocket = socket(AF_INET, SOCK_DGRAM, 0);
if (dstSocket < 0)
{
printf("\t==>Creating socket failed!!\n");
return 0;
}
printf("\t==>Creating socket succeeded!!\n");
// UDPパケットの送信
printf("\t==>Sending UDP...\n");
sendto(dstSocket, senddata, strlen(senddata), 0, (SOCKADDR *)&dstAddr, sizeof(dstAddr));
printf("\t==>UDP is sent!!\n");
closesocket(dstSocket);
WSACleanup();
return 0;
}
基本的にはTCP接続の時と同じ実装なので詳細は割愛します。
違いとしては、UDP接続なのでSocketを作成する際の指定をSOCK_DGRAM
にする点と、3wayハンドシェイクを行わないのでconnect
関数を使用せず、sendto
関数でデータ転送を行う点です。
通信の確認
このプログラムを実行し、実際に通信が行われているかWireSharkで確認してみました。
まず、以下の通りPOSTリクエストが送信されていることが確認できました。
また、UDPパケットも受信していることを確認しました。
これでプログラムがちゃんと動作していることが確認できたので、最後にリバーシングしてみたいと思います。
リバーシングする
Ghidraでデコンパイルする
まずはentry
関数からmain
関数を特定しました。
その後、send_http_post
関数とsend_udp
関数を特定した後、win_tcp_udp.exe
を実行したときに展開されたイメージベースを設定しました。
WinDbgでTTDトレースを取得する
解析を容易にするため、ビルドしたプログラムのTTDトレースを取得しました。
TTDトレースの取得方法は以下の記事にまとめてあります。
参考:【WinDbg Preview】Time Travel Debuggingで始める新しいデバッグ手法
また、取得したトレースファイルは以下のリポジトリにも置いてあります。
Socketの中身を追う
とりあえず最初の解析ターゲットとして、socket
関数の挙動を追っていくことにします。
Ghidraのディスアセンブリ結果から0xdf726e
にブレークポイントを設定し、g
コマンドで処理を進めます。
> bu 0x00df726e
> g
Step into
でsocket
関数の中に入ってみると、Prolog
とついたオブジェクトを扱っているように見えます。
Prolog
に関してはしばらく調べたものの該当しそうな情報が見つからなかったのでいったんスキップしました。
誰か詳しい人いたら教えてください。。
作成されたソケット
続いて、socket
関数の直後の行を見てみます。
socket
関数は、作成したSocketを指すファイルディスクリプタを戻り値として返します。
EAXレジスタの中身を見てみると以下の値が格納されていました。
> r eax
eax=00000150
参考:socket function (winsock2.h) - Win32 apps | Microsoft Docs
このファイルディスクリプタを指すハンドルはTTDでは拾うことができませんが、ライブデバッグを行うと!handle
コマンドで参照することができます。
> r eax
eax=00000108
> !handle 108 f
Handle 108
Type File
Attributes 0
GrantedAccess 0x16019f:
ReadControl,WriteDac,Synch
Read/List,Write/Add,Append/SubDir/CreatePipe,ReadEA,WriteEA,ReadAttr,WriteAttr
HandleCount 2
PointerCount 65534
No Object Specific Information available
ファイルオブジェクトの中身も見ようと思ったら多分カーネルデバッグを仕掛けないとだめなのかな?(たぶん)
sockaddr_in構造体を読む
次に解析するのはソースコードでいうと以下の箇所です。
struct sockaddr_in dstAddr;
memset(&dstAddr, 0, sizeof(dstAddr));
dstAddr.sin_port = htons(port);
dstAddr.sin_family = AF_INET;
dstAddr.sin_addr.s_addr = inet_addr(destination);
sockaddr_in
のメモリ領域を確保して、ポート番号、アドレスファミリ、IPアドレスをセットしています。
sockaddr_in
構造体は以下の構造になっています。
in_addr
構造体には4バイトでIPv4アドレスが格納されます。
sin_zero
はシステムに予約された領域で、すべて0埋めされます。
typedef struct sockaddr_in {
#if ...
short sin_family;
#else
ADDRESS_FAMILY sin_family;
#endif
USHORT sin_port;
IN_ADDR sin_addr;
CHAR sin_zero[8];
} SOCKADDR_IN, *PSOCKADDR_IN;
参考:SOCKADDR_IN (ws2def.h) - Win32 apps | Microsoft Docs
参考:in_addr (winsock2.h) - Win32 apps | Microsoft Docs
ちなみに、sin_port
とsin_addr
にはネットワークバイトオーダの値を入れる必要があるため、ソースコードではhtons
関数とinet_addr
関数を使用しています。
参考:htons function (winsock.h) - Win32 apps | Microsoft Docs
参考:inet_addr function (winsock2.h) - Win32 apps | Microsoft Docs
解析対象の構造がおおむね確認できたところで、memset
関数の呼び出し後のアドレス0xdf7227
にブレークポイントを設定します。
ここでmemset
関数の戻り値を確認すると、確保されたアドレスが0x55d810
から始まることがわかります。
> r eax
eax=0055d810
メモリアドレスを見てみると16バイト分の領域に値がセットされているように見えます。
sockaddr_in
構造体は、ポートとアドレスファミリの領域にそれぞれ2バイトずつ、IPアドレスに4バイト、システム予約領域に8バイトを使用します。
このまま処理を進めていくと、空だったメモリの領域にポート番号、アドレスファミリ、IPアドレスが設定されました。
このままだと少しわかりづらいので表示を変えてみました。
> dyb 0055d810
76543210 76543210 76543210 76543210
-------- -------- -------- --------
0055d810 00000010 00000000 00000000 01010000 02 00 00 50
0055d814 10101001 11111110 01100100 00011110 a9 fe 64 1e
0055d818 00000000 00000000 00000000 00000000 00 00 00 00
0055d81c 00000000 00000000 00000000 00000000 00 00 00 00
ポート番号80が0x0050
とネットワークバイトオーダ(ビッグエンディアン)形式で格納されています。
また、0x55d814
からの4バイトは、IPアドレスが同じくネットワークバイトオーダで格納されています。
まとめ
年の瀬にソケットプログラミングとリバーシングなどをしてました。
来年も良い年になるとよいですね。