VMProtect 2 가상머신 난독화 기법

VM
Code Obfuscation

라온화이트햇 핵심연구팀 권재승

VMProtect 2 - 가상머신 난독화 기법

1. 개요

최근 업무를 함에 있어 프로그램 가상화를 접하거나 직접 개발할 일이 생겨, 솔루션으로 판매되고 있는 다양한 가상화 기법들에 대해 알아보게 되었습니다. 지피지기(知彼知己)면 백전불태(百戰不殆)라고, 무작정 가상화 개발을 시도하는 것보다는 이미 오래동안 연구 · 개발되어온 가상화 솔루션들을 먼저 살펴보기로 하였습니다.

그래서 이번 포스팅은 가상화 연구, 개발 목적으로 VMProtect 2 가상머신 아키텍처에서의 난독화 기법을 중심으로 다뤄보도록 하겠습니다.

2. VMProtect 2

VMProtect 2는 실행가능한 프로그램의 명령어를 RISC, 스택머신 기반 가상 명령어로 변환하는 가상 머신 기반 난독화(obfuscator) 프로그램입니다. 이 제품을 통해 보호된 각 바이너리에는 VMProtect의 가상 머신에서 실행될 수 있는 고유한 가상머신 명령셋을 가지고 있으며, 이 가상화된 명령어들은 네이티브 코드로 변환되지 않고 가상머신에서 실행되어 원래 프로그램와 똑같은 동작을 수행합니다.

VMProtect에서 가상 명령어를 실행하는 가상 CPU 역시 여타 다른 CPU와 마찬가지로 메모리로부터 인스트럭션을 가져와 Fetch - Decode - Execute를 거치며 프로그램을 실행해나갑니다. 하지만 여기서 VMProtect는 조금 더 가상머신의 아키텍처 분석을 어렵게 하기 위해 난독화 기법을 몇가지 추가하여 크래커와 해커로부터 프로그램 이해를 어렵게 합니다.

3. 난독화

VMProtect 2에서 사용하는 난독화는 대부분 2가지 유형으로 데드스토어(deadstore)와 불투명 분기(opaque branch)라는 것을 사용합니다. 그리고 암호화된 가상 명령어 해독을 위해 롤링 복호화(Rolling Decryption)를 하게 되는데

여기서 데드스토어(deadstore), 불투명 분기(opaque branch), 롤링 복호화(Rolling Decryption)에 대해 알아보도록 하겠습니다.

3-1. 데드스토어(deadstore)

데드스토어 난독화는 명령어들 사이에 실제 연산과 전혀 상관이 없는 쓰레기 코드(junk code)들을 추가하여 분석을 방해하는 의도로 사용됩니다. 중간중간에 삽입되어있는 이러한 명령어들은 아무소용 없으며, 이런 명령어들을 손으로 쉽게 찾아 제거해나가면 원래 실행하고자하는 명령어들만을 얻을 수 있습니다.

예시를 들기위해 VMProtect 2의 vm_exit 함수의 일부를 들고와 데드스토어를 제거해보겠습니다.

.vmp0:0000000140004149 66 D3 D7               rcl     di, cl
.vmp0:000000014000414C 58                     pop     rax
.vmp0:000000014000414D 66 41 0F A4 DB 01      shld    r11w, bx, 1
.vmp0:0000000140004153 41 5B                  pop     r11
.vmp0:0000000140004155 80 E6 CA               and     dh, 0CAh
.vmp0:0000000140004158 66 F7 D7               not     di
.vmp0:000000014000415B 5F                     pop     rdi
.vmp0:000000014000415C 66 41 C1 C1 0C         rol     r9w, 0Ch
.vmp0:0000000140004161 F9                     stc
.vmp0:0000000140004162 41 58                  pop     r8
.vmp0:0000000140004164 F5                     cmc
.vmp0:0000000140004165 F8                     clc
.vmp0:0000000140004166 66 41 C1 E1 0B         shl     r9w, 0Bh
.vmp0:000000014000416B 5A                     pop     rdx
.vmp0:000000014000416C 66 81 F9 EB D2         cmp     cx, 0D2EBh
.vmp0:0000000140004171 48 0F A3 F1            bt      rcx, rsi
.vmp0:0000000140004175 41 59                  pop     r9
.vmp0:0000000140004177 66 41 21 E2            and     r10w, sp
.vmp0:000000014000417B 41 C1 D2 10            rcl     r10d, 10h
.vmp0:000000014000417F 41 5A                  pop     r10
.vmp0:0000000140004181 66 0F BA F9 0C         btc     cx, 0Ch
.vmp0:0000000140004186 49 0F CC               bswap   r12
.vmp0:0000000140004189 48 3D 97 74 7D C7      cmp     rax, 0FFFFFFFFC77D7497h
.vmp0:000000014000418F 41 5C                  pop     r12
.vmp0:0000000140004191 66 D3 C1               rol     cx, cl
.vmp0:0000000140004194 F5                     cmc
.vmp0:0000000140004195 66 0F BA F5 01         btr     bp, 1
.vmp0:000000014000419A 66 41 D3 FE            sar     r14w, cl
.vmp0:000000014000419E 5D                     pop     rbp
.vmp0:000000014000419F 66 41 29 F6            sub     r14w, si
.vmp0:00000001400041A3 66 09 F6               or      si, si
.vmp0:00000001400041A6 01 C6                  add     esi, eax
.vmp0:00000001400041A8 66 0F C1 CE            xadd    si, cx
.vmp0:00000001400041AC 9D                     popfq
.vmp0:00000001400041AD 0F 9F C1               setnle  cl
.vmp0:00000001400041B0 0F 9E C1               setle   cl
.vmp0:00000001400041B3 4C 0F BE F0            movsx   r14, al
.vmp0:00000001400041B7 59                     pop     rcx
.vmp0:00000001400041B8 F7 D1                  not     ecx
.vmp0:00000001400041BA 59                     pop     rcx
.vmp0:00000001400041BB 4C 8D A8 ED 19 28 C9   lea     r13, [rax-36D7E613h]
.vmp0:00000001400041C2 66 F7 D6               not     si
.vmp0:00000001400041CB 41 5E                  pop     r14
.vmp0:00000001400041CD 66 F7 D6               not     si
.vmp0:00000001400041D0 66 44 0F BE EA         movsx   r13w, dl
.vmp0:00000001400041D5 41 BD B2 6B 48 B7      mov     r13d, 0B7486BB2h
.vmp0:00000001400041DB 5E                     pop     rsi
.vmp0:00000001400041DC 66 41 BD CA 44         mov     r13w, 44CAh
.vmp0:0000000140007AEA 4C 8D AB 31 11 63 14   lea     r13, [rbx+14631131h]
.vmp0:0000000140007AF1 41 0F CD               bswap   r13d
.vmp0:0000000140007AF4 41 5D                  pop     r13
.vmp0:0000000140007AF6 C3                     retn

먼저 첫번째 명령어부터 6~7개만 가져와 살펴보겠습니다. 0x140004149에 있는 첫번째 명령은 rcl di, cl 입니다. RCL은 Rotate Left Carrydi 레지스터뿐만 아니라 캐리로 인해 FLAGS에까지 영향을 미칩니다. 그 다음 di가 참조되는 부분은 0x140004158에 있는 not di입니다. not은 di를 참조하여 읽고 쓰기 때문에 지금까지 실행된 di에 관한 이전 명령어들은 모두 유효합니다. 하지만 다음으로 만나는 di를 참조하는 명령어인 0x14000415B의 pop rdi는 이전 rdi에 대한 모든 쓰기 명령어들을 무의미하게 만들기 때문에, pop rdi 이전의 di에 대한 명령어들을 제거할 수 있습니다. 즉 이전 di에 대한 명령어들은 모두 데드스토어입니다.

.vmp0:0000000140004149 66 D3 D7               rcl     di, cl
.vmp0:000000014000414C 58                     pop     rax
.vmp0:000000014000414D 66 41 0F A4 DB 01      shld    r11w, bx, 1
.vmp0:0000000140004153 41 5B                  pop     r11
.vmp0:0000000140004155 80 E6 CA               and     dh, 0CAh
.vmp0:0000000140004158 66 F7 D7               not     di
.vmp0:000000014000415B 5F                     pop     rdi

⇒ 데드스토어 제거 전

.vmp0:000000014000414C 58                     pop     rax
.vmp0:000000014000414D 66 41 0F A4 DB 01      shld    r11w, bx, 1
.vmp0:0000000140004153 41 5B                  pop     r11
.vmp0:0000000140004155 80 E6 CA               and     dh, 0CAh
.vmp0:000000014000415B 5F                     pop     rdi

⇒ 데드스토어 제거 후

여기서 shld r11w, bx, 1 명령어 역시 다음에 r11을 참조하는 명령어가 pop r11가 되므로 shld 역시 데드스토어로서 제거할 수 있습니다.

이렇게 데드스토어를 모두 제거하게 되면 위 예시 코드는 다음과 같이 줄여집니다.

.vmp0:000000014000414C 58                                            pop     rax
.vmp0:0000000140004153 41 5B                                         pop     r11
.vmp0:000000014000415B 5F                                            pop     rdi
.vmp0:0000000140004162 41 58                                         pop     r8
.vmp0:000000014000416B 5A                                            pop     rdx
.vmp0:0000000140004175 41 59                                         pop     r9
.vmp0:000000014000417F 41 5A                                         pop     r10
.vmp0:000000014000418F 41 5C                                         pop     r12
.vmp0:000000014000419E 5D                                            pop     rbp
.vmp0:00000001400041AC 9D                                            popfq
.vmp0:00000001400041B7 59                                            pop     rcx
.vmp0:00000001400041B7 59                                            pop     rcx
.vmp0:00000001400041CB 41 5E                                         pop     r14
.vmp0:00000001400041DB 5E                                            pop     rsi
.vmp0:0000000140007AF4 41 5D                                         pop     r13
.vmp0:0000000140007AF6 C3                                            retn

3-2. 불투명 분기(opaque branch)

불투명 분기는 대부분 비트 테스트, 비교(cmp*), 플래그 세팅 명령어 등과 같은 명령어들 뒤에 JCC 명령어를 통해 분기로서 나타납니다. 하지만 분기를 위해 사용한 조건과는 무관하게 분기를 하든 분기를 하지않든 결과적으로 똑같은 명령을 실행하게 되어, 무의미한 분기로서 존재하게됩니다. 이는 단순히 쓸모없는 분기점을 늘려 프로그램을 복잡하게 함으로써 분석을 어렵게하려는 의도로 프로그램 흐름에는 영향을 주지않습니다. 이렇게만 설명하면 이해하기 어려울 수 있으니 간단한 예를 들어보겠습니다.

void opaque_test(int a, int b)
{
		int tmp;
		int res = 0;
		
		if(a > 10){
			res = a*b;
		} else {
			tmp = a;
			tmp = a*a;
			tmp = tmp*b;
			res = tmp/a;
		}
		printf("res : %d\n", res);
}

int main(){
	opaque_test(11, 5);
	opaque_test(5, 11);
	return 0;
}

위와 같은 코드가 있을 때, 프로그램은 a의 값과는 상관없이 항상 res = a*b를 수행하게 됩니다.

/assets/2021-08-01/vm_1.png

결국 분기와는 전혀 상관없이 프로그램 흐름은 똑같이 흘러가는 것인데, 여기서 tmp = a*a; 와 같은 부분이 데드스토어로서 작용하는 부분인것입니다.

그리고 위의 분기들 또한 어떤 코드가 데드스토어인지 결정하고 제거해나가며 불투명 분기 또한 제거해나가다보면 난독화 해제된 코드를 얻을 수 있습니다.

불투명 분기는 한번 생성되면 그 아래로 명령어들이 중복되서 나타나게 되는데, 이렇게 여러번 불투명 분기가 중첩되면 프로그램 분석이 더욱 복잡해집니다.

/assets/2021-08-01/vm_2.png

3-3. 롤링 복호화(Rolling Decryption)

VMProtect 2는 가상 명령어를 롤링 암호화 키를 사용하여 암호화합니다. 이 키는 가상 명령어 실행시 옵코드와 명령어의 피연산자를 해독하는데 사용되며, 이는 가상 명령어들의 순서가 바뀌게되면 롤링 해독키가 손상되어 다음 가상 명령어를 해독못하게 만듭니다. 때문에 가상 명령어들이 항상 순서대로 실행되는것을 보장하여 중간에 후킹하는 것을 방지하고 정적 분석을 어렵게 만듭니다.

다음은 VMProtect 2에서 사용하는 가상 명령어 구성입니다.

/assets/2021-08-01/vm_3.png

가상 명령어는 두 개 이상의 피연산자로 구성되며, 첫 번째 피연산자는 가상 명령어의 opcode입니다. 이 opcode는 VM Handler Table에 대한 인덱스(unsigned int8)로 사용됩니다. 두번째 피연산자는 즉치값으로 사용되며 최대 8바이트까지 있을 수 있습니다.

이제 가상명령어의 피연산자 해독이 어떻게 이루어지는지, 즉 롤링 복호화 방법에 대해서 보겠습니다. 가상명령어의 피연산자 해독을 위해서는 총 5개의 변환(Transforamation)이 적용됩니다.

이 변환은 아래와 같은 13개의 명령어들로 구성이 되며, 각 명령어들의 롤링 암복호화에 사용되는 변환은 고정되지않고 변하기 때문에, 같은 방법으로 다른 가상 명령어를 해독하지 못하게 되어있습니다. 즉, 암호화된 가상 명령어들을 해독할 때 마다 그에 맞는 변환방법을 사용해줘야합니다.

XOR, NEG, NOT, AND, ROR, ROL, SHL, SHR, ADD, SUB, INC, DEC, BSWAP

먼저 첫번째 변환은 롤링 암호키를 포함하여 변환을 수행합니다. 이 롤링 암호키는 처음에 RBX에 있고, 피연산자와 롤링암호키를 포함하여 연산이 수행되기때문에 첫번째 변환에는 아래 6가지 명령어중 하나가 쓰입니다.

XOR, AND, ROR, ROL, ADD, SUB

그 후, 두번째부터 네번째까지의 변환은 해독해야할 피연산자가 들어있는 레지스터만 적용하여 변환이 수행됩니다. 이 3번의 변환은 항상 피연산자만 포함하여 연산이 진행되게 됩니다. 이 단계가 끝나고 나면 피연산자는 완전히 해독이 되고, 해독된 값은 rax 레지스터에 들어있게 됩니다.

그 후 마지막 다섯번째 변환은 롤링 해독키와 해독된 피연산자(즉치값)을 첫번째 변환에 적용하여 롤링 복호화키를 업데이트합니다. 즉 첫번째 변환과 같은 명령어를 쓰지만 연산 순서를 반대로 하면 됩니다. 다음은 예시입니다.

xor     al, bl       ; 1/5 decrypt using rolling key...
neg     al           ; 2/5 (1/3 generic transformations...)
rol     al, 5        ; 3/5 (2/3 generic transformations...)
inc     al           ; 4/5 (3/3 generic transformations...)
xor     bl, al       ; 5/5 update rolling key...

/assets/2021-08-01/vm_4.png

위에서 설명했듯이 이러한 변환은 아래의 명령어들로 구성되므로 각 암호화된 가상명령어들 마다 다른 방법으로 복호화가 이루어집니다.

XOR, NEG, NOT, AND, ROR, ROL, SHL, SHR, ADD, SUB, INC, DEC, BSWAP

따라서 아래와 같이 다양한 방식으로 복호화가 이루어집니다.

---------- example1 ----------
add al, bl
inc al
neg al
ror al, 0x06
add bl, al

---------- example2 ----------
xor ax, bx
rol ax, 0x0E
xor ax, 0xA808
neg ax
xor bx, ax

---------- example3 ----------
sub     al, bl
ror     al, 2
not     al
inc     al
sub     bl, al

4. 마치며

VMProtect 2에서 사용하는 대표적인 난독화 기법 몇가지를 알아보았습니다. VMProtect 2는 마지막 버전이 2013년에 나왔으며, 현재 이 글을 쓰는 시점의 최신버전은 3.5버전입니다. 이번 포스팅은 먼저 선행되어서 분석이 많이 된 VMProtect 2에 대해서 알아보았지만 기회가 된다면 VMProtect 3에서의 난독화 기법도 알아보도록 하겠습니다.

5. 참고자료