0. Windows 11 에서 정상적인 실행이 되지 않으므로, hyper-V 등을 통해서 windows 10에서 실행하는 것을 권장한다.

1. 다운로드 : Python3, Visual studio 2022, DynamoRIO(https://github.com/DynamoRIO/dynamorio/releases)

2. 적절한 경로에 DynamoRIO 압축 해제

3. config.h 에서 input 파일 최대 사이즈 수정  (1*1024*1024 를 아래와 같이 수정)

/* Maximum size of input file, in bytes (keep under 100MB): */

#define MAX_FILE            (100 * 1024 * 1024)

4. x64 Native Tools Command Prompt for VS 2022 열기

git clone https://github.com/googleprojectzero/winafl
cd winafl
git submodule update --init --recursive
mkdir build64
cd build64
cmake -G"Visual Studio 17 2022" -A x64 .. -DDynamoRIO_DIR=I:\DynamoRIO\cmake -DTINYINST=1 -DUSE_DRSYMS=1 -DINTELPT=1 -DUSE_COLOR=1
cmake --build . --config Release

5. harness 작성

#include <iostream>
#include <windows.h>

typedef int(__stdcall* _OHMYGOD)(const char* data); 
_OHMYGOD func;

extern "C" __declspec(dllexport) __declspec(noinline) int fuzzme(const char* path)
{   
    int result = func(path);
    return result;
}

int main(int argc, char *argv[])
{    
    HMODULE hMod = GetModuleHandle(0);
   
    hMod = LoadLibrary(L"I:\\victim\\x64\\Release\\victim.dll");
    if (NULL == hMod)
    {
        printf("dll load error\n");
        return 0;
    }
   
    func = (_OHMYGOD)GetProcAddress(hMod, "ohmygod");
    fuzzme(argv[1]);    
}

6. victim 작성 (타겟 DLL)

#include "pch.h"
#include <iostream>

extern "C" __declspec(dllexport) int ohmygod(const char* path)
{
    std::cout << path << std::endl;
    char data[40] = { 0, };
    char buf[30] = { 0, };
    
    HANDLE hFile = CreateFileA(path, GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL);
    if (hFile)
    {
        DWORD dwRead;
       
        ReadFile(hFile, data, sizeof(data), &dwRead, NULL);
        std::cout << "read : " << dwRead << std::endl;
        if (dwRead)
        {
            memcpy(buf, data, dwRead+40);
            std::cout << buf;
        }
        CloseHandle(hFile);
    }
    return 0;
}

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

원활한 crash 발생을 위해 각각 Release 로 빌드한다.

7. 입출력 디렉토리 설정

afl은 사용자가 지정한 입력 디렉토리에 있는 파일들을 퍼징 데이터로 사용한다. 

crash를 유발하지 않는 정상 입력 데이터이어야 첫 실행 시 에러가 발생하지 않는다.

아래 데이터를 1.txt로 저장한다.

abcd

8. 공격

afl-fuzz.exe -D I:\\dynamorio\\bin64  -i "d:\\fuzz_input" -o "d:\\fuzz_output" -t 1000 -- -coverage_module victim.dll -target_module harness.exe -target_method fuzzme -fuzz_iterations 10 -nargs 1 -- I:\\harness\\x64\\Release\\harness.exe @@

실행은 반드시 afl-fuzz.exe 디렉토리에서 하여야 한다. 그렇지 않다면 아래와 같은 에러가 발생한다.

주의 : -coverage_module 및 -target_module 에는 파일명만 기입한다.

아래와 같은 화면이 나오면 실행 성공이다.

crash가 발생할 경우, i:\fuzz_output\crashes 경로에 상세 정보가 저장된다.

만약 CPU가 인텔인 경우, -D 옵션 대신 -P옵션을 사용하도록 하자. 속도가 압도적으로 빠르다. (내 PC에서는 5배 이상 차이가 났다.)

afl-fuzz.exe -P -i "d:\\fuzz_input" -o "d:\\fuzz_output" -t 1000 -- -coverage_module victim.dll -target_module harness.exe -target_method fuzzme -fuzz_iterations 10 -nargs 1 -- I:\\harness\\x64\\Release\\harness.exe @@

 

https://github.com/codetronik/DepthFantasiaHack/blob/master/DFHack/ddrawhook.cpp

 

codetronik/DepthFantasiaHack

Contribute to codetronik/DepthFantasiaHack development by creating an account on GitHub.

github.com

 

DirectDrawCreateEx 후킹 시 if ((DWORD)lplpDD == 0x8fa7d4)는 특정 게임을 타겟으로 했으므로 삭제해주셔야 합니다.

 

본 문서는 scherzo의 "INSIDE CODE VIRTUALIZER" 기반으로 작성되었습니다.

LINK : http://index-of.es/Reverse-Engineering/Inside%20Code%20Virtualizer.pdf

FULL VERSION : https://tuts4you.com/e107_plugins/download/download.php?view.3162 (zip 안에 포함되어 있음)

Code Virtualizer 1.0.1.0 (Unpacking).rar
0.56MB

분석 시 사용한 Code Virtualizer는 unpack된 버전을 사용하였습니다.

LINK : https://bbs.pediy.com/thread-29018.htm (회원가입 필요)

Unpacked_Code_Virtualizer_v1.0.1.0_Demo.rar
2.21MB

사용 예제 : 비번 1

target.zip
0.04MB

분석 편의성을 위해 아래 3가지를 고정했습니다.

가상 코드 난독화 레벨 : 0 

가상 머신 복잡성 : 0

Key SEED : 1 

예제 코드는 target.exe를 보시면 됩니다.

 

아래의 예제 코드를 작성 후 빌드합니다.

int main()
{  
    VIRTUALIZER_START
    int a = 0x12345678;
    VIRTUALIZER_END
    printf("%d", a);
}

빌드한 바이너리를 디버거에 붙여보면 아래와 같은 형태가 됩니다.

가상화되는 부분은 0x411900의 7바이트입니다.

이 바이너리를 Code Virtualizer에 넣고 protect 버튼을 누르면, 코드 가상화 및 난독화 기능을 수행합니다. 

프로그램의 동작 순서는 아래와 같습니다.

암/복호화 구조체 생성 → KEY 초기화(with time seed) → 레지스터 인덱스 생성 → 핸들러 구조체 생성 → 핸들러에 복호화 코드 추가 → 핸들러 난독화 →  원본 어셈블리어 파싱 및 가상 CPU가 읽을 수 있는 코드로 변환 → 구조체1,2,3 생성 → Prolog & Epilog 핸들러 추가 → fake 핸들러 추가 → 다음 핸들러를 가리키기 위한 핸들러 번호 및 명령어에 대한 데이터 암호화 → 파일에 저장

1) 코드 가상화

Code Virtualizer는 코드 가상화를 위해 가상화에 필요한 정보가 담긴 3개의 구조체를 생성합니다.

첫 번째 구조체 생성을 위해 가장 먼저 하는 일은 가상화 대상의 어셈블리어 코드를 기반으로 아래와 같은 텍스트 형태의 어셈블리어를 생성합니다.

section .code base 0000000h code
MOV dword ptr [EBP + 0fffffff8h], 012345678h
PUSH 000411919h // 복귀를 위해 Code Virtualizer가 추가
RET


LOAD DWORD 0x12345678
가상 CPU는 일반 CPU와 명령어를 읽는 구조가 다르므로, 이를 가상 CPU가 인식할 수 있는 문법으로 변환합니다.  (0x41EFB8  함수)

MOVE ADDR, DWORD PTR [0xF0000030] // F0000030는 EBP에 대한 범용 레지스터 변수를 의미. 추후 다룸
ADD ADDR, 0xFFFFFFF8 
STORE DWORD PTR [ADDR]
LOAD 0x411919h 
RET

이를 가지고 가상 CPU의 명령어, 접미어가 담긴 구조체.1을 생성합니다. 

구조체.1는 14바이트입니다. x64 파일인 경우 4바이트 명령어 데이터가 추가되어 18바이트가 되지만 다루지 않겠습니다.

0x00 1바이트 : 가상 CPU 명령어 번호

0x01 2바이트 : 미사용 (0x00, 0x00)

0x03 1바이트 : 0x80일 경우 명령어에 상대 주소가 있음을 의미함

0x04 4바이트 : 명령어에 대한 접미어 번호

0x08 4바이트 : 명령어에 대한 데이터

0x0C 2바이트 : 미사용 (0x00, 0x00) 

 

가상 CPU의 명령어는 52개, 접미어는 36개가 있습니다. 아래는 예제에서 사용하는 일부입니다.

가상CPU 명령어 번호 명령어 가상CPU 접미어 번호 접미어
0x00 LOAD 0x03 %sADDR, %.8x%h
0x01 STORE 0x06 DWORD PTR %s[ADDR]
0x02 MOVE 0x0E ADDR, DWORD PTR %s[%.8x%h]
0x03 IFJMP 0x11 %s%.8x%h
0x08 ADD 0x1A DWORD
0x0A CMP 0x1E FLAGS
0x33 RET 0x22 %sDWORD %d

표에 대응하여 아래와 같은 구조체.1을 생성할 수 있습니다.

 

struct.1 example
0x00 0x0000 0x00 0x00000022 0x12345678 0x0000 // LOAD DWORD 0x12345678
0x02 0x0000 0x80 0x0000000E 0xF0000030 0x0000 // MOVE ADDR, DWORD PTR [0xF0000030]
0x08 0x0000 0x00 0x00000003 0xFFFFFFF8 0x0000 // ADD ADDR, 0xFFFFFFF8
0x01 0x0000 0x00 0x00000006 0x00000000 0x0000 // STORE DWORD PTR [ADDR]
0x00 0x0000 0x00 0x00000011 0x00411919 0x0000 // LOAD 0x411919
0x33 0x0000 0x00 0x00000000 0x00000000 0x0000 // RET

이제 구조체.1을 바탕으로 구조체.2를 생성합니다.

Reversing Tip
전역변수 0x70AB68에 구조체.2 주소가 저장됩니다.
0x60A38E 함수에서 구조체.1을 구조체.2에 복사하는 작업을 수행합니다.

구조체.2는 40바이트입니다.

0x00 4바이트 : 카운터 (구조체마다 0xE씩 증가)

0x04 4바이트 : 원본 명령어가 담겨있는 메모리 주소 (명령어에 대한 첫 구조체마다)

0x08 4바이트 : 몰라

0x0C 4바이트 : 카운터에 0xE를 더한 값

0x10 1바이트 : if/switch 등 조건문 명령어가 jnz 등 인 경우 TRUE

0x11 1바이트 : (구조체.1) 가상 CPU 명령어 번호

0x12 4바이트 : (구조체.1) 명령어에 대한 접미어 번호

0x16 4바이트 : (구조체.1) 명령어에 대한 데이터

0x1A 4바이트 : (구조체.1) 명령어에 대한 x64 확장 데이터 (x86에선 0x00 0x00 0x00 0x00)

0x1E 2바이트 : (구조체.1) 미사용 (0x00, 0x00)

0x20 7바이트 : 몰라

0x27 1바이트 : 명령어에 상대주소가 있으면 TRUE (구조체.1 0x03이 0x80인 경우)

 

(구조체.1)로 표시된 것은 구조체.1에서 그대로 복사하여 사용합니다.

0x04BF000A c7 45 f8 78 56 34 12 mov dword ptr [ebp-8], 0x12345678
0x04BF0011 68 19 19 41 00       push 0x411919
0x04BF0016 C3                   ret​


구조체.2의 0x04에 위와 같은 원본 코드가 담겨 있습니다.

struct.2 example
0x00000000 0x04BF000A 0x00000000 0x0000000E 0x00 0x00 0x00000022 0x12345678 0x00000000 0x0000 00000000000000 0x00 // LOAD DWORD 12345678h
0x0000000E 0x00000000 0x00000000 0x0000001C 0x00 0x02 0x0000000e 0xf0000030 0x00000000 0x0000 00000000000000 0x01 // MOVE ADDR, DWORD PTR [0xF0000030]
0x0000001C 0x00000000 0x00000000 0x0000002A 0x00 0x08 0x00000003 0xfffffff8 0x00000000 0x0000 00000000000000 0x00 // ADD ADDR, 0xFFFFFFF8
0x0000002A 0x00000000 0x00000000 0x00000038 0x00 0x01 0x00000006 0x00000000 0x00000000 0x0000 00000000000000 0x00 // STORE DWORD PTR [ADDR]
0x00000038 0x04BF0011 0x00000000 0x00000046 0x00 0x00 0x00000011 0x00411919 0x00000000 0x0000 00000000000000 0x00 // LOAD 0x41191A
0x00000046 0x04BF0016 0x00000000 0x00000054 0x00 0x33 0x00000000 0x00000000 0x00000000 0x0000 00000000000000 0x00 // RET

 


마지막으로 구조체.1 및 구조체.2를 가지고 구조체.3을 생성합니다.

Reversing Tip
전역변수 0x70A810에 구조체.3 주소가 저장되어 있습니다.
0x606D23 함수에서 구조체.3에 값을 복사하는 작업을 수행합니다.

구조체.3은 24바이트입니다.

0x00 (2바이트) : 핸들러 번호

0x02 (4바이트) : 구조체.2 메모리 주소

0x06 (4바이트) : 암호화 된 코드 메모리 주소 (Code Virtualizer.exe에서의 주소)

0x0A (1바이트) : 명령어에 대한 데이터를 몇 바이트를 읽을 것인가? 읽을 필요가 없는 경우 0 (즉, 핸들러에 LODS 명령어가 포함되어 있지 않은 경우), LODS가 포함되어 있다면 핸들러에 따라 1(BYTE), 2(WORD), 4(DWORD), 8(QWORD)

0x0B (1바이트) : 구조체.2에 상대주소가 있거나 접미어가 FLAGS인 경우 TRUE

0x0C (4바이트) : 명령어에 대한 데이터

0x10 (4바이트) : 명령어에 대한 x64 확장 데이터 (x86에선 0x00 0x00 0x00 0x00)

0x14 (4바이트) : 암호화 된 데이터 (target.exe)

 

핸들러에 대한 설명이 필요한 경우, 2절을 미리 읽고 오시는 것을 추천합니다.

0x60802B 함수에서 구조체.3을 만드는데, 구조체.2의 0x11, 0x12, 0x27에 따라 필요한 핸들러의 갯수와 각 핸들러마다 구조체.3의 0x00, 0x0A, 0x0B, 0x0C를 지정합니다.

만약 명령어에 상대 주소가 있는 경우(구조체.2 0x27), 0x0C는 본래의 값에 특수한 연산을 취한 값을 가지게 됩니다.

0x06, 0x14는 암호화가 수행될 때 작성됩니다.  (3절에서 다룸)

struct.3 example
0x015B 0xFFFFFFFF 0x00000000 0x00 0x00 0x00000000 0x00000000 0x00000000 // Prolog Signature (FIX)
0x0004 0x04790000 0x09C5002F 0x04 0x00 0x12345678 0x00000000 0x004219df // LOAD DWORD 0x12345678
-> 바이너리 : ee(핸들러 번호) 2f 9f d6 8c(명령어 데이터)
0x0007 0x04790028 0x09C50034 0x01 0x01 0x00000000 0x00000000 0x004219e4 // MOVE ADDR, DWORD PTR [F0000030h]
-> 바이너리 : ed(핸들러 번호) 82(명령어 데이터)
0x0001 0x04790028 0x09C50036 0x00 0x00 0x00000000 0x00000000 0x004219e6
-> 바이너리 : 10(핸들러 번호)
0x0009 0x04790050 0x09C50037 0x00 0x00 0x00000000 0x00000000 0x004219e7 // ADD ADDR, 0xFFFFFFF8
-> 바이너리 : 99(핸들러번호)
0x0004 0x04790050 0x09C50038 0x04 0x00 0xfffffff8 0x00000000 0x004219e8
-> 바이너리 : c1(핸들러 번호) f2 f3 6d 94(명령어 데이터)
0x0006 0x04790050 0x09C5003D 0x00 0x00 0x00000000 0x00000000 0x004219ed
-> 바이너리 : 2a(핸들러 번호)
0x0001 0x04790050 0x09C5003E 0x00 0x00 0x00000000 0x00000000 0x004219ee
-> 바이너리 : 79(핸들러 번호)
0x0018 0x04790078 0x09C5003F 0x00 0x00 0x00000000 0x00000000 0x004219ef // STORE DWORD PTR [ADDR]
-> 바이너리 : 99(핸들러 번호)
0x0004 0x047900A0 0x09C50040 0x04 0x00 0x00411919 0x00000000 0x004219f0 // LOAD 0x411919
-> 바이너리 : 93(핸들러 번호) e1 8a ac 94(명령어 데이터)
0x015D 0x047900C8 0x09C50045 0x00 0x00 0x00000000 0x00000000 0x004219f5 // RET (해당 라인은(0x15D)은 다음 명령어가 없을 경우에만 추가됨)
-> 바이너리 : 52(핸들러 번호)
0x0154 0x047900C8 0x09C50046 0x04 0x00 0xfffffffe 0x00000000 0x004219f6
-> 바이너리 : a4(핸들러 번호) 01 00 00 00(명령어 데이터)
0x0161 0xFFFFFFFF 0x09C5004B 0x00 0x00 0x00000000 0x00000000 0x004219fb // Epilog (키를 0으로 설정)
-> 바이너리 : 1b(핸들러 번호)
0x015C 0xFFFFFFFF 0x00000000 0x00 0x00 0x00000000 0x00000000 0x00000000 // Epilog Signature (FIX)


MOVE ADDR, DWORD PTR [F0000030h]의 경우, 해당 명령어를 구현하기 위해 2개의 핸들러(0x7, 0x1)가 필요합니다.

해석해보면, 명령어에 대한 데이터를 메모리에서 1바이트 읽어와서 0x7번 핸들러에서 처리 후 0x1번 핸들러를 실행하면 MOVE ADDR, DWORD PTR [F0000030h]이 실행되는 것입니다.

구조체의 핸들러 번호를 순차적으로 실행하면, 6줄의 명령(LOAD~RET)이 모두 실행됩니다. 

 

구조체.3에서는 시작과 끝에 핸들러가 추가됩니다. 0x015B, 0x015C는 핸들러가 아닌 단순한 Signature이며, Code Virtualizer는 구조체.3 파싱 도중 이를 인식하면 시작과 끝에 Prolog 와 Epilog 핸들러 체인을 추가합니다.

구조체.2의 0x10이 TRUE인 경우, ebx(키, 추후 다룸)를 초기화 하는 0x161 핸들러가 추가됩니다. 확인해보지는 않았지만 실행 중 ebx가 변하면서 key를 예측할 수 없는 경우를 염두한 것 같습니다.

 

계속해서 데이터 값에 F0000030  혹은 03을 보아왔는데, 이 값들은 target의 범용 레지스터 변수 주소를 가리킵니다.

Prolog 핸들러는 각 블록(VIRTUALIZER_START~END)에서 가상화에 사용하기 위해 메인핸들러에서 PUSHAD 및 PUSHFD로 백업한 레지스터들을 레지스터 배열(Code Virtualizer 베이스 주소에 할당)에 저장합니다.

target에 각 레지스터가 저장되는 배열 위치는 키(핸들러 구조체 키와 동일한) 체인에 의해 생성됩니다. 이는 일반적으로 통용되는 공격을 막기 위함입니다. 

 

구조체.3의 03은 아래의 수식으로 Code Virtualizer.exe의 레지스터 배열 위치를 가리킵니다.

6(array no) = (0xF0000030(데이터) + 0x10000000) >> 3 

 

array no. Code Virtualizer.exe (fixed) 레지스터 데이터 target 배열 위치 예시
0 0x70A814 EAX 0xF0000000 3
1 0x70A818 EBX 0xF0000008 2
2 0x70A81C ECX 0xF0000010 6
3 0x70A820 EDX 0xF0000018 5
4 0x70A824 ESI 0xF0000020 4
5 0x70A828 EDI 0xF0000028 1
6 0x70A82C EBP 0xF0000030 0
7 0x70A830 EFLAGS 0xF0000038 7

 

아래는 target에서 메인 핸들러 진입 전의 레지스터입니다.

Prolog 핸들러 실행 후 아래와 같이 배열에 저장됩니다. (예시이므로 순서는 항상 다를 수 있습니다.)

2) 핸들러

핸들러는 가상 opcode를 처리하기 위해 존재합니다. 가상 CPU라고 봐도 무방합니다. Code Virtualizer는 모든 코드를 핸들러 단위로 처리합니다.

각각의 핸들러는 opcode와 접미어가 결합된 형태로 구성되어 있습니다. 총 150개가 존재하며, 메인 핸들러는 별도로 존재합니다.

이해를 돕기위해 LOAD DWORD 12345678h가 있다고 가정합니다.

가상 CPU 명령어 LOAD는 명령어 데이터를 push하는 명령어인데, 접미어에 따라 push하는 바이트 수가 다릅니다.

명령어에 대한 데이터는 ".v-lizer"섹션 메모리에 저장되어 있습니다. 접미어가 DWORD이므로 메모리에서 4바이트를 읽습니다. 

이를 어셈블리어로 구현하면 아래와 같이 됩니다.

LODS DWORD PTR DS:[ESI]
PUSH EAX

이러한 코드를 가진 핸들러가 0x4번에 구현되어 있습니다. 따라서 구조체.3의 "LOAD DWORD 12345678h"에 0x4번 핸들러를 읽으라고 지정된 것입니다.

만약 LOAD WORD이라면, 아래와 같이 됩니다. 

LODS WORD PTR DS:[ESI]
MOVZX EAX,AX
PUSH AX

이 또한 0x3번 핸들러에 구현되어 있습니다.

즉, 모든 상황에 맞는 prebuild된 핸들러가 존재한다고 보시면 됩니다.

LOAD 처럼 단일 핸들러로 처리할 수 있는 명령어도 있지만, MOVE처럼 2개이상의 핸들러를 필요로 하는 명령어도 있습니다. 이 경우 여러 개의 핸들러가 실행됩니다. (구조체.3 참고)

 

수 많은 핸들러들을 다루기 위해 Code Virtualizer는 핸들러 구조체를 만들어 사용합니다. 핸들러 구조체(0x603B72)는 아래와 같은 16바이트 구조로 되어 있습니다.

0x00 (2바이트) : 핸들러 번호 (0x0~0x161)

0x02 (4바이트) : Code Virtualizer에서의 핸들러 시작 주소

0x06 (4바이트) : Code Virtualizer에서의 핸들러 끝 주소

0x0A (4바이트) : target(가상화 할)에서의 핸들러 시작 주소

0x0E (2바이트) : target에서의 핸들러 번호 (0x0E~0xA3)

 

파일을 가상화하기 전에는 아래와 같이 초기화되어 있으며,

(Code Virtualizer.exe 프로세스 메모리 일부, 이하 생략)

 

가상화가 끝나면 아래와 같이 채워집니다. target에서의 핸들러 번호는 키 체인에 의해 생성됩니다. (0x6084E9 함수)

중간중간에 비어있는 번호가 보이는데, 이 중 0x015B, 0x015C 는 실제로 존재하는 핸들러가 아닌 구조체.3의 START, END를 식별하는 Signature입니다.

 

이해를 돕기위해 예시를 들어보겠습니다. 0x1번 핸들러는 아래와 같습니다.  (단지 pop edx만 수행)

그러나 target의 핸들러는 모양이 다른데, 이는 Code Virtualizer가 복잡성을 증가시키기 위한 코드를 삽입하였기 때문입니다. target 파일마다 다르게 생성되므로 분석가는 핸들러를 특정할 수 없게 됩니다.

아래는 가상 머신의 복잡성 레벨을 0으로 설정했을 경우입니다. (복잡성 레벨은 0~3) 레벨이 높을 수록 삽입되는 코드가 많아집니다.

handler 0x1 example
PUSH DWORD PTR SS:[ESP]
MOV EDX,DWORD PTR SS:[ESP]
ADD ESP,0x4
ADD ESP,0x4

언뜻 보기엔 복잡하게 보이지만, 실제 수행은 동일합니다.

LODS 명령어가 없는 핸들러의 경우는 데이터 복호화 로직이 없기 때문에, 코드들을 전부 NOP처리한 후, pop edx 명령어만 붙여넣어도 코드는 정상적으로 수행됩니다. 

150개의 핸들러 주소는 target파일의 ".v-lizer" 섹션에 추가됩니다. ".v-lizer"섹션은 원본의 ".resource" 섹션에 핸들러 주소와 핸들러 코드가 추가된 형태입니다. 

Code Virtualizer.exe의 0번째 핸들러(0x41F566)는 정확히 0x1F (0x2D - 0xE) 번째에 존재하는 것을 볼 수 있습니다.

 

여기까지 보셨으면 아마 의문이 드셨을 겁니다. 다음 핸들러의 위치는 어떻게 가리키는가? 예를들어, 구조체.3의 예시의 경우 0x4 → 0x7 인데, 0x4 핸들러에서 0x7 핸들러로 어떻게 이동하는가?

답은 NEXT 핸들러를 사용하여 다음 핸들러를 가리키게 됩니다. NEXT 핸들러는 인덱스(번호)를 이용하여 위의 ".v-lizer"섹션 메모리에서 다음 핸들러의 주소를 찾습니다.   

target을 실행하면 메인핸들러 → NEXT 핸들러 → Prolog 핸들러(여러개) → NEXT 핸들러 → 0x4 → NEXT 핸들러 → 0x7 → NEXT 핸들러 → .... → Epilog 핸들러(여러개) 순으로 실행됩니다.

 

2-1) 메인 핸들러 (+ NEXT 핸들러)

메인 핸들러는 난독화 코드 block에서 최초로 실행되는 핸들러입니다.

 

두 그림 모두 메인 핸들러로, 위는 Code Virtualizer.exe, 아래는 target.exe입니다.

target의 경우는 ASLR로 인하여 임의 주소로 변경되었습니다.

메인 핸들러 호출 직전에 암호화 된 바이너리 주소가 push됩니다.

 

메인 핸들러 실행 절차는 아래와 같습니다.

(1)범용 레지스터들과 EFLAGS 레지스터를 백업합니다. 

(2) 난독화 시 이미지 베이스 0x400000 고정으로 계산되므로 필요 시 Relocation을 하여야 합니다. 이를 위해 target에 저장할 때 해당 라인(eip)의 주소를 기입합니다.   

pop edi시 edi에 0xEDF898이 저장됩니다. 따라서 Relocation 계산에 필요한 값은 0x32F898-0x41F898 = 0xFFF10000이 됩니다. 

다음으로, 0x41F600(Code Virtualizer 베이스) + 0x2C 주소에 해당 값이 있는지 확인합니다. 값이 없으면 Relocation을 수행합니다.

(3) Relocation 값을 0x32F62C에 저장하고, 핸들러 갯수 (96개) 만큼 핸들러 주소를 Relocation을 합니다.

녹색 줄은 Relocation 완료, 파란색 줄은 미완료입니다.

 

(4) push한 0x4229B0(ASLR로 인해 .rsrc 섹션이 추가되어 주소가 변경됨)을 꺼내와 키(ebx)로 사용합니다.

다음으로, 현재 모듈의 암호화 된 바이너리 주소를 esi에 저장하여 NEXT 핸들러에서 참조할 수 있도록 합니다.

(5) Code Virtualizer 베이스 + 0x30에 TRUE를 설정합니다. (가상화 실행 중 표시)

 

각 핸들러 끝에는 다음 핸들러를 가리키는 0x604520의 NEXT핸들러가 추가됩니다.

 

여기에 암호화 된 핸들러 번호를 복호화하는 코드를 추가하여 target에 저장합니다. (3절 참조)

added decrypt code (example)
LODS BYTE PTR DS:[ESI]
SUB AL,BL
ADD AL,0x1D
ADD AL,0x96
SUB BL,AL
MOVZX EAX,AL
JMP DWORD PTR DS:[EDI+EAX*4]

 

2-2) Prolog 핸들러 (레지스터 저장)

정의된 핸들러의 조합으로 Prolog를 구성합니다.  Prolog에서는 PUSHAD, PUSHFD 를 역순으로 가져와 Code Virtualizer 변수에 저장 후 POP을 수행합니다.

실행 순서는 아래와 같습니다. (0x608F43 함수에 정의되어 있음) 모든 핸들러가 수행되어야만 레지스터가 변수에 저장되고, 스택이 정리됩니다.

0x0 ->  0x1 -> 0x18 (0x0 : EFLAGS 저장)

→ 0x0 -> 0x1 -> 0x18 (0xF0000028 : EDI 저장)

→ 0x0 -> 0x1 -> 0x18 (0xF0000020 : ESI 저장)

→ 0x0 -> 0x1 -> 0x18 (0xF0000030 : EBP 저장)

→ 0x0 -> 0x1 -> 0x18 (0xF0000008 : ESP 저장) // ESP는 저장할 필요가 없지만 POP을 위해 임시로 저장함

→ 0x157 (0xF0000010 : ECX의 인덱스를 저장)

→ 0x0 -> 0x1 →  0x18 (0xF0000008 : EBX 저장)

→ 0x0 -> 0x1 →  0x18 (0xF0000018 : EDX 저장)

→ 0x0 -> 0x1 →  0x18 (0xF0000010 : ECX 저장)

→ 0x0 -> 0x1 →  0x18 (0xF0000000 : EAX 저장)

→ 0x14E →  0x9 → 0x4 →  0x6 →  0x14D (스택 정리 : ESP원복)

 

예시는 베이스 주소(EDI)를 0x41E600으로 합니다. 괄호 안은 핸들러 번호입니다.

아래는 target의 메인 핸들러 진입 전 스택 구성입니다. (암호화 된 데이터 0x4219B0가 PUSH 되었음)

[0x00] LODS BYTE PTR DS:[ESI]
[0x00] MOVZX EAX,AL                     // EAX = 7(배열 위치)
[0x00] LEA EAX,DWORD PTR DS:[EDI+EAX*4] // 41E61C
[0x00] PUSH EAX
[0x01] POP EDX
[0x18] POP DWORD PTR DS:[EDX]           // EFLAGS = 41E61C
 
[0x00] LODS BYTE PTR DS:[ESI]
[0x00] MOVZX EAX,AL                     // EAX = 1
[0x00] LEA EAX,DWORD PTR DS:[EDI+EAX*4] // 41E604
[0x00] PUSH EAX
[0x01] POP EDX
[0x18] POP DWORD PTR DS:[EDX]           // EDI = 41E604
 
[0x00] LODS BYTE PTR DS:[ESI]
[0x00] MOVZX EAX,AL                     // EAX = 4
[0x00] LEA EAX,DWORD PTR DS:[EDI+EAX*4] // 41E610
[0x00] PUSH EAX
[0x01] POP EDX
[0x18] POP DWORD PTR DS:[EDX]           // ESI = 41E610
 
[0x00] LODS BYTE PTR DS:[ESI]
[0x00] MOVZX EAX,AL                     // EAX = 0
[0x00] LEA EAX,DWORD PTR DS:[EDI+EAX*4] // 41E600
[0x00] PUSH EAX
[0x01] POP EDX
[0x18] POP DWORD PTR DS:[EDX]           // EBP = 41E600
 
[0x00] LODS BYTE PTR DS:[ESI]
[0x00] MOVZX EAX,AL                     // EAX = 2
[0x00] LEA EAX,DWORD PTR DS:[EDI+EAX*4] // 41E608
[0x00] PUSH EAX
[0x01] POP EDX
[0x18] POP DWORD PTR DS:[EDX]           // ESP = 41E608 ; 임시저장
 
[0x0157] LODS BYTE PTR DS:[ESI]
[0x0157] MOV BYTE PTR DS:[EDI+0x28],AL  // EDI = 41E628(41E600 + 0x28), EAX = 6(ECX의 인덱스)
 
[0x00] LODS BYTE PTR DS:[ESI]
[0x00] MOVZX EAX,AL                     // EAX = 2
[0x00] LEA EAX,DWORD PTR DS:[EDI+EAX*4] // 41E608
[0x00] PUSH EAX
[0x01] POP EDX
[0x18] POP DWORD PTR DS:[EDX]           // EBX = 41E608 ; 덮어 씌움
 
[0x00] LODS BYTE PTR DS:[ESI]
[0x00] MOVZX EAX,AL                     // EAX = 5
[0x00] LEA EAX,DWORD PTR DS:[EDI+EAX*4] // 41E614
[0x00] PUSH EAX
[0x01] POP EDX
[0x18] POP DWORD PTR DS:[EDX]           // EDX = 41E614
 
[0x00] LODS BYTE PTR DS:[ESI]
[0x00] MOVZX EAX,AL                     // EAX = 6
[0x00] LEA EAX,DWORD PTR DS:[EDI+EAX*4] // 41E618
[0x00] PUSH EAX
[0x01] POP EDX
[0x18] POP DWORD PTR DS:[EDX]           // ECX = 41E618
 
[0x00] LODS BYTE PTR DS:[ESI]
[0x00] MOVZX EAX,AL                     // EAX = 3
[0x00] LEA EAX,DWORD PTR DS:[EDI+EAX*4] // 41E60C
[0x00] PUSH EAX
[0x01] POP EDX
[0x18] POP DWORD PTR DS:[EDX]           // EAX = 41E60C
 
[0x014E] MOV EDX,ESP  // 스택 정리한 후, EDX = ESP
[0x0009] PUSH EDX
 
[0x0004] LODS DWORD PTR DS:[ESI]        // 나머지 스택 정리
[0x0004] PUSH EAX // 4 (고정)
[0x0006] POP EAX
[0x0006] ADD DWORD PTR SS:[ESP],EAX
[0x014D] POP ESP                        // ESP = 0x19FE1C ; ESP 복원



2-3) Epilog 핸들러

Prolog에서 백업해 놓은 범용 레지스터 및 EFLAGS 레지스터를 스택에 PUSH합니다. PUSHFD, PUSHAD와 동일한 기능을 수행합니다.

마지막으로 POPAD, POPFD로 레지스터들을 원복시킵니다.

최종적으로 원래 주소로 RETN합니다.

실행 순서는 아래와 같습니다. (0x608A4B 함수에 정의되어 있음)

0x0 → 0x1 → 0xC (0x0 : EFLAGS 복원)

→ 0x0 → 0x1 → 0xC (0xF0000000 : EAX 복원 )

→ 0x0 → 0x1 → 0xC (0xF0000010: ECX 복원)

→ 0x0 → 0x1 → 0xC (0xF0000018 : EDX 복원)

→ 0x0 → 0x1 → 0xC (0xF0000008 : EBX 복원)

→ 0x0 → 0x1 → 0xC (0xF0000008 : EBX 복원) // 임시로 아무 레지스터나 복원하여 공간만 만들어 놓음. ESP는 복원하지 않아도 나중에 맞춰짐

→ 0x0 → 0x1 → 0xC (0xF0000030 : EBP 복원)

→ 0x0 → 0x1 → 0xC (0xF0000020 : ESI 복원)

→ 0x0 → 0x1 → 0xC (0xF0000028 : EDI 복원)

→ 0x14A (스택 정리 및 RETN)

9회 실행
[0x00] LODS BYTE PTR DS:[ESI]
[0x00] MOVZX EAX,AL                    
[0x00] LEA EAX,DWORD PTR DS:[EDI+EAX*4]
[0x00] PUSH EAX
[0x01] POP EDX
[0x0C] PUSH DWORD PTR DS:[EDX]  // PUSH EFLAGS, EAX, ECX, EDX, EBX, EBX, EBP, ESI, EDI
 
[0x14A] MOV DWORD PTR DS:[EDI+0x30],0x0  // 가상화 실행 OFF를 의미
[0x14A] POPAD
[0x14A] POPFD
[0x14A] RETN

 

3) 핸들러 번호 및 명령어 데이터 암/복호화 

target의 ".v-lizer" 섹션에 구조체.3의 핸들러 번호와 명령어 데이터를 담은 바이너리를 저장하는데, 키 및 암/복호화 구조체를 가지고 암호화(SUB/ADD/XOR)를 한 후 저장합니다.  

키의 경우 최초로 설정된 값은 target에서의 암호화 된 바이너리가 저장된 address이며, 각 핸들러 실행 시마다 계속 변하게 됩니다. 체인 형태로 되어 있어 이전 핸들러가 실행되어야만 다음 핸들러의 키 값을 알 수 있습니다. 

복호화를 위해 Code Virtualizer는 target의 각 핸들러(150개 + NEXT핸들러)에 복호화 코드를 추가합니다. 복호화는 암호화의 역순으로 합니다. 가령, 암호화 시에 1.SUB → 2.ADD → 3.ADD를 수행했다면, 복호화 시에 3.SUB → 2.SUB → 1.ADD를 수행하는 코드를 추가합니다.

복호화 코드는 핸들러에 LODS명령어(암호화 된 바이너리를 꺼내오는)가 있을 경우에만 추가됩니다. 

Reversing Tip
암호화 로직 생성 : 0x6087BB
복호화 로직 생성 : 0x6081A9

LODS가 읽어들이는 바이트 수(1/2/4)에 따라 동일 사이즈로 암/복호화를 하게 됩니다.

LODS 명령어 바로 밑에 4개의 복호화 명령어가 추가됩니다. 이는 암호화 시 생성되는 암/복호화 구조체([0x70A870])와 밀접하게 연관되어 있습니다.

 

암/복호화 구조체는 2가지 경우에 사용됩니다.

암호화 : 핸들러 번호 암호화 (NEXT핸들러 구조체 사용), 각 핸들러에서 처리되는 명령어에 대한 데이터 1/2/4바이트 암호화  

복호화 : NEXT 핸들러에 복호화 로직 추가 (NEXT 핸들러 구조체 사용), 각 핸들러에 복호화 로직 추가  

 

암/복호화 구조체에는 4개의 암/복호화 명령어 타입과 2개의 랜덤키가 정의되어 있습니다.

0x00 (2바이트) : 핸들러 번호 (0x0~0x161, NEXT 핸들러의 경우 0x0으로 정의되어 있음)

0x02 (1바이트) : 명령어.3 에 대한 암,복호화 타입 

0x03 (1바이트) : 명령어.2 에 대한 암,복호화 타입 

0x04 (1바이트) : 명령어.1 에 대한 암,복호화 타입 

0x05 (1바이트) : 명령어.4 에 대한 암,복호화 타입 

0x06 (4바이트) : 명령어.3 에 대한 랜덤 키

0x0A (4바이트) : 명령어.2 에 대한 랜덤 키

 

암/복호화 구조체의 명령어.1~4 타입 및 랜덤키는 키 체인(다음 키는 이전 키의 값에 영향을 받음)에 의해 생성됩니다. (0x606A40 함수에서 생성) 레지스터 변수의 인덱스 생성 시에도 동일한 키 체인이 적용됩니다.

최초 키 생성 시 SEED는 GetLocalTime() 으로 생성하는데(0x653134 함수), 암/복호화 구조체 생성 이후에 SEED를 생성하므로 최초 실행 시의 암/복호화 구조체엔  항상 동일한 SEED(0x3039)가 적용되어 아래 예제의 값은 최초 실행 시에는 고정이 됩니다.

example
0000 01 01 00 01 32A55896 10F3261D // NEXT 핸들러 및 핸들러 번호 전용
0000 00 00 00 00 62D34738 64B7CA2B // 이하 각 핸들러 및 데이터 암/복호화에 사용
0001 00 00 00 01 7D1840D9 04F96246
0002 02 02 01 00 2B2AFD01 3A7C55C9
0003 01 01 02 02 7021F1B5 1BC3CFF7
0004 01 01 02 01 78076014 0D986F9A
0006 01 02 01 02 20E473F0 6F4C262F
0007 02 02 01 00 44AD5FB7 60BFF50B
0009 02 01 02 01 7C0DFEFA 6C75F611
이하 생략

명령어 1~4의 타입에 따라 명령어가 다르게 추가됩니다. ebx는 키입니다.

타입 명령어1 명령어2 명령어3 명령어4
0 add eax, ebx(Key) add eax, RandomKey add eax, RandomKey add ebx(Key), eax
1 sub eax, ebx(Key) sub eax, RandomKey sub eax, RandomKey sub ebx(Key), eax
2 xor eax, ebx(Key) xor eax, RandomKey xor eax, RandomKey xor ebx(Key), eax

(암호화 로직에 추가되는 명령어. LODS로 읽은 바이트 수에 따라 al/ax/eax, bl/bx/ebx)

타입 명령어1 명령어2 명령어3 명령어4
0 sub eax, ebx(Key) sub eax, RandomKey sub eax, RandomKey add ebx(Key), eax
1 add eax, ebx(Key) add eax, RandomKey add eax, RandomKey sub ebx(Key), eax
2 xor eax, ebx(Key) xor eax, RandomKey xor eax, RandomKey xor ebx(Key), eax

(복호화 로직에 추가되는 명령어. LODS로 읽은 바이트 수에 따라 al/ax/eax, bl/bx/ebx)

 

구조체를 생성후 각 핸들러에 복호화 로직을 추가합니다. 

0x3번 핸들러를 예로 들어봅니다.

LODS WORD PTR DS:[ESI]
MOVZX EAX,AX
PUSH AX

구조체는 0003 01 01 02 02 7021F1B5 1BC3CFF7가 사용되었다고 가정하면 생성되는 복호화 핸들러는 아래와 같습니다.

LODS WORD PTR DS:[ESI]
XOR AX,BX
ADD AX,0xCFF7 // 랜덤키
ADD AX,0xF1B5 // 랜덤키
XOR BX,AX
MOVZX EAX,AX
PUSH AX

0x161번 핸들러 번호를 암호화 할 시에는 직후에 키가 0으로 설정됩니다. (바로 다음 암호화는 키 0을 사용)

복호화 시에는 0x161 핸들러에서 mov ebx(Key), 0을 수행합니다.

 

아래는 암호화 예시입니다. (Prolog 일부)

암호화 대상 :

1) 핸들러 번호 0x0(target : 0x2D) 

→ 2) EFLAGS 레지스터 인덱스 번호 (target : 0x7) 

→ 3)핸들러 번호 0x1(target : 0x89) 

명령어는 4 → 3→ 2→ 1순 입니다.

 

1)

최초 키 : 0x4219B0 

암호화 대상 : 0x2D (핸들러 번호)

복호화 핸들러 : NEXT 핸들러 

SUB EBX, EAX        // 4219B0-2D = 421983
SUB EAX, RandomKey  // 2D-32A55896 = CD5AA797
SUB EAX, RandomKey  // CD5AA797-10F3261D = BC67817A
ADD EAX, EBX        // BC67817A+4219B0 = BCA99B2A

핸들러에서 LODS하는 바이트가 1이므로 암호화된 값은 0x2A입니다. 

2)

키 : 0x421983

암호화 대상 : 0x7 (인덱스 번호)

복호화 핸들러 : 0x0 핸들러

ADD EBX, EAX        // 421983+7 = 42198A
ADD EAX, RandomKey  // 7+62D34738 = 62D3473F
ADD EAX, RandomKey  // 62D3473F+64B7CA2B = C78B116A
ADD EAX, EBX        // C78B116A+421983 = C7CD2AED

핸들러에서 LODS하는 바이트가 1이므로 암호화된 값은 0xED입니다.

3)

키 : 0x42198A

암호화 대상 : 0x89 (핸들러 번호)

복호화 핸들러 : NEXT 핸들러

SUB EBX, EAX        // 42198A-89 = 421901
SUB EAX, RandomKey  // 89-32A55896 = CD5AA7F3
SUB EAX, RandomKey  // CD5AA7F3-10F3261D = BC6781D6
ADD EAX, EBX        // EBX BC6781D6+42198A = BCA99B60

핸들러에서 LODS하는 바이트가 1이므로 암호화된 값은 0x60입니다.

target.exe 에 저장된 암호화 된 바이너리

 

아래는 target 프로그램 실행 시의 복호화 예시입니다.

명령어는 1 → 2→ 3→ 4순 입니다.

복호화 값은 명령어 3에서 도출되며, 4는 키 계산입니다.

1)

최초 키 : 0x4219B0 

복호화 대상 : 2A

실행 핸들러 : NEXT 핸들러

LODS BYTE PTR DS:[ESI] // 0x4219B0에서 1바이트 가져옴
SUB AL,BL       // 2A-B0=7A
ADD AL,0x1D     // 7A+1D=97
ADD AL,0x96     // 97+96=2D ; 타겟에서의 인덱스
SUB BL,AL       // B0-2D=83
MOVZX EAX,AL
JMP DWORD PTR DS:[EDI+EAX*4] // 41E600+(2D*4) = JMP [41E6B4] -> JMP 41F566 (0번 핸들러)


2)

키 : 0x83

복호화 대상 : ED

실행 핸들러 : 0번 핸들러

레지스터 변수 주소를 PUSH합니다.

LODS BYTE PTR DS:[ESI] // 0x4219B1에서 1바이트 가져옴
SUB AL,BL   // ED-83=6A
SUB AL,0x2B // 6A-2B=3F
SUB AL,0x38 // 3F-38=7
ADD BL,AL   // 83+7=8A
MOVZX EAX,AL
LEA EAX,DWORD PTR DS:[EDI+EAX*4] // EAX = 7(복호화 된 인덱스 번호), EDI = 41E600(베이스 주소)
PUSH EAX                         // EAX = 41E61C
JMP 42122C                       // NEXT 핸들러

 

3)

키 : 0x8A

복호화 대상 : 60

실행 핸들러 : NEXT 핸들러

LODS BYTE PTR DS:[ESI] // 0x4219B2에서 1바이트 가져옴
SUB AL,BL       // 60-8A=D6
ADD AL,0x1D     // D6+1D=F3
ADD AL,0x96     // F3+96=89 ; 타겟에서의 index
SUB BL,AL       // 8A-89=1
MOVZX EAX,AL
JMP DWORD PTR DS:[EDI+EAX*4] // 41E600+(0x89*4) = JMP [41E824] -> JMP 421482 (1번 핸들러)

 

4) 핸들러 난독화

LODS명령어가 존재하는 경우 복호화 코드가 추가 된 후, 핸들러의 복잡도를 높이기 위한 코드를 추가합니다.

코드 복잡성의 경우 위에서 살펴보았듯이, 단지 복잡성만 추가되고 실행 결과는 동일합니다. (0x60524F 함수에서 수행)

가상 머신의 복잡성 레벨이 높다면 코드도 복잡해집니다.

앞서 본 3번 핸들러의 경우 아래와 같이 난독화 될 수 있습니다.

LODS WORD PTR DS:[ESI]
XOR AX,BX
SUB ESP,0x2
MOV WORD PTR SS:[ESP],DI
MOV DI,0xCFF7
ADD AX,DI
MOV DI,WORD PTR SS:[ESP]
ADD ESP,0x2
SUB ESP,0x2
MOV WORD PTR SS:[ESP],BP
MOV BP,0xF1B5
ADD AX,BP
MOV BP,WORD PTR SS:[ESP]
ADD ESP,0x2
XOR BX,AX
MOVZX EAX,AX
PUSH 0x2EB2
MOV WORD PTR SS:[ESP],AX

 

5) Fake 핸들러

키가 특정 조건에 만족하는 경우 fake 핸들러가 추가되는 경우가 있습니다. (0x60693E 함수에서 수행)

이 fake 핸들러는 마치 NOP처럼 실행에 아무런 영향을 주지 않습니다. 이로 인해 분석이 어려워집니다.  

아래는 예시입니다. (구조체.3 참조)

fake 핸들러가 없는 경우 : 0x4 → 0x7 → 0x1 → 0x9 → 0x4 → 0x6 → 0x1 → 0x18 → 0x4 → 0x15D → 0x154 → 0x161

fake 핸들러가 추가된 경우 : 0x4 → 0x9 → 0x3 → 0x3 → 0x1 → 0x15 → 0x7 → 0x1 → 0x9 → 0x4 → 0x6 → 0x1 → 0x18 → 0x4 → 0x15D → 0x154 → 0x161

fake 핸들러가 추가된 경우 : 0x4 → 0x7 → 0x1 → 0x9 → 0x4 → 0x6 → 0x1 → 0x18 → 0x4 → 0x15D → 0x154 → 0x9 → 0x1 → 0xC → 0x1 → 0x15 → 0x161

fake 핸들러가 추가된 경우 : 0x4 → 0x9 → 0x9 → 0x4 → 0x6 → 0x1 → 0x15 → 0x7 → 0x1 → 0x9 → 0x4 → 0x6 → 0x1 → 0x18 → 0x4 → 0x15D → 0x154 → 0x161

fake 핸들러가 추가된 경우 : 0x4 → 0x9 → 0x0 → 0x1 → 0x15 → 0x7 → 0x1 → 0x9 → 0x0 → 0x1 → 0xC → 0x1 → 0x0 → 0x1 → 0xC → 0x1 → 0x15  0x9 → 0x4 → 0x6 → 0x1 → 0x18 → 0x4 → 0x15D → 0x154 → 0x161

 

6) target 실행 요약

Code Virtualizer를 거치면서 START 매크로는 JMP 메인핸들러로 변경되어 있습니다.

메인 핸들러에서 초기화 작업을 한 후, 스택 저장(Prolog 핸들러) → 매크로 코드 블럭에 대응하는 핸들러 → 스택 복원(Epilog 핸들러) → RETN 순으로 동작하고 마지막으로 다음 EIP로 복귀합니다.

암호화 된 바이너리 위에 평문 핸들러 번호가 기입되어 있습니다. (미기입은 명령어 데이터)

 

ppt :

프레젠테이션1.pptx
0.68MB

먼저 PowerShell이 무엇인지 잠깐 짚고 넘어가봅니다.

윈도 파워셸(Windows PowerShell)마이크로소프트가 개발한 확장 가능한 명령줄 인터페이스(CLI) 스크립트 언어이다. 객체 지향에 근거해 설계되고 있어 닷넷 프레임워크 2.0을 기반으로 하고 있다. 이전에는 마이크로소프트 셸(MSH, 코드네임 Monad)로 불리고 있었다.현재 윈도 XP, 윈도 서버 2003, 윈도 비스타, 윈도 서버 2008, 윈도 7, 윈도 8, 윈도 서버 2008 R2를 모두 지원한다.

위키백과에서 발췌한 내용입니다.

그냥 쉽게 말하면 리눅스의 bash 같은 쉘입니다.

파워쉘은 Windows에서 기본으로 제공하는데 여기에 쉘코드를 인젝션 시키는 것이 가능합니다.

이는 타겟 시스템의 기본 자원을 가지고 공격을 수행할 수 있다는 것을 의미합니다. 

실제 동작을 확인하기 위해 Metasploit으로 파워쉘용 쉘코드 스크립트를 작성해보겠습니다.

메시지박스를 실행시키는 페이로드를 불러온 뒤 파워쉘 스크립트로 저장합니다.

 

스크립트 파일이 생성되었습니다.

파워쉘을 실행시킨 뒤 생성된 내용을 그대로 붙여 넣기 합니다. 해당 코드는 x86이므로 wow64 파워쉘로 실행해야 합니다. (Windows x64의 경우)

(wow64 경로 : c:\windows\syswow64\windowspowershell\v1.0\powershell.exe)

 

아래는 결과입니다.

스크립트를 보면 알겠지만 쉘코드 스레드를 생성하여 실행시키는 간단한 내용입니다.

위의 스크립트를 페이로드로 활용하기엔 2% 부족합니다. 이를 Veil-Framework로 간단히 해결할 수 있습니다.

https://www.veil-framework.com/

Veil-Framework에서 제공하는 파워쉘 옵션과 다양한 페이로드를 결합하여 강력한 파워쉘 페이로드를 생성할 있습니다. 

첫 실행화면입니다. 39개의 페이로드가 로드되었다고 나옵니다. 이를 list 명령어로 확인할 수 있습니다.

이제부터 동일한 기능을 수행하는 페이로드를 생성할 것입니다.

파워쉘 쉘코드 인젝션을 이용할 것이므로 use 21을 입력합니다. 

 Generate를 입력합니다.

 디폴트 옵션을 사용합니다.

쉘코드를 선택하는데 tab 키를 누르면 리스트가 나타납니다. 그림과 같이 windows/messagebox 를 기입 후 적당한 값을 입력합니다.

 저장할 파일 이름을 입력합니다. 해당 파일은 bat확장자로 저장됩니다.

성공적으로 파일이 저장되었습니다.

해당 배치 파일을 실행하면 메시지박스 창이 나타납니다.

 

공격사례 : 작년에 발표된 "godmode" 익스플로잇입니다.

GradiusX, b33f 닉네임을 사용하는 해커들이 기존 발표된 익스플로잇에 파워쉘을 얹었습니다.

여기에 사용된 파워쉘은 위 실습 내용과 같으므로 패치되지 않은 IE 브라우저에서 실행해보시길 바랍니다.

http://www.exploit-db.com/exploits/35308/

프로세스 트리를 살펴보면 iexplore.exe -> powershell.exe 형태입니다.

이처럼 커맨드라인 RCE 공격 형태에서 매우 효과적인데, JRE 공격의 경우 대부분이 커맨드라인 형태이므로 유용할 것이라 생각됩니다.

 

이번에는 zip 파일 퍼징하는 방법을 포스팅 하겠습니다.

아래의 사이트를 토대로 테스트 하였습니다.

http://www.flinkd.org/2011/07/fuzzing-with-peach-part-1/

준비물 :

1. Peach 2.3.9 

작성된 plt이 2.3.9에서만 동작합니다. 이 버전에는 소스만 제공됩니다.

http://sourceforge.net/projects/peachfuzz/files/Peach/2.3.9/

2. Actice Python 2.7

http://www.activestate.com/activepython/downloads

3. 의존성 패키지

아래 파일들을 전부 다운로드합니다.

http://svn.code.sf.net/p/peachfuzz/svn/branches/Peach2.3/dependencies/py2.7-win32/

4. zip plt 파일

zip을 퍼징하기 위한 plt 파일입니다. 주요 경로 및 프로그램을 자신의 환경에 맞게 설정합니다. 

zip.xml

5. 타겟 프로그램

command 명령이 가능한 zip 프로그램을 사용하시면 됩니다.

 


 

Active Python 2.7을 설치 후, 의존성 패키지의 install.bat을 실행하면 패키지들이 한 번에 설치됩니다.

퍼징 전에 유효성 검사를 해보아야 합니다.

peach.bat -t zip.xml

만약 SEED 문제가 발생한다면,

<Strategy class="rand.RandomMutationStrategy" switchCount="1500" maxFieldsToMutate="7"/>

해당 라인을 제거해 주세요. 제거하지 않아도 퍼징은 가능합니다.

Peach Fuzzer는 매우 강력한 퍼징 프레임워크 입니다.

wav/pdf/doc 등 다양한 포맷을 퍼징할 수 있고, WinDbg와 연동하여 상세한 크래쉬 정보를 볼 수 있습니다.

본 포스팅을 따라하면  wav 포맷을 가지고 간단한 퍼징을 실습할 수 있습니다.

우선 .NET Framework 4와 WinDbg, Peach Fuzzer가 필요합니다.

저는 Windows 7 x64에서 테스트했으므로 WinDbg x64를 설치하였습니다. 정상 동작하기 위해선 반드시 64비트엔 64비트 디버거를 설치하여야 합니다.

다운로드 ::

Debugging Tools for Windows (x86) version 6.12.2.633

Debugging Tools for Windows (x64) version 6.12.2.633

http://peachfuzzer.com/ 


퍼징 준비물

1. WAV 플레이어

퍼징 타겟은 Sounder라는 프로그램으로 Command Line에서 재생 가능하고, 심플합니다.

http://www.elifulkerson.com/projects/commandline-wav-player.php

 

2. WAV 파일

퍼징을 위한 wav 샘플 파일입니다. 다운로드 한 뒤 Sounder.exe와 같은 디렉토리에 넣어주세요.

 sample.zip

 

3. PIT 파일

퍼징을 하기 위한 Peach XML 파일입니다. 뮤테이션 할 오프셋을 설정할 수 있습니다. 다운로드 한 뒤 peach 설치 디렉토리에 넣어주세요.

1.xml

본 파일을 다운로드 한 뒤 약간의 수정이 필요합니다.

<Data fileName="d:\\sample_fuzzer\\sample.wav"/>

<Param name="CommandLine" value="D:\\sample_fuzzer\\sounder.exe fuzzed.wav" />

<Param name="Executable" value="D:\\sample_fuzzer\\sounder.exe"/>

<Param name="WinDbgPath" value="C:\\Program Files\\Debugging Tools for Windows (x64)" />

빨간 부분을 자신의 경로에 맞게 수정합니다. WinDbgPath는 두 군데 수정하여야 합니다. 


이제 퍼징을 할 차례입니다.

cmd.exe를 관리자 권한으로 실행합니다.

d:\peach> peach 1.xml

이렇게 한 줄 입력하면 퍼징이 시작됩니다.

 

각종 로그는 logs 디렉토리에서 확인하면 됩니다.

자세한 튜토리얼은 http://old.peachfuzzer.com/v3/TutorialFileFuzzing.html 에서 확인하실 수 있습니다.

 

 

 

 

MiniFuzz는 마이크로소프트에서 제작된 파일 퍼징 툴입니다.

매우 단순한 툴이므로, ZIP/HWP/PDF 와 같이 정형화 된 파일 포맷을 가진 경우는 이 툴을 사용하여 만족할 만한 결과를 얻어낼 수 없습니다.

가장 심플한 버퍼 오버플로우 유발 예제를 가지고 퍼징 테스트를 해보겠습니다.

 target_fuzzer.exe

return 시 address를 덮어 씌우는 전형적인 BoF 예제입니다.

 

사용방법은 매우 간단합니다.

Browse로 타겟을 선택하고, Start Fuzzing 을 누르면 끝입니다.

퍼징 중 Access Violation을 잡아내면 로그가 남게 됩니다.

 

보다 자세한 사항은 로그파일에 남게 되는데, 그림의 logs 디렉토리에 로그가 남습니다.

 

<클릭하면 확대됩니다>

크래쉬 당시의 다양한 정보가 로그에 남게됩니다. 어느 파일을 가지고 퍼징을 했는지도 기록되는데요, 해당 샘플은 crashes 디렉토리에 있습니다.

 

 

 

 

본 포스팅은 기존 제작된 익스플로잇을 무작정 따라하는 내용을 담고 있습니다.

(천만 스크립트 키드 양성을 위한 그날까지..!) 

 


 

유형 : Stack Buffer Overflow

타겟 프로그램 : BlazeDVD Pro Player 6.1

테스트 환경 : Windows XP SP3 Eng

링크 : http://www.exploit-db.com/exploits/32737/ (제작자 : Deepak Rathore)

설명 : Playlist를 열때 Stack Buffer Overflow를 일으킵니다.


[재현]

  

[그림1 - 프로그램 실행 화면]

 

링크에서 타겟 프로그램과 Exploit을 다운로드 합니다.

Perl 설치가 귀찮으신 분은blazeExpl.plf<-- 이것을 다운로드 해 주세요.

 

[그림2 - Playlist Exploit 파일]

해당 파일을 Hex Editor로 열면 위의 그림과 같이 나옵니다.

위의 박스친 부분의 주소를 사용자PC에 맞게 고쳐줘야 하는데, ASLR이 적용되지 않은 모듈에서 jmp esp를 찾으면 됩니다.

 

[그림3 - JMP ESP 찾기]

저는 0x7C86467B에 있는 jmp esp를 사용했습니다.

 

[그림4 - Exploit 성공]

Playlist 를 열어서 그림4와 같이 나오면 성공입니다.

[분석] 

[그림5 - 버퍼를 스택에 복사]

Playlist를 로드하면 위의 코드에서 스택에 Playlist의 버퍼를 복사합니다. 

(스샷을 연속적으로 찍지 않아서 스택의 주소가 그림마다 다릅니다. 양해를..)

[그림6 - 스택 비교]

위의 그림은 스택 버퍼가 쓰여지기 전과 후의 모습입니다.

 [그림7 - RETN]

계속해서 Step over를 하면 최종적으로 그림7과 같은 코드를 만나게 되어 리턴합니다.

 

[그림 8 - RETN 시 스택]

RETN 직전 스택을 보면 ESP가 0x7C86467B를 가리키고 있는데 해당 주소는 JMP ESP 를 담고 있습니다.

말 그대로 ESP로 점프하라는 것이며, 한번 더 Step over 하면 스택에 0x10이 더해진 0x12F0D0으로 이동합니다.

 

[그림 9 - 쉘코드]

NOP후 쉘코드가 실행됩니다.

테스트 환경 : Windows XP SP3 Eng / Python 2.7.1

관련 링크 : http://www.exploit-db.com/exploits/32585/

취약점 유발 파일 :victim.m3u

 

해당 취약점은 Read Only 영역에 Writing을 시도할 때 발생합니다. (즉, stack limit를 뚫고 입력하는 상황)

PoC를 실행시키면 victim.m3u가 생성되는데, 이것을 AudioCoder에 넣고 돌리면 계산기가 실행됩니다.

정크 코드 'A'에 걸려서 메모리가 뻑난(?) 모습입니다. 해당 영역은 Read Only 입니다.

 

SEH 를 확인해보니 Overwrite 되었습니다. 바로 쉘코드로 점프합니다.

 

 

+ Recent posts