발자취

악성코드 04. Anti-역공학 1 본문

3-2/악성코드

악성코드 04. Anti-역공학 1

해린 2023. 9. 18. 19:06

- 역공학: 방어자가 분석 등의 좋은 목적으로 하는 행위

- Anti-역공학: 공격자가 역공학을 방어함

 

 

1. Anti-Debugging

공격자의 관점

1. Windows Debugger 탐지

*디버거: 어떤 프로그램이 동작할 때 에러없이 잘 수행되는지, 악성행위를 하는지 확인 및 탐지

   → 디버거가 동작한다? 공격자에게 좋지 않음. 자신의 악성행위가 탐지될 수 있기 때문

       → 따라서 디버거가 도는지 확인하고 안도는 경우에면 악성코드 동작시킴

 

- Windows API 사용

- 매뉴얼하게 구조 체크

 

 

2. Windows Debugger 행위 식별

*디버거 행위 및 특징

- INT 스캐닝

- 코드 체크섬 수행

- 타이밍 체크

 

 

 

2. Windows Debugger 탐지 (Windows API 사용)

1. Windows debugger 탐지의 가장 확실한 방법

- IsDebuggerPresent, CheckRemoteDebuggerPresent, NtQueryInformationProcess, OutputDebugString

 

 

1. IsDebuggerPresent

디버거가 있는지 없는지 확인 가능. (True/False)

- Windows debugger 탐지의 가장 단순한 API 함수

- Process Environment Block (PEB) 구조에서 IsDebugger 필드 탐색

 

 

2. CheckRemoteDebuggerPresent

- IsDebuggerPresent 함수와 거의 동일한 기능 수행

- Local 기기 상의 프로세스들을 위한 PEB 구조에서 IsDebugger 필드 탐색

 

 

3. NtQueryInformationProcess

- Ntdll.dll에 있는 API 함수: 주어진 프로세스에 대한 정보 획득

- 첫번째 인자: 프로세스 handle

- 두번째 인자: 획득하고자 하는 프로세스 정보의 타입을 함수에 알려줌

 

 

4. OutputDebugString

- 기본 기능: display를 위해서 debugger에 문자열을 보내는 함수

- Debugger의 존재를 탐지하는데 사용됨

 

 

방어자의 관점

5. Windows API 사용에 기반을 둔 Anti-Debugging 기법에 대한 방어책

- Malware 실행 중에 앞에서 언급된 API 함수들이 호출되지 못하도록 malware를 매뉴얼하게 수정함

- 앞에서 언급된 API 함수를 rootkit에 hook 시킴

    - rootkit: 공격자가 비밀리에 수행하는 것. 여기서는 좋은 목적으로 사용함.

   → 방어자가 자신만 알고 있는 방식으로 루트킷을 만들어서 앞에 언급된 API 함수가 사용된다면 방어자가 만든 루트킷에 연결되도록 하는 방식임

 

   ▶ 보안 관점에서는 두번째 방식이 더 좋음

 

 

 

2. Windows Debugger 탐지 (매뉴얼 구조 체크)

1. Malware 저자들에 의해 사용되는 가장 흔한 방법

- PEB 구조의 여러 flag 정보를 이용함

 

 

2. BeingDebugged 플래그 사용 및 그에 대한 방어책

- 특정 프로세스가 디버그되고 있으면 BeingDebugged 플래그 값이 1로 설정됨

- 방어책: 매뉴얼하게 DeingDebugged 플래그를 0으로 설정함 (항상 0으로 설정하는 거! 시스템 상으로는 문제가 없고, 방어자는 디버거가 되고 있는지 아는지 알 필요 없으니까 바꾸는 것!)

 

 

3-1. ProcessHeap flag 검사

- ProcessHeap 플래그는 PEB 구조에서 오프셋 0x18에 위치함

- ProcessHeap 플래그는 Heap이 debugger 내에서 생성되었으면 1로 설정됨

 

 

3-2. ProcessHeap 플래그 사용에 대한 방어책

- 매뉴얼하게 ProcessHeap 플래그를 0으로 설정함

- 디버거를 위한 hide-debug 플러그인을 사용함

 

 

4-1. NTGlobalFlag 검사

- NTGlobalFlag는 PEB 구조에서 오프셋 0x68에 위치함

- Heap 구조를 생성하는 방법에 대한 결정을 할 때 NTGlobalFlag 사용함

- NTGlobalFlag 값이 0x70이라면, debugger가 동작하는 것으로 파악함

 

 

4-2. NTGlobalFlag 사용에 대한 방어책

- 매뉴얼하게 NTGlobalFlag를 0으로 설정함

- 디버거를 위한 hide-debug 플러그인을 사용함

 

 

5. 시스템 잔여물 검사

- malware는 디버거에 대한 참조용 레지스트리 키 탐색과 같은 잔여물 검사를 통해서 디버거의 존재를 파악함

- 애플리케이션 동작하다가 잘못된 코딩때문에 에러가 발생하면 동작하는 디버거를 적음. 에러가 발생해서 디버거가 돌았구나~를 파악해서 디버거 존재 여부를 아는 방식

 

 

 

3. Windows Debugger 행위 식별

1-1. INT Scamming

*INT: 인터럽트*INT 3: 디버거에 의해 사용되는 소프트웨어 인터럽트- 수행 중인 프로그램에서 명령어를 임시로 대체하고, 디버거 예외 처리기를 호출함- Opcode(기계어 코드): 0xCC   - 디버거에 의해 사용되는 소프트웨어 인터럽트가 사용하는 오피코드가 0xCC임.   - 0xCC 사용 O → INT 3 사용 O  → 디버거 존재 O

 

 

1-2. INT Scanning 사용 및 그에 대한 방어책

- 0xCC 바이트에 대한 스캔을 수행해서 0xCC 바이트가 발견되면, 디버거가 존재한다고 판단- 방어책: 소프트웨어 브레이크포인트 대신 하드웨어 브레이크포인트를 사용함   - 0xCC는 소프트웨어 상에서 사용하는 바이트니까

 

 

2. 코드 체크섬 수행

*체크섬: 어떤 프로그램을 다운받은 뒤, 그 프로그램이 변경 및 손상됐는지 확인하는 과정에서 계산, 해당 프로그램의 요약- 디버거가 Malware 상에서 opcode들에 대한 CRC 또는 MD5 체크섬 수행

- 체크섬이 돌아갔다 → 디버거가 존재하는구나!

- 방어책: 하드웨어 브레이크포인트 사용 또는 매뉴얼하게 runtime에 디버거와 연관된 실행 경로 수정

 

 

3-1. Timing 검사

디버거가 동작하면 실행 속도가 느려질 것이기 때문에 실행 속도를 보고 공격자가 디버거 탐지를 할 수 있음- Malware가 디버거를 탐지하는데 사용하는 가장 인기 있는 방법들 중 하나임- 디버깅 될 때 프로세스가 느리게 동작한다는 사실을 디버거 탐지에 사용함- 프로그램에 대한 single-stepping은 상당한 실행 속도 저하를 야기함

 

 

3-2. Timing 검사 사용법

- 우선 timestamp 기록하고, 몇 가지 기능 수행 후에 다시 한번 timestamp를 기록함.   두 timestamp 간의 차이가 상당하면 디버거가 존재하는 것으로 가정할 수 있음.- 예외 처리 전후로 timestamp를 기록함. 프로세스가 디버그 되지 않는다면, 예외처리가 빠르게 수행됨

 

 

3-3. Timing 검사 방법

- 가장 흔한 방법은 rdtsc 명령어 (opcode: 0x0F31) 를 사용하는 것임- QueryPerfomanceCounter를 사용할 수 있음- GetTickCount 함수를 사용할 수 있음.

a = GetTickCount();
MaliciousActivityFunction();  // 악성행위
b = GetTickCount();  // 공격자가 악성행위를 한 전과 후에 타임스탬프를 찍었음

delta = b-a;

if ((delta) > 0x1A) {  // 두 시간의 차이를 비교
  // Debugger Detected
}
else {
  // Debugger Not Found
}

 

 

 

3-4. Timing 검사 사용법의 한계점

- 시간 파이를 구하기 위해서 사용된 두 개의 함수 호출 사이에 single stepping이나 브레이크포인트 설정이 되어 있어야만 디버거의 존재를 파악할 수 있음.

  (타임스탬프를 찍는 사이에 디버거가 실제로 동작해야 시간 차이가 확실히 나서 디버거의 존재를 파악할 수 있는데, 한 타임스탬프 찍을 때만 돌아가면 정확히 알 수 없다!)

 

 

3-5. Timing 검사에 의한 디버거 탐지 회피책

- Timing 검사를 수행한 직후에 브레이크포인트를 설정하고, 그 다음에 single stepping을 다시 시작함

  (타이밍 검사를 하고 나서 디버거를 동작시키는 방법!)

 

 

 

4-1. Packers & Unpacking

1-1. Packers

- Malware를 antivirus 소프트웨어로부터 숨기는 역할 수행

- Malware 분석을 복잡하게 하고 악성 실행파일의 크기를 줄임

 

 

1-2. Packers 특징

- 실행 파일을 입력으로 받아서 출력으로 실행 파일을 생성함

- Pack된 실행 파일: 압축되고, 암호화되고, 또는 변형된 실행파일

- 원본 실행 프로그램의 기능을 유지하기 위해서 packing 프로그램은 원본 실행 프로그램의 import 정보를 저장해야 함.

 

*포인트!

1) 형태는 비록 바뀌지만 악성코드의 기능 자체는 유지된다

2) 해당 실행 파일에 외부에서 가져온 라이브러리들이 있음. 이런 import된 라이브러리는 노출되면 패킹한 효과가 적어지기 때문에 숨겨줘야함

 

 

4-2. Packing 프로세스

1. Unpacking Stub

- 원본 실행파일을 메모리로 unpack함

- 원본 실행파일의 모든 imports를 해결함- 실행의 흐름을 original entry point (OEP)로 이동시킴  * original entry point (OEP): 맨처음에 실행 파일이 실행되는 포인트

 

 

2. 실행파일 로드

- Pack 되지 않은 실행파일: OS에 의해 로드됨- Pack 된 프로그램: OS가 unpacking stub을 로드하고, unpacking stub이 원본 프로그램을 로드함  * Pack 된 프로그램은 운영체제가 이해하지 못하기 때문에 바로 실행하지 못함.

 

 

3. Imports 해결

공격자 입장) import 라이브러리를 어느정도 노출하는게 효율적인가?

 

1. 가장 전형적인 접근법: unpacking stub이 단지 LoadLibrary와 GetProcAddress 함수만을 import 함   - 노출되는 라이브러리 함수를 최소화하는 방식 (LoadLibrary와 GetProcAddress 함수는 악성 프로그램이 아닌 프로그램들도 웬만하면 쓰는 함수임. 이 두 함수를 가지고 다른 외부 라이브러리 함수를 불러오는 방식!)

 

2. 원본 import table을 packing 하지 않고 그대로 두고, Windows loader가 DLL과 import된 함수들을 로드하는 방법   - 문제점: import table에 있는 모든 정보들이 노출됨

 

3. 원본 import table에 포함된 각각의 DLL마다 한 개의 import 함수를 packing 하지 않고 그대로 두는 방법   - 2번보다 패킹을 하지 않고 그대로 주는 함수 개수를 줄임. 좀 더 안전하나 여전히 노출됨

 

4. 모든 imports (LoadLibrary와 GetProcAddress 포함)를 삭제하는 방법   - 제일 안전한 방법이나 설계가 복잡해짐

 

 

→ 공격자 입장에서 제일 좋은 건 4번이고, 제일 좋지 않은 건 2번임. 가장 적절한 것은 1번 (어느정도 안전+복잡X)

 

 

 

패킹 전 원본 실행 파일, 팩된 실행파일의 모습

 

unpack된 후 메모리로 로드된 프로그램과 완전하게 unpack된 프로그램의 모습

 

- unpack된 후 메모리로 로드된 프로그램: 패킹이 된 순간 엔트리 포인트가 언패킹 스텁으로 감. 얘가 처음에 실행되어야 코드를 실행할 수 있기 때문

  *특징: 임포트가 없음

 

- 완전하게 unpack된 프로그램: 완전히 임포트까지 해결하고 나서는 엔트리포인트가 텍스트 영역을 가리키게 됨

  (LoadLibrary, GetProcAddress와 같은 함수들이 임포트를 가져온 것임!)

 

 

 

4-3. Pack된 프로그램 식별

1. Pack된 프로그램을 식별하는데 사용하는 지표

- 적은 수의 imports를 갖는 프로그램 (특히 갖고 있는 유일한 imports가 LoadLibrary와 GetProcAddress인 경우)

- 프로그램이 IDA Pro에 의해 오픈될 때, 단지 작은 양의 코드만이 자동화된 분석에 의해 해석되는 경우

- 프로그램이 OllyDbg에 의해 오픈될 때, 프로그램이 pack 되었을지 모른다는 경고가 나오는 경우

- 프로그램이 특별한 packer를 나타내는 영역 이름 (e.g. UPX0)을 보이는 경우

- 프로그램이 비정상적인 영역 크기를 갖는 경우 (e.g. Raw Data의 크기가 0인 .text 영역)