yon11b

[SK 쉴더스 루키즈] Practical Malware Analysis Lab 07-01 풀이 본문

보안/SK 쉴더스 루키즈

[SK 쉴더스 루키즈] Practical Malware Analysis Lab 07-01 풀이

yon11b 2026. 6. 3. 02:57
반응형

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

createserviceA로 Malservice를 services에 자동실행되게 등록함으로써 보장한다.
함수 인자 몇 가지만 보고 가자.
 
lpBinaryPathName
path 쪽에 bp를 건 다음 실행해서 path를 확인해보면,

BinaryPathName = "C:\Documents and Settings\Windows\Desktop\malware_analysis_samples\Practical Malware Analysis Labs\BinaryCollection\Chapter_7L\Lab07_01.exe"

 
.exe 파일 경로가 제대로 들어간 것을 확인할 수 있다.
이제 이 경로가 services에 등록이 된 것이다. 직접 확인해보면 다음과 같다.

automatic으로 되어 있기 때문에, 재부팅을 할 때마다 저 .exe파일이 실행될 것이다.
 
dwStartType
dwStartType==2이면 재부팅 시 자동실행이다.

https://learn.microsoft.com/ko-kr/windows/win32/api/winsvc/nf-winsvc-createservicea

2. 이 프로그램은 왜 뮤텍스를 이용하고 있는가?

컴퓨터가 재시작할 때마다 실행하는데 중복 실행하지 않기 위해서.
 

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

1. 뮤텍스: HGL345

2. 악성파일: Malservice
 

4. 이 악성코드를 탐지할 때 네트워크 기반으로 좋은 시그니처는 무엇인가?

http://www.malwareanalysisbook.com

 
정적분석을 통해서 wininet.dll을 사용하고 있음을 알아냈다. 즉, 네트워크 통신이 있다는 것이다.

 
ida에서 http를 입력하면 외부 url이 검색된다.

 
두 번 클릭한 다음 ctrl+x를 눌러서 따라가 보자.

url을 InternetOpenUrlA로 실행한다.
근데 그러고 바로 또 jmp 해서 위로 올라가 해당 코드를 반복한다.
즉, 무한 반복하는 것이다. → DoS 공격일 가능성!
 

5. 이 프로그램의 목적은 무엇인가?

DDoS 공격
 

6. 이 프로그램은 언제 실행을 종료하는가?

답: 종료를 안 함
InternetOpenUrlA 쪽에서 그래프를 그려보면 startaddress가 있다.
 
StartAddress란?
스레드가 시작할 함수의 주소를 의미한다.
CreateThread()의 3번째 인자인 lpStartAddress에 들어간다. CreateThread 메인 쓰레드 하나, StartAddress 서브 쓰레드 하나 또 실행 되는 것이므로 멀티 쓰레드라고 할 수 있다.

상위 코드로 가보자.
맨 첫번째 줄에 커서를 두고 ctrl+x를 눌러준다.

이 화면이 나온다. CreateThread를 0x14번 반복한다.

mov esi, 14h> esi에 0x14h 할당
dec esi > esi를 하나씩 감소시키면서 반복문 돎
중간에 보이는 lpStartAddress로 인해 아까 위에서 봤던 StartAddress 함수가 실행되어 URL 무한반복 호출 DoS 공격이 일어나게 되는 것이다.
그 밑에 코드를 보면, FFFF FFFF 만큼 sleep을 한다. 이건 메인 스레드를 무한 sleep 상태로 둔다는 말이다.

즉, 저 위치에서 Sleep 하는 건 메인 스레드이고, 이미 생성된 20개 스레드는 각자 StartAddress 안의 코드를 계속 실행한다.
 
정리
무한 sleep하는 이유?
⇒ 프로세스 종료 방지+작업 스레드들이 계속 동작하게 유지하기 위해서
 

치명적인 버그: 타이머 설정부터 무한 sleep까지 흐름 살펴보기

설명


SystemTime 구조체 값 할당
xor edx, edx =⇒ edx = 0
2100년 0월 0일 00:00:00
 
SystemTime -> FileTime 변환: SystemTimeToFileTime

SystemTimeToFileTime(&SystemTime, &DueTime);

 
SetWaitableTimer에서 시간을 FILETIME 형식으로 받기 때문에 변환하는 것이다.

SYSTEMTIME
2100-01-01 00:00:00

↓

FILETIME
Windows 내부 시간 형식

 
 
Waitable Timer 생성: CreateWaitableTimerA

hTimer = CreateWaitableTimerA(NULL, FALSE, NULL);

이름 없는 Waitable Timer 객체 생성
자동 리셋 타이머
보안 속성 기본값
이 함수의 반환값은 eax에 들어간다. (밑에서 mov esi, eax 로 eax 쓰임)
 
타이머 설정: 지정된 시간까지 기다리는 용도: SetWaitableTimer

push 0               ; fResume
push 0               ; lpArgToCompletionRoutine
push 0               ; pfnCompletionRoutine
lea edx, [esp+410h+DueTime]
push 0               ; lPeriod
**push edx             ; lpDueTime**
push esi             ; hTimer
call ds:SetWaitableTimer
  • lPeriod 0 -> 반복 주기 없음
  • lpDueTime까지 기다림

lpDueTime은 SystemTimeToFileTime의 출력값이다. 따라서 lpDueTime=2100년 0월 0일 00:00:00이 된다.
 
hHandle(hTimer)가 신호상태가 될 때까지 무한 대기: WaitForSingleObject

push 0FFFFFFFFh      ; dwMilliseconds
push esi             ; hHandle
call ds:WaitForSingleObject

타이머가 신호 상태가 될 때까지 무한 대기(2100년이 되기 전까지는 아무 실행도 안 하고 그냥 잠잠히 대기만 타고 있음)
⇒ 조건이 충족되면 깨어남
 
++제일 마지막 코드(아까 봤던 부분) - 무한 sleep

 
만약 2100년이 되었다면, 위의 조건이 충족되어 깨어나고, 다음 코드(지금 여기)를 실행하게 된다.
여기서는 쓰레드를 0x14개(20개) 생성하고 무한 sleep을 걸게 된다.
이 무한 sleep은 탈출 조건도 없어서 진짜 영원히 thread가 돌아가게 된다.


SYSTEMTIME 문서
지금 SystemTime 구조체 값이 2100년 0월 0일 00:00:00으로 할당되어 있다.
그런데 SystemTime 구조체에 대한 msdn 문서를 살펴보면 month값과 day은 1부터 시작한다고 나와있다.

https://learn.microsoft.com/ko-kr/windows/win32/api/minwinbase/ns-minwinbase-systemtime

SystemTime 구조체가 잘못된 날짜를 나타내고 있는 것이다.
 
SystemTimeToFileTime 문서
SystemTime → FileTime을 변환을 위해 사용되는 SystemTimeToFileTime 함수 문서도 살펴보면, SystemTime이 잘못된 날짜를 나타내면 반환값으로 False 를 전달한다고 나와있다.

https://learn.microsoft.com/ko-kr/windows/win32/api/timezoneapi/nf-timezoneapi-systemtimetofiletime

그런데! 코드를 보면 SystemTimeToFileTime 함수 반환값에 대한 검사(test eax, eax)를 하지 않고 있다.
그래서 잘못된 값이 그대로 SetWaitableTimer로 넘어가게 된다.
 
SetWaitableTimer 문서

https://learn.microsoft.com/ko-kr/windows/win32/api/synchapi/nf-synchapi-setwaitabletimer

사실 여기서는 잘못된 날짜가 들어왔을 때 어떻게 반응하는지에 대한 내용은 나와있지는 않다.
그럼 그냥 실행을 시켜보자. 어떤 문제가 일어날까?


답부터: CreateThread 부분이 바로 실행이 된다!
 
SystemTimeToFileTime
SystemTimeToFileTime에서 일단 systemtime → filetime 값이 어떻게 나오는지 보자.

systemtime(0x0012F868)은 2100년 0월 0일 0시 0분 0초로 나오고 (34 08 ==2100년이다.)
filetime(0x0012F878)은 0년 0월 0일 0시 0분 0초로 나온다.
systemtime자체가 잘못된 값이니까 filetime에도 제대로 값이 잘 안 들어간 것이다.
 
SetWaitableTimer

arg2는 lpDueTime을 나타낸다. 그 주소로 가보면 여전히(당연히) 0년 0월 0일 0시 0분 0초 값이 저장되어 있다.

BOOL SetWaitableTimer(
  [in]           HANDLE              hTimer,
  [in]           const LARGE_INTEGER *lpDueTime,
  [in]           LONG                lPeriod,
  [in, optional] PTIMERAPCROUTINE    pfnCompletionRoutine,
  [in, optional] LPVOID              lpArgToCompletionRoutine,
  [in]           BOOL                fResume
);

 
그리고 쭉 내려가보면 createThread 밑으로 내려가진다. 즉, createThread가 실행이 된다는 것이다.

 
JNZ에서 다시 PUSH 0으로 올라와 반복문을 실행한다.

 

1월 1일로 바꿔서 실행하면?

0월 0일

 
1월 1일

 
갑자기 F878값도 바뀌었다. 우리는 이미 이 주소가 pFileTime 주소인걸 위에서 봤다.

pFileTime는 pSystemTime이랑 다르게 1601-01-01 00:00:00 UTC부터 흐른 100나노초 단위 카운터 값을 메모리에 나타낸다. 1601-01-01 부터0x022F716377640000나노초 만큼 흐른 날짜가 언제인지를 계산하는 파이썬 코드는 다음과 같다.

from datetime import datetime, timedelta, timezone

filetime = 0x022F716377640000

epoch = datetime(1601, 1, 1, tzinfo=timezone.utc)
dt = epoch + timedelta(microseconds=filetime // 10)

print(dt.strftime("%Y-%m-%d %H:%M:%S UTC"))

결과: 2100-01-01 00:00:00 UTC

잘 변환되어 들어간 것을 확인할 수 있다.
 
그리고 계속 내려가서 WaitForSingleObject까지 가면 이 이상으로는 더 내려갈 수가 없다.

2100년이 될 때까지 계속 wait timer가 돌고 있는 것이다.
 
 
(이해에 도움을 주신 복습방 스터디 멤버 조**와 김**님 감사합니다^^)

728x90