首页
论坛
课程
招聘
[原创]萌新逆向学习笔记——CreateRemoteThread注入Shellcode
2020-9-19 16:31 1654

[原创]萌新逆向学习笔记——CreateRemoteThread注入Shellcode

2020-9-19 16:31
1654

前言

笔者已经有一段时间没发文了,说实话最近学习逆向没劲儿,不知道是不是因为天气总是变化无常,人感觉有点疲惫。

 

友情提示:下面一堆笔者废话,所以只想看技术细节可跳过。

 

之前一直在看韩国人写的《逆向工程核心原理》,但总感觉缺了点什么,于是乎买了本《加密与解密》。总体上来说看到现在给我的感觉就是,很难,似乎较为注重理论知识。与《逆向工程核心原理》一章好几个实践不同,《加密与解密》前面用了大部分章节去介绍诸如动静分析技术,加密算法,window内核等基础理论知识。较为后面才有HOOK,注入等偏实践的内容。即便有实践其过程也没有《逆向工程核心原理》那么详细和细致。所以个人认为,刚入门还是先看《逆向工程核心原理》,根据上面的内容去实践,熟悉win32的开发和一些常用的API,有了基础再去看《加密与解密》。不然就可能会和笔者一样,被《加密与解密》其前面庞大的基础理论内容搞得晕头转向, 丧失一定的兴趣和耐心。

 

回到这次的主题上,这次的内容是使用CreateRemoteThread的方法在目标进程中运行自己写的Shellcode,大部分内容是效仿《加密与解密》中的一些代码。虽然《逆向工程核心原理》中也有提到,但是当时笔者没怎么留意,一些写Shellcode的便捷方法书里也没提到,所以当时也就放弃了。在看到《加密与解密》中用了一些较为人性的方法写Shellcode时,于是便想试试,才有了这篇文章。

原理

提醒:本文全篇使用的均是32位的程序
上一篇文章中,笔者介绍了使用CreateRemoteThread去迫使其他进程执行LoadLibrary并加载我们自定义的DLL以达到目的。而事实上只要我们定义的函数的模板符合CreateRemoteThread参数中定义的函数模板,就能通过远程线程的方式去执行它,而不仅限于LoadLibrary。

1
2
3
DWORD WINAPI ThreadProc(
  _In_ LPVOID lpParameter
);

所以我们只要定义只有一个参数的函数,把它转换成LPTHREAD_START_ROUTINE(这是一个指向上面模板的指针)就可以了。

 

可问题来了,参数只有一个,万一我的函数用了若干个参数呢?而且这个参数必须位于目标进程的虚拟内存当中,否则是无法使用的。

 

对于第二个问题,我们可以使用VirtualAllocEx函数)向目标申请内存虚拟空间。

 

对于第一个问题,我们可以构建一个结构体,存放所有的参数,然后在调用的时候通过内存偏移来访问参数:

1
2
3
4
5
typedef struct _INJECT_DATA
{
    char lpText[8];  //参数1
    char lpCaption[8];  //参数2
}INJECT_DATA;

那有没有更加方便快捷的方法呢?结合上面两点就产生了本篇文章的主旨内容Shellcode。

 

有人说Shellcode仅限于Linux和unix,而经百度似乎也有很多种说法。个人理解是一段被注入后可独立运行,依赖性少或完全没依赖的代码。为了撇开争论,暂且保持书上所说的那样,称之为Shellcode。

 

说的直白一点,其实就是把参数,要运行的函数地址全部放到结构体里,最后以一段用汇编编写的Shellcode运行起来。以调用MessageBoxA弹出窗口的Shellcode为示例:

 

首先是存放Shellcode和参数,函数地址的结构体:

1
2
3
4
5
6
7
typedef struct _INJECT_DATA
{
    BYTE shellCode[0x1D];  //一段ShellCode执行MessageBoxA
    char lpText[8];  //message
    char lpCaption[8];  //title
    LPVOID lpThreadStart;  //MessageBoxA地址
}INJECT_DATA;

一段Shellcode:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
__declspec (naked)
void shellCodeFun(void) {
    __asm {
        call L001;
 
        L001:
        pop ebx;
        //sub ebx, 5;
        and bx, 0;
        push 0;
        lea esi, dword ptr ds : [ebx]INJECT_DATA.lpCaption ;  //0x25注意偏移量
        push esi;
        lea esi, dword ptr ds : [ebx]INJECT_DATA.lpText ;  //0x1D
        push esi;
        push 0;
        call dword ptr ds : [ebx]INJECT_DATA.lpThreadStart ;  //0x2D
        ret;
    }
}

最后把整个结构体通过CreateRemoteThread注入即可。因为结构体的起始地址就是Shellcode的起始地址,因此注入后Shellcode就会运行起来。

实践

步骤一定义和初始化:

定义结构体和编写Shellcode,当然这些都是根据自己的需求来具体定义的,在这里同样以弹框来做示例(Shellcode代码在上面,这里不赘述):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    INJECT_DATA data;
    ZeroMemory(&data, sizeof(INJECT_DATA));
    PBYTE pShellCode = (PBYTE)shellCodeFun;
#ifdef DEBUG
    if (pShellCode[0] == 0xE9)
    {
        //debug环境下会多一个jmp xxxx指令,必须拿到xxxx地址,地址大小为5字节
        //因为jump xxxx,这个xxxx为相对地址,所以为 目前地址+xxxx地址+整个jmp指令长度
        pShellCode = pShellCode + *(ULONG*)(pShellCode + 1) + 5;
    }
#endif // DEBUG
    memcpy(data.shellCode, pShellCode, sizeof(data.shellCode));
    char text[] = "message";
    char title[] = "title";
    memcpy(data.lpText, text, 8);
    memcpy(data.lpCaption, title, 8);
    /*data.lpText = text;
    data.lpCaption = title;*/  //值一定要在目标进程空间内
    HMODULE hmod = GetModuleHandleA("user32.dll");
    FARPROC dialog = GetProcAddress(hmod, "MessageBoxA");
    data.lpThreadStart = dialog;

这里说一下一些注意事项:

  1. 结构体需要初始化,用诸如ZeroMemory的函数来进行初始化,不然会报错。
  2. 说明一下Shellcode中部分代码:

    1
    2
    3
    4
    5
    6
    7
    8
    call L001;  //自己是为了自定位,为了拿到Shellcode开始的地址,也即是结构体的地址。
     
    L001:
    pop ebx;  //拿出call时保存的返回地址
    and bx, 0//因为使用VirtualAllocEx函数申请的虚拟内存都是64kb对齐的,只要低地址。
    push 0;
    lea esi, dword ptr ds : [ebx]INJECT_DATA.lpCaption ;
    ....
  3. 注入进程是debug版本,需要对Shellcode地址做额外处理:

    1
    2
    3
    4
    5
    6
    7
    8
    #ifdef DEBUG
     if (pShellCode[0] == 0xE9)
     {
         //debug环境下会多一个jmp xxxx指令,必须拿到xxxx地址,地址大小为5字节
         //因为jump xxxx,这个xxxx为相对地址,所以为 目前地址+xxxx地址+整个jmp指令长度
         pShellCode = pShellCode + *(ULONG*)(pShellCode + 1) + 5;
     }
    #endif // DEBUG

    如何依旧无法理解,可移步到文末的附加说明。

步骤二申请内存并写入:

通过VirtualAllocEx申请虚拟内存,通过WriteProcessMemory写入虚拟内存:

1
2
3
int injectSize = sizeof(INJECT_DATA);
PBYTE mBuffer = (PBYTE)VirtualAllocEx(mProcess, NULL, injectSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);  //注意要为 PAGE_EXECUTE_READWRITE
WriteProcessMemory(mProcess, mBuffer, &data, injectSize, NULL)

步骤三建立远程线程执行:

1
mRemoteThread = CreateRemoteThread(mProcess, NULL, 0, (LPTHREAD_START_ROUTINE)mBuffer, NULL, 0, NULL);

总结

第一次写Shellcode,虽然看上去简单,但却遇到了很多问题。例如什么是Shellcode中的自定位,Shellcode中如何使用参数,如何使用调试器调试等等。

 

说实话这次将MessageBox作为示例,但个人感觉似乎不太妥当。因为只有当目标程序加载了User32.dll,这段Shellcode才能生效。也就是说这里的Shellcode对User32.dll这个库产生了依赖。或许可以在ntdll.dll中找到与之对应的函数,调用这个才更符合Shellcode无依赖的定义吧。

 

Shellcode的应用很广泛,粗略看了《加密与解密》,书中介绍到很多注入都是编写Shellcode并利用形同如CreateRemoteThread函数能执行一段函数的特性来实现的。所以以后凡是看到一个函数能调用其另一个函数,就有这种注入的可能。

附加说明

第一条:使用调试器调试

这条才是最重要的,因为所有的疑问都能在调试器里找到答案。

 

为了调试远程线程,首先我们在visual studio中对CreateRemoteThread打上断点:

 

1

 

然后用ollydbg或者x32dbg附加目标程序,随后设置线程开始断点:

 

2

 

再然后运行我们的注入程序,程序会在创建远程线程的地方断点:

 

3

 

同时记录我们调用VirtualAllocEx时返回的内存地址,这是我们Shellcode所在的目标进程的地址。

 

在调试器中跳到这个地址,然后我们就能看到自己写的Shellcode:

 

4

 

当然我们来这里不是为了旅游的,打下断点,并且让visual studio中的断点继续运行,调试器会在线程开始的地方断下(因为我们设置过):

 

5

 

F9运行后就能到我们Shellcode的断点了。

 

其实只要找到Shellcode地址下断就行了,并不需要在olldbg/32xdbg中设置线程开始的断点。只不过这样做是为了更直观的看到线程的工作流程。

第二条Shellcode自定位

为什么要用到Shellcode自定位,因为我们要确定Shellcode在目标程序中的虚拟内存地址。

 

为什么要用到这个地址,因为我们Shellcode中用到的参数,如文中弹窗标题,内容等,都需要通过这个地址来寻找。

 

为什么能通过Shellcode地址定位到我们参数的地址?因为我们的Shellcode和参数都是放到同一个结构体中的,而结构体里的成员地址都是连续的。结构体的首地址便是结构体中定义的第一个成员的地址(文中的是Shellcode)。

 

8

 

使用Call会将Call下一条的地址入栈,随后使用pop拿出来这条地址,通过低16位and运算置0(书上说的是因为使用VirtualAllocEx申请的内存大小是64kb对齐,说实话我也没搞懂),或者sub减去5(Call 地址指令的大小),就能拿到基址:

 

7

 

这样,我们就可以通过基址+Shellcode大小+偏移量拿到参数,也可以通过dword ptr ds : [ebx]INJECT_DATA.lpCaption这样的方法拿到参数。

第三条debug版本中Shellcode地址处理

当我们的注入程序是debug版本,在把Shellcode写入结构体前,需要对Shellcode的地址进行处理:

1
2
3
4
5
6
7
8
#ifdef DEBUG
    if (pShellCode[0] == 0xE9)
    {
        //debug环境下会多一个jmp xxxx指令,必须拿到xxxx地址,地址大小为5字节
        //因为jump xxxx,这个xxxx为相对地址,所以为 目前地址+xxxx地址+整个jmp指令长度
        pShellCode = pShellCode + *(ULONG*)(pShellCode + 1) + 5;
    }
#endif // DEBUG

这是因为处于debug版本的注入程序下,会在跳到真正的函数地址前多出一个jmp指令的中间层:

 

9

 

如果此时没有对地址进行处理,本来应该复制Shellcode的代码到结构体成员当中,现在却复制了 jmp xxxx代码,从而造成错误。

 

因此我们在debug版下必须对地址进行处理。
而处理的方法就是将当前指向jmp代码的地址加上jmp后面的地址,最后在加上整个jmp指令的长度。

 

真正的Shellcode地址=006E180C(当前指向jmp的地址)+9EFF(jmp指令后面的地址数据)+5(jmp指令长度)=6EB710

 

造成这种现象是因为debug版本下编译器并没有进行优化,使得函数的调用都有一个jmp表,如上图。

 

而十六进制为E9的jmp为近距离跳转,也就是说jmp后面跟的地址是相对于这条jmp指令的相对地址,也叫偏移。因此,两者相加,最后加上指令本身的长度,就能获得原地址了。

附件

附件中的源程序包含之前文章DLL注入代码,可无视,输入进程PID后,按1即可进行Shellcode注入。

 

https://share.weiyun.com/AhueFz5l


挖华为终端产品漏洞赢巨额奖金! 注明看雪会员还有额外奖励!!

收藏
点赞0
打赏
分享
最新回复 (9)
雪    币: 1130
活跃值: 活跃值 (233)
能力值: ( LV3,RANK:10 )
在线值:
发帖
回帖
粉丝
のばら 活跃值 2020-9-19 21:50
2
0
针不戳,针不戳,精华帖子针不戳
雪    币: 288
活跃值: 活跃值 (622)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
BIX 活跃值 2020-9-19 22:42
3
0
之前学习逆向核心工程原理,就是疑惑debug模式编译的为什么不行,学习了/
雪    币: 2269
活跃值: 活跃值 (1650)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
psycongroo 活跃值 1 2020-9-19 22:47
4
0
BIX 之前学习逆向核心工程原理,就是疑惑debug模式编译的为什么不行,学习了/
对,逆向核心工程原理这书少了一些技术细节,我也是两本一起看的时候才发现了。
雪    币: 1098
活跃值: 活跃值 (56)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
wx_葫芦之恋 活跃值 2020-9-21 13:48
5
0
看的是看雪的加密与解密第四版吗
雪    币: 2269
活跃值: 活跃值 (1650)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
psycongroo 活跃值 1 2020-9-21 18:15
6
0
wx_葫芦之恋 看的是看雪的加密与解密第四版吗
是的
雪    币: 1098
活跃值: 活跃值 (56)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
wx_葫芦之恋 活跃值 2020-9-22 10:31
7
0
psycongroo 是的[em_76]
的确小白看这个很吃力
雪    币: 1098
活跃值: 活跃值 (56)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
wx_葫芦之恋 活跃值 2020-9-22 10:32
8
0
debug模式编译问题是自己调试的时候发现的吗
雪    币: 2269
活跃值: 活跃值 (1650)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
psycongroo 活跃值 1 2020-9-22 19:07
9
0
wx_葫芦之恋 debug模式编译问题是自己调试的时候发现的吗
书上有说,但一开始我没懂,就拿去调试器调试看了下就明白了。
雪    币: 2269
活跃值: 活跃值 (1650)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
psycongroo 活跃值 1 2020-9-22 19:08
10
0
wx_葫芦之恋 的确小白看这个很吃力
先看《逆向工程核心原理》呗,那个简单入门好,《加密与解密》确实很难...尤其是前面实在太多基础了,我还是比较喜欢先做,碰壁之后才去了解。
游客
登录 | 注册 方可回帖
返回