Ghidra 로 Dynamic API Resolution 을 사용하는 멀웨어 분석 방법

Ghidra 로 Dynamic API Resolution 을 사용하는 멀웨어 분석 방법

동적 API 해석(Dynamic API Resolution)은 멀웨어에서 사용되는 난독화 기법 중 하나로, 멀웨어의 동작에 사용되는 API 주소를 동적으로 찾아내 저장, 사용하는 기법이다. 일반적인 실행 파일은 런타임에 사용되는 API 에 대한 정보를 IAT 에 저장된다. 하지만 이러한 파일은 분석가가 정적 분석만으로도 쉽게 해당 파일에서 어떤 API 를 호출하는지 알아낼 수 있고, 이에 따라 어떤 동작을 수행할지도 대략적으로 유추할 수 있다.

공격자는 이러한 정적 분석을 회피하고자 악성 행위와 관련된 핵심 API 를 IAT에 저장하지 않고, 런타임 중 LoadLibrary(),GetProcAddress() 함수를 호출해 임의의 DLL 모듈의 임의의 API의 주소를 알아내고, IAT가 아닌 다른 메모리 공간에 이를 저장, 참조해서 해당 API를 호출한다.

이에 따라 필연적으로 Ghidra 와 같이 정적인 분석 프로그램은 동적으로 해석된 주소가 호출될 때 어떤 API를 호출할지 알 수 없어 분석에 어려움을 겪을 수 있다.

다음 이미지는 Ghidra 로 Dynamic API Resolution 을 사용하는 멀웨어의 코드를 정적 분석한 결과다. CALL 명령어로 EAX 레지스터 값으로부터 0x114만큼 떨어진 위치의 4바이트 주소를 호출하려 하지만, 정적 분석으로는 해당 주소에 어떤 API 의 시작 주소가 담겨 있는지 알 수 없다.

EAX+0x114 주소의 함수를 호출하는 코드

이 부근의 코드를 Decompile하면 필연적으로 아래 이미지처럼 된다. EAX(param_1)을 기준으로 일정 offset만큼 떨어진 위치의 주소들을 CALL 명령어로 호출하려고 하지만, 정적인 분석으로는 어떤 함수인지 알 수 없어 분석이 어렵다.

동적 API 호출 코드를 Decompile 한 모습

x64dbg 로 해당 코드를 동적으로 분석하면 해당 주소에 저장된 API가 lstrcpy인 것을 손쉽게 확인할 수 있다. 하지만 Ghidra로 정적 분석할 때 매번 API 호출할 때마다 comment를 다는건 몹시 비효율적이니 다른 방법이 필요하다.

lstrcpy 를 호출하는 동적 API 호출 코드

이 글에서는 Ghidra 의 커스텀 구조체 생성, 데이터 타입 설정 기능을 이용해 Dynamic API Resolution 기법을 사용하는 멀웨어를 분석하는 방법에 대해 설명하겠다.

1. 예제 파일 정보

  • MD5 : 03A285B026519DDC0D469DBCD543BF65
  • SHA256 : B4C37E3995D5FF94754CEDD49F8FC6765448A16027A5951E37BD0DA06661CD88
  • FileType : DLL
  • 악성 행위 : USB Worm

MalwareBazaar 에서 예제 파일을 다운로드할 수 있다. 이 파일은 악성 파일이므로 반드시 격리된 환경에서만 실행할 것을 권고한다. 이 멀웨어는 정상 파일로 위장한 채 DLL Side Loading 기법을 이용해 로딩되어 실행된다. 핵심 악성코드는 Odrinal_0x00000027 함수에 구현되어 있으므로 동적 분석 시 참고하길 바란다.

DLL 파일 분석과 관련해서는 이전에 작성한 DLL 파일 분석에 유용한 DllLoader를 참고하자.

2. API 테이블 생성

예제 파일은 현재 아래와 같은 방식으로 Dynamic API Resolution을 진행하고 있다.

  • Heap 에 공간 할당 후, 동적으로 resolution 한 API 주소를 차례로 저장하여 API 테이블 생성.
  • API 호출 시 Base 주소를 기반으로 일정 offset 만큼 떨어진 주소의 API 호출.

따라서 예제에서 사용하는 API 테이블과 동일한 구조체를 Ghidra에 생성, CALL 명령어로 호출하는 Base 주소 변수의 타입을 해당 구조체로 설정한다면 Ghidra에서 적절하게 인식할 수 있을 것이다.

우선 Ghidra 상단에서 Window - Data Type Manager 를 선택한다.

Data Type Manager

팝업된 Data Type Manager 창에는 미리 준비된 데이터 타입들이 보인다. 우리가 작성할 구조체는 해당 예제 파일에서만 사용할 예정이므로 예제 파일명을 우클릭 - New - Structure 를 클릭해 새로운 구조체를 생성한다.

Data Type Manager 새 구조체 생성

이제 팝업된 Structure Editor 에서 동적으로 해석된 API 들을 저장할 테이블 구조체를 작성해야 한다. 우선 예제 파일에서 얼마나 많은 API 를 로딩하는지 확인할 필요가 있다. x64dbg 로 Dynamic API Resolution 이 종료될 때까지 분석한 결과 총 0x12C 바이트 공간을 사용하고 있었다. 하나의 API 당 4바이트를 차지하므로 총 75개의 API가 연속적으로 저장된 것을 유추할 수 있다.

상단의 + 버튼을 클릭해 임시로 컴포넌트 하나를 생성하자. offset 0x 에 length 가 0x1인 컴포넌트가 하나 생성된다. 우리가 필요한 건 4바이트 크기의 컴포넌트 75개다. 우선 임시로 생성한 컴포넌트의 DataType 을 void* 로 변경하면 length 가 자동으로 0x4로 변경되며, 구조체 전체 크기도 4바이트가 되는 것을 확인할 수 있다.

구조체에 새 컴포넌트 추가

이제 해당 컴포넌트를 복제하자. 컴포넌트를 우클릭 - Duplicate Multiple of Component 를 클릭한다. 복제할 컴포넌트 갯수를 묻는 팝업창이 출력되면 적절한 수를 입력한다.

컴포넌트 복사

앞서 분석한 내용에 따르면, API 테이블로부터 0x114 바이트 떨어진 위치에는 lstrcpy() 함수의 주소가 저장되니 이에 맞게 컴포넌트 필드 값을 수정해보자. Offset 값이 0x114인 컴포넌트를 선택, DataType을 lstrcpyA* 로 수정한다. API 이름을 검색하면 Ghidra에서 미리 정의된 API의 프로토콜을 불러올 수 있으니 적극 활용하자. DataType을 변경하면 불러온 프로토콜에 맞게 Ghidra에서 인자, 리턴 값의 타입을 적절하게 인식한다.

DataType을 변경했다면 바로 옆의 Name 필드 값도 lstrcpyA로 변경한다. Name 필드는 Decompiler 에서 해당 필드를 어떤 이름으로 출력할지에 대한 데이터를 가진다. 컴포넌트를 적절하게 변경했다면 하단의 Name 폼에 적절한 구조체 이름을 지어주고, 상단의 디스크 아이콘을 클릭해 구조체 변경 사항을 저장한다.

컴포넌트 정보 수정

3. 변수 데이터 타입 설정

이제 CALL 명령어가 호출하는 base 주소를 저장하는 변수를 앞서 생성한 구조체의 타입으로 변경해야 한다. 앞서 예시로 보였던 Decompile 코드를 다시 보자. param_1 변수의 값을 기준으로 0x114 바이트 떨어진 위치의 주소를 CALL 로 호출하고 있다. 따라서 param_1 변수의 데이터 타입을 앞서 생성한 API 테이블의 포인터로 변경해야 한다.

param_1 타입 변경 전 디컴파일된 코드

해당 변수를 우클릭 - Retype Variable 을 선택하면 변수의 데이터 타입을 선택하는 다이얼로그가 팝업된다. 앞서 작성한 구조체 이름*로 선택한다.

데이터 타입 변경 다이얼로그

변경된 데이터 타입을 적용하면 lstrcpyA() 함수를 호출하도록 디컴파일된 것을 확인할 수 있다. 마찬가지 방식으로 구조체에 다른 컴포넌트에 DataType, Name 필드 값을 적절하게 설정하면 API 테이블을 참조하는 함수 호출을 편하게 분석할 수 있다.

데이터 타입 변경 후 디컴파일된 코드