Posts LLVM - 가상화 난독화를 수행하는 pass 제작 (1/4)
Post
Cancel

LLVM - 가상화 난독화를 수행하는 pass 제작 (1/4)

이번 목표 - 가상화 난독화 손으로 작성해보고 익히기

  • 가상화 난독화 C 코드 작성
  • LLVM으로 해당 코드의 IR 확인

우리 프로젝트의 최종 목표는 가상화 난독화이다. 하지만.. 나한테 가상화 난독화라는게 아직 낯선 개념이어서 조금 어떤 난독화인지 익힐 수 있는 시간이 필요했다.

그래서 이번 주에는 본격적인 가상화 난독화를 들어가기에 앞서, 직접 가상화된 난독화 코드를 작성해 보고 어떻게 변했는지 바이너리 코드를 확인해볼 것이다.

1. 가상화 난독화 C 코드 작성해보기

난독화를 위해, 다음 코드를 사용했다.

1
2
3
4
5
6
int adder(int a, int b){
  int c;
  c = a+b;
  c = c*100;
  return c;
}

가상화 난독화는 switch문과 같은 곳에 각 과정들을 넣어둠으로써 어떤 흐름으로 프로그램이 실행되는지를 알아볼 수 없게 만든다. Line에서 개발한 난독화 컴파일러에 대한 글의 “제어 흐름 난독화”부분이 가상화 난독화를 이해하는데 큰 도움이 되었는데, 이처럼 switch문을 이용해 모든 흐름들을 같은 수준으로 놓으면서 조건에 따라 각 순서로 진행하도록 한다.

나도 이번에 가상화 난독화된 프로그램을 작성하면서 switch문을 작성하고 실행 순서를 섞어두면서, case문 안에서도 어떤 것을 실행하는지 바로 알기 힘들도록 포인터 함수를 이용해 다시 바꿔 두었다.

이 코드를 가상화 난독화 해 본 코드는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
//각 연산자를 함수로 빼 둠
int plusOp(int a, int b) {
	return a + b;
}

int mulOp(int a, int b) {
	return a * b;
}

int adderObfuscated(int para01, int para02) {
	int (*func01)(int, int) = plusOp;
	int (*func02)(int, int) = mulOp;
  //포인터 함수 선언

	int res = 0;
  //변수 선언

	for (int i = 0; i >= 0;) {
		switch (i)
		{
		case 0:
      // 난독화 전 : c=a+b;
			para01 = func01(para01, 1);  //dummy
			para01 = func01(para01, para02);
			res = func01(res, para01);
			res = func01(-1, res); //dummy
			i = 2;
			break;
		case 1:
      // 난독화 전 : return c;
			return res;
      // i++;
      // break;
      // 어차피 return하고 끝나서 i 이동시켜 줄 필요도 없고 break;도 필요 없을 것 같은데 넣어두면 좀 더 보기 어려울 것 같기도 하다
		case 2:
      // 난독화 전 : c=c*100;
			res = func02(1, res);  //dummy
			res = func02(res, 100);
			i = 1;
			break;
		}
	}

	return 0;
}

원본 코드 한 줄당 switch문의 case 하나로 쪼개고, i에 값을 넣어서 어떤 순서로 실행할지를 설정해 두었다. 연산자도 미리 함수(plusOp, mulOp)로 정의해 놓고, 함수 포인터를 통해 연산하는 부분을 수행할 수 있도록 했다. 중간중간에 case문 안에서 dummy 코드로 복잡하게 해 보기도 하였다.

또.. 해 보면서 알았던 점인데, switch문에서 변수를 선언할 수 없어서(한다고 해도 해당 case안에서만 사용 가능함) 모든 변수 선언도 함수 선언과 마찬가지로 미리 상단에서 해 두어야 할 것 같다.

+ 더 복잡하게 하려면, i를 읽기 어렵게 하고 순서를 마구 섞어도 좋을 것 같고, dummy 코드를 지금보다 더 넣어도 더 난독화가 될 것 같다. 또 지금은 연산자만 함수로 빼 두었는데 연산자가 아니라 어떤 작업을 하는 코드? 자체를 함수로 빼 두고 실행하면 좋지 않을까 싶다.

2. LLVM으로 해당 코드의 IR 확인해보기

작성한 코드를 예전에 코드 프린트용으로 작성해둔 path를 이용해 확인해본다.

난독화 전

img

난독화 후

img

(plusOp 함수와 mulOp 함수)

img img

일단 코드가 엄청나게 길어졌는데, sw.bb라는 BasicBlock이 눈에 띈다. 이렇게 for.body BasicBlock에서 나타나는 것처럼

1
2
3
4
5
6
- Instruction :   switch i32 %1, label %sw.epilog [
    i32 0, label %sw.bb
    i32 1, label %sw.bb4
    i32 2, label %sw.bb5
  ]

어떤 값일 때, 어떤 블럭으로 이동할 것인지 알려준다. 마지막으로 ss.epliog를 실행해서 for.end로 진행하는 것으로 추정되는데, bb4에서 return이 있어서 갈 일이 없는 BasicBlock 같다.

(이렇게 난독화하고 보니까 나도 어떤 흐름인지 보기가 힘든데.. 보기 쉽게 cfg로 나타내는 방법이 있었던 것 같기도 한데 찾아봐야겠다!!)

This post is licensed under CC BY 4.0 by the author.

Contents