반응형


실행 파일 바이너리를 보호하기 위한 솔루션 Themida [ http://www.oreans.com ]

Themida 의 기능 중 가장 강력한 보안 기능을 꼽으라면 VM(Virtual Machine) 을 꼽을 수 있는데요.. :)


이 기능은 프로그램 개발시 보호하고 싶은 소스코드를 VM 매크로로 감싸두면

Themida 로 패킹된 파일을 실행할 때 VM 매크로로 감싼 코드 부분이...

Themida 가 제공하는 가상의 CPU 머신을 통해서 실행이 됩니다.



Themida 에서 제공하는 Virtual Machine



VM 으로 실행되는 코드의 경우 우리가 흔히 인지하는 Intel CPU 명령과는 달라서... 

실행되는 과정을 분석하기가 매우 까다롭습니다.

( 실제 소스코드 상에서 입력한 코드에 대한 디버깅이 거의 불가능합니다.. ^^;;; )


이런 강력함으로 인해 강력한 보안이 필요한 코드에 대해서 VM 을 적용하는 경우가 있는데요...

VM 을 적용할 때 몇가지 주의해야 할 점을 분석해볼까 합니다.


우선 먼저 Themida Help 문서를 보면 다음과 같이 주의점에 대해 안내가 되어있습니다. :))



Themida VM 사용 시 주의점



#1. for, while, do ... 등의 반복 루프를 피하라.



VM 이라는 기능 자체가 소프트웨어적으로 구현된 가상의 CPU 를 통해

가상의 CPU 가 해석할 수 있는 명령어를 실행하는 방식인데...

이런 방식 자체가 실제 물리 CPU 상에서 코드를 실행하는 것보다는 퍼포먼스가 떨어집니다.

( 조금 더 자세한 내용은 Ezbeat 님 블로그 - "Themida 2.1.2.0 Virtual Machine" 참고... )


MOV EAX, 1


위와 같은 코드의 경우 실제 CPU 로는 한 스텝(Intel CPU 어셈블리 명령 처리 기준)이면 실행이 가능하지만...

동일한 코드를 Themida VM 으로 실행하면 실제 CPU 기준에서는 수십에서 수백 스텝이 걸릴 수도 있습니다.


단일 코드만 해도 이렇기 때문에 수없이 반복하는 코드를 VM 안에서 실행시키면...

그 만큼 퍼포먼스가 저하됩니다..


for 루프 반복 수에 따른 Performance




#2. VM 매크로 내부에서 switch 구문이 동작하지 않을 수 있다.



우선 간단한(?) switch 구문을 살펴봅시다~ :)

VM_START
	switch (nNumber)
	{
	case 1:
		printf("Number 1\n\r");
		break;

	case 2:
		printf("Number 2\n\r");
		break;

	case 3:
		printf("Number 3\n\r");
		break;

	case 4:
		printf("Number 4\n\r");
		break;

	case 5:
		printf("Number 5\n\r");
		break;

	default:
		printf("Other...\n\r");		

	}
VM_END


위의 코드가 컴파일되면 다음과 같은 어셈블리 코드로 변환이 됩니다.




switch 구문에서 각각의 case 구문을 실행하는 방식은 주소 테이블을 참조한 JMP 입니다.

0x85108C 위치 각 case 구문에서 실행될 코드의 주소가 저장되어 있습니다.


;------------------------------------------------------

0x85108C : case 1: 일 때 참조됨 -> 0x851047

0x851090 : case 2: 일 때 참조됨 -> 0x85104E

0x851094 : case 3: 일 때 참조됨 -> 0x851055

0x851098 : case 4: 일 때 참조됨 -> 0x85105C

0x85109C : case 5: 일 때 참조됨 -> 0x851063 

;------------------------------------------------------


이 코드에 VM 이 적용되면 VM_START 가 마크된 0x851025 부분이 VM 을 호출하는 코드로 변경되고..

그 이후부터 VM_END 가 끝나는 0x851089 앞부분 까지의 코드가 변합니다..




여기서 문제가 발생하는데요...


바로 위의 캡쳐를 보면 아시겠지만... switch 구문 실행 시 참고하는 테이블은...

그 위치 그대로... 같은 내용물(case 구문에서 실행될 코드의 원래 주소)이 담겨져 있는 것을 알 수 있습니다.

( 윈도우의 ASLR 으로 인해 이미지 베이스가 변경되긴 했지만 오프셋은 동일합니다. )


VM 매크로 안의 코드는 패킹하는 과정에서 코드가 달라졌지만...

switch 구문에서 참조하는 테이블은 변경되기 전의 코드 주소를 가지고 있고...

case 구문이 실행될 때 해당 주소를 가보면... @_@ ;;; 정상적인 코드가 아니기 때문에 크래쉬가 발생합니다..



VM 매크로 영역 안에 있는 switch 구문이 컴파일될 때, 

위와 같이 매크로 영역 밖의 테이블을 참조해서 case 구문을 실행하는 형태가 될 경우는...

100% 문제가 발생합니다...


참고로 ... 간혹 switch 내부의 case 수가 적은 경우는... 

참조 테이블을 생성하지 않고 단순 조건비교( if 구문처럼 )로 처리되기도 하는데...

이런 경우는 VM 매크로가 적용된다고 해도 문제가 없습니다.. ^^;;;


안전성을 위해서는 왠만하면 VM 매크로 내부에 switch 구문을 사용하지 않는 것이 좋겠죠...




#3. 예외처리가 제대로 동작하지 않는다. try - except 를 VM 매크로로 감싸는 것을 피하라.



따지고 보면 #2 와 같은 이유에서 발생하는 문제입니다.

VM 매크로 밖의 코드에서 VM 매크로 안쪽의 코드에 접근하려다가 문제가 발생하는 거죠..

VM_START

	__try {
		int nResult = nNumber / 0;
		printf("nNumber= %d, nResult : %d\n\r", nNumber, nResult);
	}
	__except(EXCEPTION_EXECUTE_HANDLER) {
		printf("Exception !!! \n\r");
	}

VM_END

일반적으로 예외처리기 등록은 함수 시작 극초반부에서 이루어 집니다..

소스코드 상에서는 함수내에서 VM 매크로를 아무리 끌어올려도... ( 함수 여는 괄호 '{' 바로 밑에 둬도.. )

예외처리기 등록부는 항상 그보다 더 위에 있습니다... :(




예외처리기 등록은 함수 시작부분부터 0xA5102A 까지해서 이루어지고..

예외처리기 주소는 0xA51220, 예외처리기가 참고하는 scopetable 은0xA5BA30 으로.. 

둘 다 VM 매크로 영역 밖입니다.

scopetable 에는 예외처리에 대한 Filter 함수(?), Handler 함수(?)의 주소가 담겨져 있습니다.

( 조금 더 자세한 내용은 Ezbeat 님 블로그 - "Visual C++ SEH Filter, Handler 루틴" 참고... )







코드가 실행되다가 예외가 발생할 경우 예외처리기에서는 scopetable 을 참고하여...

Filter 함수 및 Handler 함수를 호출하게 되는데, 이 때 #2 와 마찬가지로 문제가 발생합니다.

scopetable 에 담겨져있는 Filter 함수 및 Handler 함수 주소는 패킹하기 전 그대로인데...

패킹 후에는 해당 주소부분이 다른 코드로 바뀌어 버린거죠.. @_@ ;;;;



개발 중인 제품에 Themida VM 매크로 적용하려는데...

뭔가 계속 문제가 발생한다... 싶으면 위와 같은 요소들을 살펴보시기 바랍니다..ㅎ



반응형
AND