Memory Forensics for CTF

CTF用のメモリーフォレンジック問題を考える。
メモリーフォレンジックが必要になるシーンとしては以下がある。

1.組織のUTMが内部から外部へのマルウェアからの通信を検知する。
 例)ブラックリストとして登録されたC&Cサーバへのhttps通信を検知

2.UTMのログをもとに通信元のPCを調査する。
 例)おそらくはProxy経由でのHTTPS通信が対象となるためProxyログからマルウェアに感染したと思われるPCを見つける。

3.マルウェアに感染したと思われるPCのメモリダンプを取得する。

4.メモリ解析ツール(Volatilityなど)で不正な通信を行ったプロセスの特定、マルウェア実行プロセスのダンプを実施する。

5.取得したマルウェアをIDA-ProやGhidraなどの解析ツールを用いてどのような挙動をするかの解析を行う。

今回は上記4のVolatilityを用いてメモリ解析を問うCTF問題を作成する。

■Volatilityインストール
OS:Ubuntu

git,python,curlインストール
 sudo apt install git
 sudo apt install python
 sudo apt install curl

GitHubからVolatilityをクローンする.
 git clone https://github.com/volatilityfoundation/volatility.git

もろもろ
 cd volatility
 sudo python setup.py build install
 curl https://bootstrap.pypa.io/pip/2.7/get-pip.py -o get-pip.py
 python get-pip.py
 export PATH=”$PATH:/home//.local/bin”
 sudo apt install gcc python2.7-dev
 pip install pycrypto
 pip install distorm3==3.4.4

■メモリダンプの準備
・Win10をVirtualBox上で用意(メモリ2G)
・RamCaptureでメモリダンプ取得

■Volatilityによる解析

■解析プロファイルの決定
ダンプしたメモリイメージを解析するためのプロファイルを以下コマンドで確認する。

python vol.py -f memory.file imageinfo

実行結果が「Suggested Profile(s)」で示される。以降、各コマンドを実行するときは解析用のプロファイルを指定する必要がある。
※今回はWin10x64_19041プロファイルを用いる。

■実行されていたプロセス一覧の確認
pslistオプションで起動していたプロセス一覧を取得する。

python vol.py -f memory.file –profile=Win10x64_19041 pslist

■通信一覧の確認とプロセスの抽出
netscanオプションで通信一覧とその通信を行っていたプロセス情報を取得することができる。

python vol.py -f memory.file –profile=Win10x64_19041 netscan

■不審な通信を行っているプロセスIDを特定し、そのプロセスを抽出する
python vol.py -f memory.file –profile=Win10x64_19041 procdump -p malware-pid -D dumpdirectory

おまけ
printkey オプションでレジストリ情報も見ることができる。

CTF用の問題を作成するには以下が必要となる。

・特定のファイルのデータを読み取り、特定のサイト(A.B.C.D)への通信を行うプログラムの準備
・上記プログラムをWin10上で実行している状態のメモリダンプの取得

CTFの問題としては以下が考えられる。
・あるサイト(A.B.C.D)に通信していたプロセスIDを答えよ
・そのプロセスが持ち出そうとしたデータ(ファイル名)を答えよ

■検証1
Win10環境で外部とHTTP通信するPowerShellスクリプトを実行してメモリイメージを取得。Volatilityで解析をした。その際にnetscanオプションで該当の通信をしているpidを確認したところ「-1」となり、正確なpidがわからなかった。そのためyarascanオプションを用いて該当のアドレスの情報を持つプロセスを検索した。

・yaraのインストール
  pip install yara-python

・yaraを用いたVolatility検索実行
  python vol.py -f 20210319.mem –profile=Win10x64_19041 yarascan -Y “www.google.co.jp”

★yarascanを用いることでキーワードを含むプロセスの特定が可能になる。

■検証2
 検証1ではPowerShellを用いて特定のサイトに接続するプログラムをVolatilityで抽出できるかの検証を行った。検証2では同じ内容のプログラムをexe化したものを同じく追跡できるかどうか、また抽出したプロセスをデバッグ可能かを確認する。

172.217.31.131:80へ通信する「a.exe」を実行中のイメージを取得し、volatilityのnetscanコマンドを実行した結果である。
python vol.py -f memory.file –profile=Win10x64_19041 netscan

通信としては確認できるがプロセスIDが検証1と同じく「-1」となっている。

yarascanオプションで「172.217.31.131」の文字を含むプロセスを確認する。

python vol.py -f memory.img –profile=Win10x64_19041 yarascan -Y “172.217.31.131”

文字列を含むプロセスが複数現れる。PID5592に注目する。
※このプロセスID以外、pslistでは出てこなかった。

volatilityのpslistコマンドを確認するPID5592のプロセスが「a.exe」であることがわかる。

procdumpコマンドでPID5592をexeファイルとしてdumpする。

dumpしたファイルはUbuntu上のgdbやobjdumpでは解析ができなかった。Windows上でMinGWをインストールした環境でgdbで解析するとinfo functions、disas mainは成功しなかった。しかし、runは成功した。Windows上のobjdumpを実行すると「file truncated」と出て成功しなかった。おそらく、Volatilityのprocdumpコマンドでは解析可能な形でのDumpができなかったと考える。(IIJさんのサイトにページングファイルを使用しているときなどうまく取れない、などの記事があった https://www.iij.ad.jp/dev/report/iir/046/02.html)

したがって、CTFの問題としては不正な通信先のアドレス情報から「クライアントPC上でその通信を実行していたPIDおよび実行ファイル名は何か」という問題がよいと考える。

■使用したPowerShell

param( [string] $remoteHost = "www.google.co.jp", [int] $port = 80 )   

try
{
	Write-Host "Connecting to $remoteHost on port $port ... " -NoNewLine
	try
	{
		$socket = New-Object System.Net.Sockets.TcpClient( $remoteHost, $port )
		Write-Host -ForegroundColor Green "OK"
	}
	catch
	{
		Write-Host -ForegroundColor Red "failed"
		exit -1
	}

	$stream = $socket.GetStream( )
	$writer = New-Object System.IO.StreamWriter( $stream )
	$buffer = New-Object System.Byte[] 1024
	$encoding = New-Object System.Text.AsciiEncoding

	while( $true )
	{
		start-sleep -m 500
		while( $stream.DataAvailable )
		{
			$read = $stream.Read( $buffer, 0, 1024 )
			Write-Host -n ($encoding.GetString( $buffer, 0, $read ))
		}
		$command = Read-Host
		$writer.WriteLine( $command )
		$writer.Flush( )
	}
}
finally
{
	if( $writer ) {	$writer.Close( )	}
	if( $stream ) {	$stream.Close( )	}
} 
■使用したexeのソース(コンパイル:gcc a.c -lws2_32)

#include<stdio.h>
#include <windows.h>
#include<winsock2.h>

#pragma comment(lib,"ws2_32.lib") //Winsock Library

int main(int argc , char *argv[])
{
	WSADATA wsa;
	SOCKET s;
	struct sockaddr_in server;
	char *message , server_reply[2000];
	int recv_size;

	printf("\nInitialising Winsock...");
	if (WSAStartup(MAKEWORD(2,2),&wsa) != 0)
	{
		printf("Failed. Error Code : %d",WSAGetLastError());
		return 1;
	}
	
	printf("Initialised.\n");
	
	//Create a socket
	if((s = socket(AF_INET , SOCK_STREAM , 0 )) == INVALID_SOCKET)
	{
		printf("Could not create socket : %d" , WSAGetLastError());
	}

	printf("Socket created.\n");
	
	
	server.sin_addr.s_addr = inet_addr("172.217.31.131");
	server.sin_family = AF_INET;
	server.sin_port = htons( 80 );

	//Connect to remote server
	if (connect(s , (struct sockaddr *)&server , sizeof(server)) < 0)
	{
		puts("connect error");
		return 1;
	}
	
	puts("Connected");
	
	//Send some data
	message = "GET / HTTP/1.1\r\n\r\n";
	if( send(s , message , strlen(message) , 0) < 0)
	{
		puts("Send failed");
		return 1;
	}
	puts("Data Send\n");

	Sleep(30000);
	
	//Receive a reply from the server
	if((recv_size = recv(s , server_reply , 2000 , 0)) == SOCKET_ERROR)
	{
		puts("recv failed");
	}
	
	puts("Reply received\n");

	//Add a NULL terminating character to make it a proper string before printing
	server_reply[recv_size] = '\0';

	puts(server_reply);

	return 0;
}

コメントをどうぞ