2016. 10. 16. 17:46 :: 문제풀이/Pracical Malware

안녕하세요. Message입니다.

<실전 악성코드와 멀웨어 분석> 책의 실습 문제 7-3을 분석합니다.

이번 실습은 동적분석과 정적분석 이후에 교재의 문제를 풀이합니다.

 

분석환경은 WindowXP / Vmwre 12.1.0 build 입니다.

  # Windows7 에서 분석 수행 시 오류가 발생하여 멈추는 현상이 발생할 수 있습니다.

  # 기본적으로 실습교재의 샘플들은 XP에 최적화 되어 있습니다.

 

------------------------------------------------------------------------------------------------------------------------------------------

1. 기초 동적 분석

------------------------------------------------------------------------------------------------------------------------------------------

문제풀이에 앞서 간단한 동적 분석을 진행합니다.

 

1. PE구조 & 패킹

exe 파일과 dll 파일 모두 별도의 패킹이 되어 있지 않습니다.

 

 

2. DLL

1) Lab07-03.EXE

기존에 실습했던 악성코드가 임포트하는 DLL과 다를바가 없습니다.

 

하지만 임포트 함수 중 CreateFileA, CreateFileMappingA, MapViewOfFile은 프로그램이

파일을 오픈해 메모리로 매핑함을 알려줍니다. 사실 해당 함수는 지난 실습에서 꾸준히 등장했지만

분석 포커스에 빗나가 있었기 때문에 주목(?) 받지 못했습니다.

또한 FindFirstFileA, FindNextFileA, CopyFileA 함수를 이용하여 디렉토리를 탐색하고

CopyFileA를 통해 악성코드 본인 또는 특정 파일을 복사할 것으로 짐작됩니다.

주목할만한 점은, Lab07-03.dll을 어떤 방식으로든 사용할 것이 분명하지만, 

지금까지 많이 보아 왔던 LoadLibrary, GetProcAddress 함수가 임포트 목록에 없는 사실은

런타임에 해당 DLL을 로드하지 않고 다른 방법을 사용하고 있음을 짐작케 합니다.

 

2) Lab07-03.DLL

Lab07-03.dll이 임포트하는 dll 파일은 WS2_32.dll 입니다.

교재의 첫 실습[1-1]을 통하여 공부했던 네트워크 관련 API들이 얼핏 기억납니다.

검색해보니 2014년도에 포스팅이네요.

 

특이한점은 dll의 주요 역할인 Export 목록이 비어있다는 점입니다.

익스포트가 없으면 다른 프로그램이 임포트할 수 없습니다.

이를 염두해두고 분석을 진행합니다.

 

3. String

1) Lab07-03.EXE

지난번 "kernel32.dll" 파일명을 "kerne132.dll" 으로 위장한 악성코드를 실습한 이후부터는

String 윈도우에서 관련 문자열을 발견하면 숫자가 섞이지 않았나 체크하게됩니다.

역시 영문자 L 을 숫자 1로 변경한 kerne132.dll 문자열이 보입니다.

 

2) Lab07-03.DLL

악성코드가 접속을 시도할것으로 추정되는 IP(127.26.152.13)가 눈에 띕니다.

.data 영역의 "exec, sleep, hello" 문자열을 통하여 네트워크 통신 관련된 기능을 염두해 둡니다.

 

4. Function Calls

이번 실습에서는 기존에 사용해본적 없는 [ Function Calls ] 윈도우를 사용해봅니다.

특히 현재 분석중인 DLL은 DllMain 중심으로 대부분의 기능을 구현하고 있기 때문에

호출하는 함수만 요약하여 볼 수 있는 기능이 악성코드 동적/정적 분석에 속도를 더해줍니다.

 

1) Lab07-03.EXE

CreateFileA, CreateFileMappingA, MapViewOfFile 등의 메모리맵 파일에 관련된 API들이 보입니다.

메모리맵 파일의 기능을 이용하면 EXE/DLL 파일의 읽기/쓰기를 할 수 있기 때문에

LoadLibrary, GetProcAddress 함수를 사용하지 않고도 DLL 파일을 사용할 수 있습니다.

메모리맵 파일을 사용하기 위해서는 3단계 실행흐름(CreateFIle > CreateFileMapping > MapViewOfFile)을 가지며,

아래의 Function Calls 에 나오는 함수명과 호출 순서가 일치합니다.

또한 중간에 서브루틴 sub_401040이 여러번 등장합니다. 아직 어떤 기능인지 알 수 없지만

빈번하게 호출되는 서브루틴임을 참고하고 넘어갑니다.

 

2) Lab07-03.DLL

지난 실습에서 마주쳤던 Mutex 관련 함수들(OpenMutex, CreateMutexA)이 보입니다.

해당 함수들을 이용하여 악성코드의 중복 실행을 방지하는것으로 추측됩니다.

또한 네트워크 연결을 위한 소켓 관련 함수들(socket, inet_addr, htons, connect, send, recv, closesocket)도 보입니다.

어떤 내용을 주고받는지는 모르겠지만, 대략적인 흐름이 보입니다.

 

 

------------------------------------------------------------------------------------------------------------------------------------------

2. 정적분석

------------------------------------------------------------------------------------------------------------------------------------------

문제를 풀이를 위한 정적분석을 진행합니다.

 

1. Lab07-03.DLL

1) 전체적인 코드구조 파악

DLL 파일의 DllMain을 Graph overview 윈도우로 살펴보면 매우 복잡해 보입니다.

똑같겠지 생각하고선 [Flow Chart/F12] 를 실행해보니 분기점이 정리된 그래프가 나오더군요.

그래프 오버뷰에서는 파악하지 힘들었지만, 조건문이 많다는걸 한눈에 알 수 있었습니다.

 

실제로 Hex-ray 기능을 이용해보면 다수의 If 문이 존재함을 알 수 있습니다.

아마 코드를 먼저 보았다면 머리속이 복잡했을테지만

함수목록으로 함수 목록을 체크한 것이 실행 흐름을 파악하는데 많은 도움이 되었습니다.

 

 

2) 스택할당

DllMain 초반 도입부에서 __alloca_probe 함수가 보입니다.

사실 이 함수는 책에서도 가볍게 언급하고 있을 뿐이며, 악성코드 분석에 있어서 중요한 부분은 아닙니다.

하지만 매번 보던 스택프레임이 아니어서 약간 찜찜하기도 했고

삽질 하다보니 배우는게 여럿 있어 살펴보았습니다. 악성코드의 핵심 내용만 보시려면 3)번 항목으로 직행하세요~

 

책에서는 "스택에 공간을 할당하는 함수이며, 프로그램이 큰 스택을 사용함을 알 수 있음" 이라고 설명합니다.

정확한 명세를 알아보고자 구글링을 했더니, 해당 함수에 대해 명확한 설명을 찾지 못했습니다.

 

이것저것 삽질을 하다가 Ollydbg를 통해 살펴보니

__alloca_probe 함수를 호출하는 명령 라인에 "Allocates 4600, bytes on stack" 코멘트가 달려 있었습니다.

코멘트의 4,600이라는 숫자는 __alloca_probe 함수 호출 바로 윗라인에서 EAX 레지스터에 넣어준 11F8 Hex값 입니다.

 

EAX 레지스터에 넣어준 값이 스택 할당에 영향을 주었다는 연관성은 알겠지만,

push를 통해서 인자를 전달한게 아니기 때문에 약간 의아했기에 함수 내부를 살펴보았습니다.

지금까지 함수 호출시에 넣어주는 인자들은 "push" 명령어를 사용한다는 고정관념을 깨고

함수의 정의 부분에서 EAX레지스터가 매개변수로 되어 있음을 발견하였습니다.

 Tip. __usercall vs __userpurge

 Hex-ray 기능을 사용하면 두가지 Calling Convention을 만날 수 있습니다.

 __usercall : 스택정리를 caller가 수행

 __userpurge : 스택정리를 callee가 수행

 

함수를 실행전과 후의 스택 주소값을 비교해보면, 

0018FC34 였던 스택 주소가 0x11F8(4,600) 만큼 할당되어

[ 18FC34 - 11F8 = 18EA3C ] 으로 변경되었음을 알 수 있었습니다.

 

어셈블리어로 좀더 자세히 __alloca_probe 함수를 살펴보겠습니다.

지난 실습에서 배운 반복문 for문의 구조가 보입니다.

For문의 조건식은 a1 >= 0x1000 이므로, 두번째줄의 cmp eax, 1000h 명령어로 최초 비교를 수행하고 난 후

JB(Jump if Below) 명령어로 반복문 내부로 진입할지 탈출할지를 결정합니다.

 

아래 보이는 loc_1000122C 은 반복문 내부입니다.

ECX 레지스터와 EAX 레지스터 모두 SUB 명령어를 이용하여 1000h 만큼 뺄셈을 수행합니다.

ECX에서 1000h를 빼는 이유는 For문 내부의 i -= 1000h 코드를 수행하기 위해서이며,

EAX에서 1000h를 빼는 이유는 For문을 한번 실행할때마다 수행되는 증감식 때문입니다.

증감식이 수행되면, 조건식에 의하여 반복문을 빠져나갈지 여부를 결정합니다.

 Tip. MOV vs LEA

 MOV와 LEA의 기능은 비슷합니다. 하지만 LEA에는 장점과 제약이 존재합니다.

 ① 장점 : LEA 명령어는 우변(SRC)의 값을 연산한뒤 좌변(DST)에 옮길 수 있다. ex) LEA EAX, [ESP+1000]

 ② 제약 : LEA의 좌변에는 레지스터만 올 수 있다.

 

위에서 분석한 반복문의 의미는 스택을 4096(1000h) 단위로 연산하기 위함입니다.

따라서 함수에 인자로 전달된 11F8 이라는 값은 4600이므로 반복문을 1회 수행합니다.

반복문을 탈출하고 4600 단위 보다 낮아서 할당되지 않은 잔여 값(504)은 SUB ECX, EAX 명령어로 조정됩니다.

이후 MOV ESP, ECX 명령어로 계산한 스택값을 ESP에 넣어줍니다.

결론 : 이 프로그램은 큰 스택을 사용합니다. (...)

 

__alloca_probe 함수가 끝나고 나면, ebx/ebp/esi/edi 레지스터가 순서대로 push됩니다.

특정함수가 호출되는것도 아니고, 그냥 백업용인가? 생각하여 함수가 리턴되는 끝부분으로 가보니

push한 역순으로 각 레지스터값을 pop 하며 복구하는 부분이 있었습니다. (백업이 맞나봅니다)

 

3) 중복 실행 방지

fdwReason 값이 1인지 CMP 명령어를 이용하여 비교하며, 1이 아닐 경우 loc_100011E8 으로 분기합니다.

이후 이전 문제에서 실습한 Mutex 함수를 이용하여 중복 실행을 방지합니다.

OpenMutex 함수 이전에 실행되는 memset 함수는 4)번 항목에서 분석합니다.

 Tip. DllMain에서 fdwReason의 의미

 ① DLL_PROCESS_ATTACH : 1

 - DLL이 프로세스(클라이언트 프로그램)의 주소 공간에 맵핑될 때 호출

 - 암시적호출 > 프로세스가 시작될 때

 - 명시적호출 > LoadLibrary가 리턴되기 이전에 이 값과 함께 DllMain 호출

 ② DLL_PROCESS_DETACH : 0

 - DLL이 프로세스 주소 공간에서 분리될 때 호출

 - 암시적호출 > 프로세스가 종료될 때

 - 명시적호출 > FreeLibrary 함수에 의해 이 값과 함께 DllMain 호출

 ③ DLL_THREAD_ATTACH : 2

 - DLL을 사용하는 클라이언트 프로세스에서 스레드를 생성할 때마다 이 값과 함께 DllMain 호출

 - DLL에서는 이 값을 받았을 때 스레드별 초기화를 수행해야 한다.

 ④ DLL_THREAD_DETACH : 3

 - DLL을 사용하는 클라이언트 프로세스에서 스레드가 종료될 때마다 이 값과 함께 DllMain 호출

 - DLL에서는 이 값을 받았을 때 스레드별 종료 처리를 한다.

 

 

4) 네트워크, 데이터 송수신

현재 분석중인 악성코드는 데이터 송/수신을 위해 윈속(Winsock) 을 사용합니다.

악성코드는 Server/Client 중에 Client에 해당하므로, bind, listen 등의 함수는 생략되며

WSAStartup > socket > connect > send/recv > WSACleanup 순서로 호출됩니다.

WSAStartup/WSACleanup은 윈속을 사용하기 위해 반드시 호출해야하는 초기화/해제 함수입니다.

 

Hex-ray로 변환된 코드를 보면 아래 스샷의 첫줄에 if( !WSAStartup() ) 코드가 보입니다.

어셈블리어로 살펴보면, 제법 자주나와 눈에 익은 TEST EAX, EAX -> JNZ 명령어 콤보입니다.

이후 socket 함수의 결과값은 CMP ESI, 0FFFFFFFFh 명령어로 -1이 아닌지 비교합니다.

데이터를 송수신하는 과정에서 주목해야할 부분은 inet_addr 함수에 들어가는 고정IP(127.26.152.13)

htons 함수로 변환되어 들어가는 0x50(80) 포트번호입니다. 웹트래픽이 발생하겠군요.

이후 connect 함수의 결과값이 -1인지 확인하기 위해

CMP EAX, 0FFFFFFFFh > JZ 명령어 콤보를 사용합니다. 이번기회에 아예 외워버리겠네요

 Tip. 2의보수

 1의 보수는 단순히 0과 1을 변경하여 음수를 표현합니다.

 하지만 0이 2개가 생기는 문제가 발생합니다. ex) 0000 0000 = 1111 1111 = 모두 0

 이러한 문제의 해결책이 2의 보수 입니다. 1의 보수의 결과값에 1을 더합니다.

 2의 보수를 이용하여 -1을 표현하려면 0000 00001 -> (1의보수) -> 1111 1110 -> (+1) -> 1111 1111

 따라서 위의 코드에서 if(v3 != -1) 을 어셈블리어로 표현할때 CMP ESI, 0FFFFFFFFh 명령어가 나왔습니다.

 

 

본격적으로 while문 내부에서 어떤 통신이 이루어지는지 살펴봅니다.

while문 내부의 조건에 2가지 함수가 위치하고 있습니다.

첫번째는 send 함수로서, 서버측으로 "hello" 문자열을 송신합니다.

두번째는 shutdown 함수로서 네트워크를 종료하는데 사용하는 함수입니다.

shutdown 함수는 close 함수와는 다르게, 옵션을 통해서 연결의 한쪽만 닫을 수 있습니다.

뒤쪽에 아직 recv, closesocket 함수 등이 남아있기 때문에 1(SD_SEND) 옵션을 통해 send 쪽만 닫습니다.

 

서버에 "Hello" 문자열을 보내고 sned 소켓을 닫아버렸습니다. 이후에는 서버의 응답을 기다립니다.

recv 함수의 리턴값은 수신한 바이트 크기이므로 [ 결과값 > 0 ]이면 다음으로 넘어가 문자열 비교를 수행합니다.

TEST EAX, EAX (플래그설정) + JLE(Jump If Less or Equal) (<=) 명령어 콤보로 [ 결과값 <= 0 ]인 경우 분기시킵니다.

이후 서버의 응답을 구분하기 위해 strncmp 함수를 통하여 5(unsigned int) 만큼 문자열 길이를 비교하여 "sleep" 인지 체크합니다.

strncmp 함수는 일치할 경우 0을 리턴하므로, TEST EAX, EAX (플래그설정) + JNZ(Jump If Not Zero) (!=0) 명령어 콤보로

[ 결과값 != 0 ]인 경우, 중략된 코드를 실행하기 위해 분기하고, [ 결과값 == 0 ]인 경우에는 1분간 Sleep 하게됩니다.

 

명령어가 "sleep"이 아닌경우, 분기하여 "exec" 문자열과 비교합니다.

일치하지 않을 경우에는 [ Str==113 ]인지 확인한 후 핸들을 닫지만,

일치한 경우에는 hex-ray 상으로 memset, CreateProcessA 함수를 호출합니다.

 Tip. memset이 없다?

위 어셈블리 코드에서 memset 함수는 rep stosd 명령어로 대체되었습니다.

① rep : 반복적인 작업 수행 시 사용하며, 한번 수행시마다 ECX값 -1씩 감소 ( ECX>0 일때까지 수행 )

② stosd : 4byte(DWORD) 단위로 EDI에 EAX값을 저장하며, 실행 될 때마다 EDI값 +4 증가

이를 바탕으로 위의 어셈블리 명령어를 해석하면

"ECX에 넣어준 11h(17번) 횟수 만큼 EDI에 넣어준 StartupInfo 구조체를 EAX에 넣어준 0 값으로 초기화" 입니다.

4byte 단위로 초기화 하므로 총 68byte를 초기화하는 셈이며, 실제 _STARTINFOA 구조체의 크기(sizeof)를 찍어보면 68입니다.

 

 

이러한 초기화 형태는 3)번 항목의 OpenMutex 함수 실행전에 있던 memset 함수에서도 동일하게 적용됩니다.

다만 그곳에서는 stosw 명령어로 2byte 초기화를 연속해서 수행합니다.

memset 이후에 수행되는 __int16(2byte) 형의 v13 변수 초기화를 수행하기 때문입니다.

char 형 변수인 v14 의 경우에는 [ v14 = 0 = null ] 로 처리되어 별도의 stosb 가 수행되지 않은것 같습니다.(추정)

 

어쨋든, 이러한 초기화 작업이 끝나고 나면

CreateProcess 함수를 통해 특정 프로세스를 실행시킵니다.

여기서 눈여겨 보아야할 것은, 실행시키는 프로세스가 무엇이냐 일텐데,

CreateProcess 함수의 두번째 인수에 들어가는 CommandLine 변수가 어떤 값으로 지정되는지는 아직 알 수 없습니다.

 

지금까지 분석한 DLL의 행위를 정리해보면 아래와 같습니다.

- 큰 스택을 사용한다. (이유는 아직 알 수 없다)

- 뮤텍스를 생성하여 DllMain 안에 있는 코드의 중복 실행을 방지한다.

- WinSock을 이용하여 특정 IP주소(127.26.152.13)와 통신한다.

- WInSock을 이용하여 받은 문자열이 "exec"일 경우, 특정 프로세스를 실행시킨다.

  다만, 아직 어떤 프로세스가 실행될지는 알 수 없다.

 

 

2. Lab07-03.EXE

 

1) 커맨드라인 인자 확인

실행파일의 main 함수를 먼저 살펴보면, 아래와 같이 인자의 개수와 값을 확인합니다.

argc 값이 2가 아니거나, strcmp 결과값이 "WARNING_...." 문자열과 일치 않으면 바로 종료됩니다.

 

문자열을 비교하는 부분은, 분홍색의 라이브러리 함수로 간단하게 표시될줄 알았지만

바이트 단위로 연산을 수행하는 어셈블리 명령어로 되어 있어서, 내부 동작을 살펴볼 수 있었습니다.

strcmp 함수 앞에 !(not) 연산자가 붙어 있다 보니, cmp 결과값이 0이 아니면 바로 점프하는 JNZ 명령어가 있습니다.

비교를 하고난 뒤에 TEST CL, CL + JZ 명령어를 통해 문자열의 끝을 체크합니다.

 Tip. 올리디버거 Dump Window

 argument의 값을 체크하려다가 Code Window에서 보니, 문자열 값이 명령어로 표시되더군요.

 

 확인하고 싶은 값을 Hex 또는 아스키값으로 확인하려면 아래와 같이 Dump Window를 활용합니다.

 

 

2) DLL 파일 생성, 읽기, 복사

현재 분석중인 악성코드는 메모리맵 파일을 이용하여 DLL 파일에 접근하고 있습니다.

파일을 올려 읽는 단계는 아래와 같이 이루어집니다.

① CreateFileA : 파일오픈

② CreateFileMappingA : 파일 내용을 메모리에 올림

③ MapViewOfFile : 메모리에 올려진 파일의 첫번째 주소값을 얻음

이와같은 실행흐름을 통해 Kernel132.dll 파일과 Lab07-03.dll 파일을 메모리에 올리고 있습니다.

이후 어떤 행동을 하는지 살펴봅니다.

 Tip. Symbolic Constant 또는 OllyDbg

 함수에 인자가 많을 경우 일일이 MSDN을 찾아가며 파악하면 시간이 너무 오래 걸리는 경우가 많습니다.

 또한 이전에 한번 파악해두었던 함수의 경우엔 Symbolic constant 기능을 이용하면 빠르게 파악이 가능합니다.

 또한 해당 기능을 통해 상수를 지정해 놓으면, Hex-ray 에서 변환된 코드에서도 동일하게 반영됩니다.

 

 

 IDA에서 일일이 Symbolic constant 를 검색을 통해 일일이 적용하기가 부담스러울 수 있습니다.

 MSDN을 뒤져가며 맞는지 확인해야 하는 경우가 있기 때문이죠.

 이럴땐 OllyDbg를 먼저 참조하는것이 도움이 될 수 있습니다.

 아래 스샷에 보시다시피 우리가 IDA에서 바꾸었던 상수들이 표기되어 있음을 알 수 있습니다.

 

 

이어지는 소스코드는 메모리로 복잡한 읽기와 쓰기가 진행됩니다.

프로그램의 모든 마지막 오퍼레이션까지 확인할 수는 없지만

악성코드의 핵심 기능에서 자주 쓰이는 sub_401040 서브루틴은 어떤 오프셋 계산을 수행하는지

뒤쪽에서 4)번 항목에서 분석합니다.

 

산술연산과 오프셋 계산 과정을 마치면,

메인 하단의 소스코드에서 CloseHandle 함수를 이용하여 파일 편집을 종료했음을 알 수 있습니다.

이후 kernel132.dll 에 Lab07-03.dll 파일을 복사합니다.

kerne132.dll은 kernel32.dll 대신 사용할 것이라고 추측할 수 있지만, 로딩될지 여부는 아직 알 수 없습니다.

주목할만한 사항은 sub_4011E0 함수에 전달되는 "C:\\*" 경로값 입니다.

해당 경로를 어떻게 사용하는지 살펴볼 필요성이 있습니다.

 

 

3) 디렉토리 탐색

sub_4011E0 함수 역시 main과 동일하게 각종 산술연산과 오프셋 계산으로 

어떤 동작을 하는지 파악하기가 어려웠습니다. 그래서 일단 Function Calls 윈도우를 통해

하나씩 차근차근 파악을 해보려 합니다. 주목할만한 단서는 2가지입니다.

① FindFirstFileA, FindNextFileA, FindClose + 경로문자열(C:\\*)

② sub_4011E0(본인/재귀함수)

 

①번을 이용하여 인자값으로 "C:\\*" 경로를 넣어주고

FindFirstFileA, FindNextFileA 함수를 이용하여 코딩하면 아래와 같이 파일을 탐색할 수 있습니다.

해당 함수의 결과값을 보았을때, 재귀함수가 사용되었던 이유는

C드라이브의 모든 디렉토리를 재귀함수를 이용하여 탐색하기 위함으로 생각됩니다.

 

함수의 도입부에서 IF문의 조건을 살펴보면

dwFileAttributes(파일속성) 값이 디렉토리(0x10, FILE_ATTRIBUTE_DIRECTORY16) 이며,

strcmp 함수를 통한 비교 결과가 0이 아닌 경우 진입합니다.

strcmp는 동일할 경우 0을 반환하므로, 비교 문자열이 "." 또는 ".." 이 아닌 경우이죠.

정리하면, 디렉토리일 경우에는 진입, 실행파일인 경우에는 else문입니다.

 

이렇게 진입을 나눠 놓은 이유는 재귀함수 구조를 이용하여 디렉토리를 탐색하기 때문일

가능성이 높습니다. 이를 확인하기 위해 코드를 살펴본 결과, 디렉토리인 경우에는

아래와 같이 본인을 호출하는 코드가 있었습니다.

 

탐색 결과가 실행파일인 경우의 else문을 살펴보면

함수의 마지막 부분에서 stricmp 함수를 통해 ".exe" 를 비교하는 부분이 눈에 띕니다.

EXE파일을 따로 추출하여 어떤 작업을 수행하는 부분은 직관적으로 알 수 있었지만,

IF문 안에 들어간 조건문이 생각보다 까다로웠습니다.

 

stricmp 함수의 첫번째 인자를 계산하는 부분은 LEA EBX, [ESP+ECX+154h+FIndFileData.dwReserved1] 명령입니다.

여기서 ECX는 strlen() 함수를 이용한 cFileName의 결과값입니다.

OR ECX, FFFFFFFF 명령어를 통해 최대값을 넣어준 후 repne scasb 명령어를 이용하여 문자열 길이를 얻어냅니다.

 Tip. IDA의 변수 표현법

 IDA에서는 변수를 표현하는 방법이 다양합니다.

 특히 FindFIleData 구조체는 [esp+154h+FindFileData] 으로 표현되고 있습니다.

 자칫 잘못 생각하면 esp에 계속 주소값이 + 된다고 착각할 수 있지만,

 FIndFileData가 선언된 부분을 살펴보면, 주소값이 ebp 기준으로 [-140h] 입니다.

 결국 [esp + 154h - 140h ] = [esp + 14h] 인 것이죠.

 분석가의 가독성을 높이기 위헤 + 로 표현되어 있지만,

 무심코 머리속으로 플러스(+)만 계산하면 낭패를 볼 수 있습니다.

 

 

 [ESP+154h+FindFileData] 명령어에서 154h가 스택포인터인지 알 수 있는 방법은

 아래처럼 [Options - General - Stack pointer] 옵션을 체크하면 됩니다.

 덤으로 [ Auto comment ] 옵션도 체크해 보았는데, Jump 구문이 해석되어 있어 유용했습니다.

 

 

 

 가독성을 높힌 코드가 아니라, 실제 ESP레지스터 기준의 어셈블리어 명령어를 보려면

 마우스 우클릭을 하여 아래와 같이 변경해주면 됩니다.

 

 

stricmp 명령어의 첫번째 인자에서 dwReserved1 주소값이 들어가는 이유는

아래 그림처럼 4바이트 앞의 주소값에서 시작하여 파일이름의 길이만큼 더하면 확장자만 추출할 수 있기 때문입니다.

(Hex-Ray 코드와 어셈블리코드가 dwReserved0과 dwReserved1을 혼용하고 있으므로, 어셈블리어 기준으로 진행)

 

결과적으로는 재귀함수를 통해 디렉토리를 탐색하며 EXE 파일이 보이면

sub_4010A0 서브루틴을 수행하는 것으로 보이며, 어떤 행위를 하는지 이어서 살펴봅니다.

 

 

4) EXE파일 변조, sub_4010A0()

EXE파일이 검색되면, sub_4010A0 함수에 아래와 같이 경로를 포함한 파일을 인자로 넣고 호출합니다.

분석을 용이하기 위해 notepad.exe 파일을 C:\ 경로에 미리 옮겨 놓았습니다.

 

MapViewOfFile 함수를 이용하여 메모리맵 파일의 Starting Address를 이용하여 PE Signature를 체크합니다.

주소값을 이용하기 때문에 중간중간 IsBadReadPtr 함수를 이용하여 포인터의 유효성을 검사합니다.

 Tip. "[ ]" 연산자를 이용한 주소값 계산 [1]

 ptr이 임의의 배열을 가리키는 포인터이고, n이 정수일때 ptr[n] = *(ptr+n) 이 성립합니다.

 즉, 위에서 *((_DWORD *)result +15) 코드에서, result 변수는 void 형이기 때문에

 사용과 동시에 (_DWORD *) 포인터형으로 캐스팅 되었고, "[]" 연산자를 사용할 수 있습니다.

 따라서 4byte(DWORD) 단위로 계산되어, ImageBase에서 60byte 떨어진 곳에 위치하는  

 _IMAGE_DOS_HEADER 구조체의 e_lfanew 멤버를 가리키게 됩니다.

 e_lfanew는 NT header의 오프셋을 가지므로, 

 (int)(char *)result + *((_DWORD *)result +15) 를 통하여 

 _IMAGE_NT_HEADERS 구조체의 Signature 멤버가 PE(45 50 00 00, 17,744) 인지 체크할 수 있습니다. 

 

"PE" 시그니처가 확인되면 sub_401040 함수에 인자 3개를 넣어줍니다.

① [arg1] : 7604 : *(_DWORD *)(v5 + 128) = *(_DWORD *)(3D0160) = Import Directory RVA

② [arg2] : v5 : 3D00E0 = ImageBase + E0 = NT헤더 시작주소 (PE Signature)

③ [arg3] : v2 : 3D0000 = ImageBase

 Tip. "[ ]" 연산자를 이용한 주소값 계산 [2]

 위의 "[]" 연산자와 동일하게 생각하여 v5가 4byte(int) 단위라는 생각에

 무심코 128*4 = 512 주소값을 더하여 계산하면 낭패를 보게됩니다.

 v5는 포인터형이 아닌 int 형이므로, "[]" 연산자의 조건이 성립하지 않습니다.

 따라서 (v5 + 128)의 의미는, v5에 저장된 정수(실제로는 주소)에 128을 더하는 연산입니다.

 OllyDbg로 확인해보면 위와 같이 EBP+80h(128) 으로 계산되는 부분을 확인할 수 있습니다.

 

해당 인자값들을 이용하여 어떤 값을 반환하는지 알아보기 위헤 sub_401040 서브루틴을 살펴봅니다.

함수 내부를 살펴보면, 또다시 매개변수 a1, a2를 이용하여 sub_401000 서브루틴을 실행합니다.

특히 리턴값이 result = a3 + a1 + *(_DWORD *)(result + 20) - *(_DWORD *)(result + 12) 계산식이므로,

sub_401000 의 결과값을 알지 못하면 해석이 불가능합니다.

 

sub_401000 서브루틴으로 한단계 더 깊숙히 들어가봅니다. 중간의 while 구문이 함수의 핵심 기능인것 같습니다.

while 문은 한번 반복할때마다 Import Directory RVA 값과 RVA(섹션시작주소) + VirtualSize(섹션크기) 값을 비교하여

Import Directory RVA 값이 더 크면 result 에 40을 더합니다.

이것이 의미하는 바는, Import Directory RVA 즉, IID가 존재하는 섹션의 헤더를 찾겠다는 의미입니다.

만약 IID가 포함된 섹션이 아니라면, IMAGE_SECTION_HEADER 구조체의 크기인 40씩 더하여

다음 섹션헤더로 넘어갑니다. 반복문에서 탈출하면 계산된 섹션헤더의 주소값을 리턴합니다.

sub_401000 함수의 기능을 정리해보면 아래와 같습니다.

> NT헤더 시작주소(Signature)를 첫번째 인자로 받아서, 두번째 인자의 RVA 주소가 위치하는 섹션 헤더를 찾아 반환

 Tip. sub_401000 서브루틴 주요 변수

 코드를 해석하기 위해 계산한 변수값들은 아래와 같습니다. (굵게 표시된 부분이 RVA)

 ① v3 = 3 (NumberOfSerctions)

    # *(_WORD *)(a2 + 6) = *(_WORD *)(3D00E0(a2) + 6) = *(_WORD *)(3D00E6) = 3

 ② result = 3D001D8 (Section Header Offset, Opiotnal Header 오프셋 + 헤더크기)

    # Opiotional Header Size : *(_WORD *)(a2 + 20) = *(_WORD *)(3D00E0+ 20) = *(_WORD *)(3D00F4) = E0

    # Optional Header Offset : a2 +24 = 3D00E0 +24 = 3D00F8 =NT헤더 오프셋 + NT헤더 크기

 ③ v5 = 1000 (Virtual Address, 메모리에서 섹션의 시작 주소, RVA)

    # *(_DWORD *)(result + 12) = *(_DWORD *)(3D001E4) = 1000

 

이제 sub_401000 서브루틴의 리턴값이 IID가 포함된 섹션헤더의 주소임을 알았습니다.

sub_401040 함수에서 분석하지 못했던 나머지 부분을 다시 살펴보면 아래와 같습니다.

result = a3 + a1 + *(_DWORD *)(result + 20) - *(_DWORD *)(result + 12)

이것을 sub_401000 함수의 리턴값으로 분석해보면 아래와 같습니다.

result = 3D0000(ImageBase) + 7604(RVA) - 1000(VA) + 400(Pointer to Raw Data) : 3D6A04 

해당 공식은 RVA 값을 이용하여 File Offset 을 구하는 공식입니다.

결과적으로 sub_401040 서브루틴의 주요 기능을 정리하면 아래와 같습니다.

> RVA, NT헤더 시작주소(PE Signature), RVA를 인자로 받고, 해당 RVA의 파일 오프셋을 게산하여 반환

 Tip. sub_401040 서브루틴 주요 변수

 result = 3D6A04 (File Offset)

    # result = 3D0000(ImageBase) + 7604(RVA) - 1000(VA) + 400(Pointer to Raw Data) : 3D6A04 

    #  *(_DWORD *)(result + 20) = *(_DWORD *)( 3D001D8+ 20)  = *(_DWORD *)(3D001EC) = 400 (Pointer to Raw Data)

    # *(_DWORD *)(result + 12) = *(_DWORD *)( 3D001D8+ 12) = *(_DWORD *)(3D001E4) = 1000 : (VA)

 

for( i = (int)((char *)v6 + 12); ; i +=20) 코드의 의미는 20byte 단위의 IID(IMAGE_IMPORT_DESCRIPTOR) 구조체에서

12byte 에 위치하고 있는 Name 멤버를 변수 i 에 할당한 후

stricmp 함수로 "kernel32.dll" 문자열을 비교하기 위해 수행하는 반복문입니다.

 

For문 내에서도 sub_401040가 또한번 호출되고 있음을 볼 수 있는데,

IID구조체의 첫번째 변수가 Import Name Table RVA 값을 가지기 때문에

이 역시 파일 오프셋 값으로 변환해야되기 때문입니다.

해당 악성코드가 왜 자꾸 RVA 값을 파일 오프셋으로 변환하는지 알아낼 필요가 있습니다.

 

문자열 비교 후에 do while 문으로 반복을 수행하는 것이 보입니다. 해당 부분은

REPNE SCASB 명령어로 문자열의 길이를 얻는 코드로서, 어셈블리 명령어로 보는것이 가독성이 빠릅니다.

길이를 계산한 이후에는 memcpy 명령어를 이용하여 IID의 Name 멤버의 "kernel32.dll" 문자열을

Lab07-03.dll의 복사 파일인 "kernel132.dll" 문자열로 패치합니다.

실행파일이 임포트하는 DLL을 조작하는 핵심코드이며, 이를 위해서 RVA -> FileOffset 변환이 필요했습니다. 

 Tip. 문자열길이 구하기 : REPNE + SCASB

 해당 명령어의 의미는 아래와 같습니다. 

 - rep : ecx > 0 인 동안 명령어 반복

 - repne : Zero Flag 가 1이고, ecx >0 인 동안 명령어 반복

 - scasb/scasw/scasd ptr : 단위별로 EAX와 ptr 값을 비교

 이것을 이용하여 memcpy 에서 사용되는 v12 변수의 문자열 길이를 구하는 부분을 살펴보면

 MOV EDI, EBX 명령어를 통해 EDI에 문자열을 입력하고 난 후

 OR ECX, FFFFFFFF 명령어를 통해 ECX 값을 최대치로 설정해둡니다.

 REPNE SCAS [EDI] 명령어를 통해 EDI에 저장된 문자(byte, word, dword 단위)와 EAX(=0) 값을

 비교하여 동일한 값이 아니면 [ECX = ECX -1] , [EDI = EDI +1] 연산을 반복합니다.

 EDI에 저장된 문자열의 끝의 Null(=0x00)을 만나거나, ECX의 값이 0이 되면 루틴이 종료되며, 

 마지막에 계속 -1씩 깍던 ECX의 값을 NOT 연산자로 반전시켜 문자열의 길이를 얻습니다.

 

 

패치 이후에는 [v5 + 208], [v5 + 208 + 4] 주소에 있는 값들을 0을 초기화 시킵니다.

해당 위치는 BOUND IMPORT_Directory_Table(BIT) 영역인데, 왜 0 값으로 설정할까요?

리버싱 서적인 나뭇잎 책에서는 패치를 통한 DLL Injection 부분에서

"BIT는 DLL 로딩 속도를 향상시킬 수 있는 기법이며, 그냥 놔둬도 문제 없는 경우가 많지만

인젝션을 테스트할 때 BIT 때문에 정상 동작하지 않아서 0으로 설정 한다" 는 내용이 있었습니다.

아마 우리가 분석중인 악성코드도 정상 실행을 보장하기 위해 해당 코드를 넣지 않았을까 생각됩니다.

모든 작업이 종료되면 UnmapViewOfFile, Close Handle 함수 등으로 할당받은 자원을 해제합니다.

 

지금까지 분석한 EXE의 행위를 정리해보면 아래와 같습니다.

- 메모리맵 파일을 이용한 DLL 복사

- 디렉토리를 탐색하여 EXE파일 검색

- EXE 파일의 IID에서 Name 멤버값이 "kernel32.dll" 인 경우 오프셋 계산

- 반환된 오프셋을 이용하여 정상 DLL의 이름을 악성코드 DLL로 패치

 

------------------------------------------------------------------------------------------------------------------------------------------

3. 문제풀이

------------------------------------------------------------------------------------------------------------------------------------------

 

1) 이 프로그램은 어떤 방식으로 컴퓨터가 재시작할 때마다 실행을 보장하는가?

이 프로그램은 C:\\Windows\System32에 DLL을 복사해서 영구적으로 감염시키고,

C드라이브의 모든 EXE 파일이 해당 DLL을 임포트하도록 변조합니다.

 

2) 이 프로그램을 탐지할 때 호스트 기반으로 좋은 시그니처는 무엇인가?

프로그램은 "kernel132.dll" 파일명으로 하드 코딩되어 있으므로,

호스트기반의 시그니처로 사용할 수 있습니다.

 

3) 이 프로그램의 목적은 무엇인가?

프로그램의 목저은 삭제하기 어려운 원격 호스트 접속을 가능하게 만드는 백도어를 생성하는데 있습니다.

백도어는 두 명령어를 가지는데, 하나는 명령어를 실행시키는 "exec" 이며, 나머지는 "sleep" 입니다.

 

4) 일단 악성코드가 설치된다면 어떻게 삭제할 수 있는가?

해당 프로그램은 C드라이브의 모든 exe 파일을 감염시키므로 매우 삭제하기 힘듭니다.

이런 경우는 백업해서 복구하는게 가장 좋으며, kernel32.dll의 이름을 kerne132.dll로 변경하면

임시적인 대처를 할 수 있습니다.

 

 

------------------------------------------------------------------------------------------------------------------------------------------

4. 마무리

------------------------------------------------------------------------------------------------------------------------------------------

이번 문제는 어디까지 분석을 진행하느냐에 따라 매우 분량이 커질 수 있다고 생각합니다.

실제로 오프셋을 계산하는 많은 부분들을 생략했음에도 불구하고,

지금까지 분석한 실습 악성코드중 가장 많은 포스팅 내용이 나왔습니다.

내용이 많아 오타 또는 잘못된 내용이 있을 수 있으므로,

댓글 남겨주시면 확인 후 수정하겠습니다.

 

By Red_Message.

 

posted by Red_Message