본 문서는 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
분석 편의성을 위해 아래 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 :