코드 가상화 개인 공부 [0x03]

2017. 12. 20. 21:380x03 Reversing Theory

728x90

코드 난독화 기술들을 접해보았다. 


다형성(Polymorphic) 코드


다형성 코드는 바이러스 제 4세대에서 즐겨 사용한 기법이기도 하다. 


잠시 바이러스 관점에서 설명을 하면, 바이러스는 안티 바이러스의 탐지를 피하기 위해 독특한 식별자를 사용한다.

Polymorphic 바이러스는 변조된 프로그램에 결합이 되어 있다. 


바이러스 코드는 실행할 때 마다 자기 스스로 변하게 되어 식별자를 구분하기 어렵게 한다.




다시 본래의 논문으로 돌아가보면,  다형성 코드는 코드가 코드를 수정하는 방법인데, 런타임에 코드 decode를 수행하는 방법을 이용한다고 한다.

그렇기 때문에 코드를 실행하기 전에는 원래의 코드를 확인할 수 없다. 이전 글에서 리소스에 암호화를 시켜두는 원리와 비슷하다고 생각된다.


Metamorphic 기법


바이너리 코드의 시멘틱은 보존하되, 다른 인스트럭션으로 치환하는 방법으로 시그니처 탐지를 막을 수 있다고 한다. 


Dead code 삽입

죽은 코드, 즉 프로그램의 흐름에 전혀 상관 없는 코드를 의미한다.

때때로 정상적인 코드 중간 중간에 프로그램과 관계없는 코드를 삽입하여 혼란을 야기 시킬 수 있다.


인라인어셈으로 작성을 하기 힘든사람은 c코드로 작성한 후 컴파일, 빌드를 거쳐 디스어셈블리 프로그램으로 바이너리를 열어보면 실제 instruction(명령어)

이 보일 것이다.


그럼 잠시 프로그램을 닫고 자신의 코드 일부분에 다음과 같은 구문을 추가 해두자. 

__asm{

nop

nop

nop

nop

}

nop이라는 것은 오피코드 00을 뜻하는데 아무것도 들어가 있지 않은 값이라는 의미이다.


다시 빌드 후 실제 instruction(명령어)에 가서 nop을 원하는 instruction(명령어)으로 패치만 하면 끝이다.


Control Flow Obfuscation 기법

코드의 흐름을 변경하여 분석을 방해할 수도 있다.

여기서 주의해야 할 점은 난독화 코드나 복호화 코드나 동일한 행위를 취해야 한다.

예를 들어보겠다.


인라인어셈으로 LABEL 설정을 하면 된다.

여기저기 왔다 갔다 할 수 있는 GOTO 역할을 LABEL이 대신 할 수 있다.


슈도코드를 간단하게 작성해 보겠다.


이해를 돕기위해 코드 대신 한글로 표기를 해볼까 한다.


정상적인 프로그램 상황


c0nstant는 자판기에 향한다.

자판기에 어떤 물건들이 있는지 살펴본다.

원하는 물건이 있다면 가격을 살펴본다

가격을 본 c0nstant는 동전 혹은 지폐를 삽입한다.

음료수가 뽑힌다. 

만약 기계에 돈이 남아있다면 그 돈을 거슬러 준다. 

집으로 간다.


이게 정상적인 프로그램입니다.  위의 문구들은 각각의 모듈 즉, 함수라고 가정합니다.


이제 개발자는 이러한 로직을 조금이나마 시간을 지연 시키기 위해 셔플을 진행합니다.


c0nstant는 자판기에 향한다.

// 자판기에 향하는 것 까지는 정상적인 진행입니다. 이때 c언어는 절차지향이라는 특징을 빌리게 되면 두번째 접근은 돈을 거슬러 주는 것이 될 겁니다. 개발자는 여기에서 자판기에 물건이 있는 지 살펴보는 주소를 알아서 주소에 점프시키거나, label을 이용하여 점프 시킬 수 있습니다.


그렇다면, label을 각각 적어두겠습니다.


L5만약 기계에 돈이 남아있다면 그 돈을 거슬러 준다. 

L1자판기에 어떤 물건들이 있는지 살펴본다.

L4음료수가 뽑힌다. 

L2원하는 물건이 있다면 가격을 살펴본다

L3가격을 본 c0nstant는 동전 혹은 지폐를 삽입한다.



절차지향으로 보게 되면 이 로직은 L5 -> L1 -> L4 -> L2 - > L3 으로 비정상적인 흐름입니다. 이것을 해결해보겠습니다.


c0nstant는 자판기에 향한다.

JMP L1 

// L1에 접근 

자판기에 어떤 물건들이 있는지 살펴본다.

JMP L2

// L2에 접근 

원하는 물건이 있다면 가격을 살펴본다

JMP L3

// L3에 접근 

가격을 본 c0nstant는 동전 혹은 지폐를 삽입한다.

JMP L4

// L4에 접근 

음료수가 뽑힌다. 

JMP L5

// L5에 접근 

만약 기계에 돈이 남아있다면 그 돈을 거슬러 준다. 

집에 간다.


원리가 이해가 되시나요? 

프로그램을 디버깅 하지 않고 실행하면 우리가 모르는 사이에 컴퓨터가 LABEL에 접근하면서 알아서 순서대로 접근해주겠지만,

디버깅을 하게 되면 STEP IN과 STEP OVER를 이용하여 한 줄 한 줄 분석을 해야 합니다.

지금은 비록 쉬운 예제라서 '에이... 이게 어떻게 난독화?' 라고 생각하실 수 있지만 

실제 바이너리에 적용 시켜두면 분석하고 싶지 않게 만드는 특징이 있습니다.


Register Reassignment 기법


대부분의 사람들은 EAX하면 "그거 산술 연산이지 바보야~" , ECX하면 "그건 카운터고~" 라고 할 겁니다.

저 역시 이 기법을 보기 전에는 이런 생각을 가졌었겠죠? 난독화를 꿈꾸는 개발자는 이러한 Common Sense를 꺾어버립니다.

이것이 진정 역발상이 아닐까 생각이 들기도 합니다.

EAX의 기능을 ECX가 하고 ECX의 기능을 EBX가 하고 EBX의 기능을 EAX가 처리한다면? 

사람들은 멘붕에 빠질 것입니다. 분명 나는 ECX 카운터로 알고 있는데 비교하는 곳에서 쓰이는게 아니고 주소 값이 들어가있네? 

이런 미치고 팔짝 뛰게 만들겁니다.. 으아.. 내가 레지스터의 개념을 잘 못 알고 있었나? 하면서 어느새 스톡홀름 증후군 처럼 

아..내가 개념을 잘못알고있었구나 하고 착각하게 만들 수도 있습니다. 


여기서 핵심은 레지스터 간 기능을 변경하게 되었을 때 즉, EAX의 기능을 ECX가 가지게 되었을 때 ECX의 라이프 사이클 시간 동안 EAX는 사용되면 안됩니다. 

이 방법은 프로그램의 동작에 영향도 없습니다. 그저 이름바꾸기 게임만 했을 뿐이니까요 

추가적으로 명령어 삽입도 없었고 흐름 변경이 없기 때문에 속도에도 영향이 없습니다.


DATA Obfuscation 기법


이번에는 레지스터를 변경하는게 아니라 해당 명령과 동일한 행위를 수행하는 다른 명령어를 구현해두는 것입니다.


예를 또 들어보겠습니다.


저희는 XOR이란 연산자를 잘 알고 있습니다. 

배타적 논리합 이라고도 표현하죠. 

0 0 = 0

0 1 = 1

1 0  = 1

1 1 = 0 

이라는 논리식을 소유하고 있는 아이입니다.


그렇다면, EAX의 값이 뭐가 됬든 

XOR EAX, EAX 하게 되면 이 EAX는 무조건 일치하니까 진리 값은 0이 되겠죠?


그럼 xor eax, eax를 MOV EAX, 0 으로 해도 eax는 0이 됩니다.


이런 단순한 기법은 시그니처 기반 탐지를 회피할 수 있다고 합니다.