Anti-Reversing

» CyKor-Seminar

목차

What is Anti-Reversing?

Obfuscation

 Strip (debugging 심볼 지우기)

 Dummy Code

 Control Flow Flattening

 Unfold

 Layout 난독화

 Virtualize

 Metamorphic

 Polymorphic

 Packing

 그 외 잡다한 것들


What is Anti-Reversing??

단순하게 말해서 Reversing을 방해하는 기법을 의미. 뭐 anti-reversing 용어에 대단한 뜻은 딱히 없어요.

일반적으로 대표적인 anti-reversing 기법들은 존재하나, 사실 너무나 많은 anti-reversing이 존재하기 때문에 단순히 “anti-reversing의 모든 케이스를 공부해서 모든 종류의 anti-reversing에 대비하겠어!” 같은 생각은 음….

물론 anti-reversing과 관련된 지식은 많으면 많을 수록 좋지만, 위의 접근 방식은 그렇게 옳은 접근 방식이 아닐 수 있습니다. 이번 강의에서도 일반적인 수준의 지식은 전달하려고 하겠지만, 막막하고 감도 안 오는 상황을 타개하는 역량을 길러 주는 것이 목표입니다.

anti-reversing에는 크게 디버깅을 방해하는 anti-debugging과 난독화(obfuscation)이 있습니다.


Obfuscation

난독화를 크게 2가지 타입으로 분류하면

  1. 코드를 대상으로 한 난독화
  2. 바이너리의 구조를 변형하거나 일반적인 바이너리가 가지는 format과 어긋나도록 하는 난독화

이렇게 분류할 수 있습니다.

1번의 난독화 종류들은 C 같은 컴파일 언어 뿐 아니라, python이나 js같은 스크립트 언어에도 자주 적용되는 난독화입니다. 일반적으로 사람이 프로그래밍하는 포멧을 벗어나게 하여 사람이 이해하고 읽는데 어렵게 합니다. 사람이 보기에는 기괴하게 느껴지지만 실제 프로그래밍 언어의 문법을 전혀 벗어나고 있지는 않습니다.

2번의 경우에는 컴퓨터가 실행 파일을 실행하는 방식에 대한 깊은 이해를 바탕으로 이루어집니다. 당연히 바이너리가 정상적으로 실행은 되어야 하기 때문에 컴퓨터가 바이너리를 읽고 실행하는 방식을 훼손하지는 않습니다. 컴퓨터가 실행 파일을 정상적으로 실행할 수 있는 범위 내에서 다른 형태의 포멧을 철저히 훼손하는 방식이 2번의 경우입니다.

예를 들자면, C언어는 기계어와 1대1 대응이 아니지만 어셈블리어는 기계어와 1대1 대응이 됩니다. 이 말은 C언어 → 어셈블리언어로 변환하는 함수는 정의가 가능하지만, 이에 대한 역함수는 존재하지 않습니다. 물론 우리에게 친숙한 IDA 디버거는 대부분의 상황에서 어셈블리언어 → C언어로 디컴파일을 수행해주지만, 그것은 C언어로 코딩된 바이너리이기 때문에 당연히 기계어(어셈블리언어)를 C언어로 표현하는 것이 가능하기 때문입니다. 그렇다면, C언어로 표현이 안되는 어셈블러 형식도 있다는 말이겠죠?? 이런 방식은 C언어로 디컴파일이 되지 않습니다. 간단한 예시가 레지스터의 용도를 바꿔서 사용하는 경우입니다. rdirsi등을 사용하는 대신에 다른 레지스터를 사용한다면, 일반적인 디컴파일에서 함수의 인자가 전달되는 부분에 대한 디컴파일이 제대로 되지 않을 것입니다. 보통 2번의 경우에는 이처럼 decompiler나 disassembler 등의 리버싱 툴을 무력화시키는 목적으로 사용되는 anti-reversing입니다.

사실 2번 종류를 obfuscation으로 분류하는지는 정확히는 잘 모르겠어요. 보통 anti-decompile, anti-disassemble이나 packing, protecting(packing이나 protecting은 더 넓은 의미) 등으로 표현되는 경우가 많습니다. 저는 그냥 obfuscation에 묶어서 표현하도록 할게요.

Strip (debugging 심볼 지우기)

우리에게 매우 익숙합니다. reversing할 때 마주치는 대부분의 바이너리가 strip 옵션이 적용되어 있습니다. python이나 js에도 난독화를 목적으로 변수명이나 함수명을 아무런 의미가 없는 이름으로 바꾸기도 합니다.

Dummy Code

아무 의미 없는 더미 코드들을 중간 중간 삽입합니다.

nop과 같이 실제 실행 되지만 아무런 동작도 하지 않는 코드들을 삽입할 수도 있지만,

일반적으로 실제로 무언가 동작을 수행하는 코드들로 이루어져 있습니다. 대신에, 절대 실행될 일 없는 control flow에 집어 넣는 경우가 많습니다.

외딴 곳에 이런 코드를 삽입하면 크게 방해가 안되겠죠??

그래서 if 문 등으로 절대 실행 되지 않는 분기문과 함께 삽입합니다.

Control Flow Flattening

실제 프로그램은 ifswitch 문, for문 등으로 입체적인 구조를 가지고 있습니다. 이를 평면화시킨 것이 control flow flattening입니다.

for i in range(10):
	a += 1

위 코드를 평면화하면

a += 1
a += 1
a += 1
a += 1
a += 1
a += 1
a += 1
a += 1
a += 1
a += 1

위의 형태가 되겠네요.

Unfold

Data unfold, Code unfold가 있는데

Data unfold는 100이라는 값을 10 + 51 + 45 + 71 - 77 이런 식으로 펼치는 것을 말합니다.

code unfold도 비슷한 느낌이에요. 연속해서 실행되어야 하는 일련의 코드 조각들을 여러 곳으로 흩뿌려 놓는 거에요

a = 1;
b = 2;
c = 3;

위 형태의 코드가

(생략)

a = 1;
goto LABEL 1;

(여기서는 생략하지만 사이에  많은 코드가 껴있음. 더미 코드 일수도 있고)

LABEL 2;
c = 3;
goto LABEL 3;

(생략)

LABEL 1;
b = 2;
goto LABEL 2;

(생략)

LABEL 3;

(생략)

요로코롬 되는 거죠.

Layout 난독화

주로 어셈블리어(기계어)의 특징을 이용한 난독화 방법입니다.

상당수의 아키텍쳐에서 어셈블리어(기계어)는 코드 하나의 길이가 가변적이에요. 그렇기에 program counter(x86의 경우 rip가 pc에 해당)가 기계어 시작 지점이 아닌 기계어 중간 지점을 가리키는 것을 허용하게 설계될 수 밖에 없습니다. 이를 이용한 난독화 기법입니다.

2학기 CyKor CTF에 출제되었던 No Main의 Entry point에 있는 start 함수가 디컴파일이 수행되지 않았는데 이 첫 번째 이유가 레이아웃 난독화 때문이었습니다. 궁금한 사람은 찾아 보시면 좋을 듯. 어셈블리 중간에 jmp [rip + 1]이 있는데 rip + 1이 기계어 중간을 가리키고 있을 거에요. 하지만 IDArip + 1 지점을 기준으로 디스어셈블 한 것이 아닌 rip 지점을 기준으로 디스어셈블 해서 무언가 디스어셈블 결과가 부자연스러울 겁니다.

(근데 아마 layout 난독화 해제해도 디컴파일 안 될 걸? ㅋㅋㅋㅋㅋ)

(IDA에서 디컴 시도 했을 때 보여주는 에러 메세지를 잘 보고 해결해 보자)

Virtualize

직역하면 가상화인데, 바이너리를 python이나 javascript engine처럼 짜는 겁니다.

interpreter처럼 짜서, 난독화를 수행한 사람 임의로 정한 문법의 코드를 바이너리가 실행하는 형식이죠.

그런 임의의 코드를 바이너리가 cpu architercture의 기계어로 변환하고 그걸 cpu가 실행하는 형태입니다.

이렇게 되면 분석가는 바이너리를 분석하여 가상화 패턴을 분석하여 임의의 코드를 알고 있는 형태의 언어로 변환하고, 변환된 언어를 해석해야 합니다. 분석가 입장에서는 이런 변환의 과정이 불가피하게 추가되게 되는 거죠.

자세한 사항들은 다음 강의에서 다루고자 합니다.

Metamorphic

CRC문제를 기억하시려는지요.

metamorphic이 적용되어 있습니다.

image.png

위에서 39 line의 코드가 상당히 복잡하게 구성되어 있죠??

v10[j] %= 128과 완전히 동일한 연산입니다. 하지만 이해하기 힘들죠?

이런 방식으로 코드의 동작을 다른 방법으로 표현하는 방식을 metamorphic이라고 합니다.

일반적인 난독화 해제 방법이 없으며 가장 창의성을 요구하는 난독화 방식입니다.

Polymorphic

polymorphic은 여러 모습을 갖고 있다는 뜻을 가지고 있는데, polymorphic code 형식의 난독화가 되어 있으면 이 코드가 진짜 여러 모습을 갖고 있답니다.

과거 1학기 때에 assembly handray 과제를 내준 적이 있는데, 그 때 바이너리가 polymorphic code 형식입니다. IDA로 정적으로 봤을 때는 디스어셈블조차 되지 않았지만, gdb로 실행해보면 정작 실행할 때는 정상적으로 작동하는 것을 확인할 수 있습니다(이상하게 실행 시점에서는 디스어셈블이 잘 되죠? 실행하기 전에 코드 영역에 대해서 스스로 패치를 수행합니다).

이런 식으로 실행 중에 코드의 내용이 바뀌는 형식입니다. 디스어셈블러 혹은 디컴파일러를 무력화시키며 정적 분석을 어렵게 하기 위한 목적으로 사용되는 난독화 형식입니다.

Packing

packing은 본래 난독화를 목적으로 한 것은 아니고, 바이너리 사이즈를 줄이기 위한 목적으로 개발되었다. 대표적으로 UTX가 있으며, 이렇게 패킹된 바이너리는 그 구조가 매우 뒤틀려 있습니다.

다만, 난독화를 목적으로 한 것이 아니기 때문에 unpacking 툴이 같이 있는 경우가 대부분.

난독화를 목적으로 하는 packing도 분명 있으며 이런 경우에는 unpacking이 같이 개발되어 있지 않으 은데, 이런 packer를 protector라고 합니다. Themida가 그런 예시입니다.

이런 프로텍터는 단순히 바이너리의 구조만 뒤틀린 것이 아니라 많은 난독화 기법과 안티 디버깅 등이 같이 걸려 있어요. 그 중에서는 바이너리의 퍼포먼스에 큰 영향을 끼치는 옵션도 있으며(가상화 같은 옵션들) 좋은 프로텍터의 경우에는 이러한 옵션들을 임의로 선택하여 프로텍팅할 수 있습니다.

고 수준의 상용 프로텍터의 경우 일반적으로 개인이 완전히 바이너리를 해독하는 것은 매우 어려움.

그 외 잡다한 것들

ROP

우리가 잘 알고 있는 Return Oriented Programming입니다.

난독화를 목적으로 ROP로 프로그래밍하는 경우도 있습니다.

레지스터 용도 바꾸기

일반적으로 레지스터의 용도는 정해져 있지만 사실 rip 같은 것만 안 건드리면 프로그램이 바이너리를 실행하는 것에는 큰 문제가 없습니다. 용도를 바꿔서 코딩하게 되면 당연히 정상적으로 디컴파일이 되지 않겠죠.

Calling Convention 훼손

일반적인 calling convention을 따르지 않는 것으로 디컴파일러를 무력화시킵니다. 이는 위에서 얘기한 rdi, rsi 등의 어셈블리 사용을 다르게 하여 인자 전달 메커니즘을 변형하는 것도 되지만 ret 매커니즘을 변형시키는 경우도 종종 있습니다.

C-unlike code

calling convention을 훼손하는 것도 어떻게 보면 c 같지 않은 코드에 해당됩니다. 함수를 호출하는 포멧은 어셈블리 수준에서 정의되는 것이 아니라 C 수준에서 정의되는 것이기 때문입니다. 이러한 것 외에도 C의 형식을 벗어나게 코딩하는 방법은 꽤 다양합니다.

예를 들어서 범용 레지스터 하나를 전역 변수처럼 사용한다고 해봅시다. C에서 레지스터를 지역 변수처럼 활용하는 경우는 자주 있기에 같은 함수 내에서 해당 변수의 사용은 디컴파일러가 잘 보여줍니다.

하지만, A라는 함수에서 그 레지스터에 값을 넣고 다른 함수에서 그 레지스터의 값을 꺼내 쓴다면??

B의 디컴파일 결과에서는 초기화하지 않은 값이 사용되는 기이한 현상이 발견될 겁니다. 이런 방식은 컴파일 할 때 O3 같이 최적화 방식을 사용하면 자주 발견됩니다.

또 stack을 C의 형식을 벗어나게 사용하는 방법도 자주 사용됩니다.

예를 들어서, rbprsp의 용도를 다르게 사용하는 것도 해당되며

지역 변수를 전역 변수처럼 사용한다면??

당연히 해당 변수에 대한 reference가 디컴 결과에 나타나지 않겠죠.

사실 잡다한 것이 너무 많아요. 다 적기도 그래.. 내가 모르는 것도 많고. C를 벗어나게 코딩하는 방법도 너무 많고…

바이너리 구조를 변경하는 기법도 너무 다양하고 아키텍쳐별로 실행 파일의 형식별로 다 다릅니다.

그러니 당연히 난독화에 대한 지식 그 자체보다는… 난독화의 원리가 되는 실행 파일의 구조나, 아키텍쳐, C언어 등에 대한 깊은 지식이 더 중요하다고 생각합니다.