常见注入手法第一讲EIP寄存器注入

             常见注入手法第一讲EIP寄存器注入

鉴于注入手法太多,所以这里自己整理一下,每个注入单独一片博客。方便大家简单理解。

但是有的注入可能需要需要注入方法的相结合,什么意思,也就是说以前我们写的汇编代码注入,原理就是通过远程线程注入得来的

所以前提你就要理解远程线程注入

今天我们讲一下EIP寄存器注入。我们上一讲是 异常处理(SEH)第一讲,但是中间岔开了,也是为了整理一下注入手法。所以异常第二讲明天继续。此篇文章主要讲解注入。和异常没有任何关系,如果你是奔着异常处理而来,那么你可以直接去看异常处理。

废话不多说,开始讲解。

我们昨天,也就是异常第一讲的时候,我们知道了我们可以设置寄存器的值,或者获取寄存器的值,微软也帮我们提供了API

但是现在这个API正是我们要用的时候了。

一丶寄存器注入,之写入代码注入

什么是写代码注入,简而言之就是你把代码写进了对方进程进行执行,全程没有任何DLL,而且杀毒不会报毒,属于很强大的手法,因为我们挂起线程,然后写内容进去执行,比如你的软件,你会不会往内存写内容。所以杀毒不能报毒,这个属于很正常的操作。

我们开始吧

昨天简单说了下思路

/*
思路:
1.查找窗口,获得窗口句柄
2.获得线程ID进程PID
3.获得线程句柄,同时也要获得进程的句柄
4.挂起线程
5.获得寄存器的值
6.修改EIP的值
7.申请远程内存
8.写入远程内存,把EIP也要写进去,这样远程执行完毕之后会切换回来继续执行
9.恢复线程
10.关闭线程句柄
*/

一看上面,我们发现我们要写的很多,其实一点也不多,主要上面是思路,体现在代码上很少。

那么从第一步开始写吧

今天我们还是拿我们可爱的32位计算器做实验 :)  (其他的我也没有)

①.查找窗口获得窗口句柄

 HWND hWnd = FindWindow(TEXT("SciCalc"), TEXT("计算器"));
  if (NULL == hWnd)
  {
    MessageBox(NULL, TEXT("对不起,找不到窗口"), TEXT("错误"), MB_OK);
    return 0;
  }

这一步不多讲了,如果想学习注入,API的知识必不可少,所以不会API,请自己查询MSDN,或者Google一下API的意思,在这里我认为大家都已经会了API

②.获得线程的ID

/*2.获得线程的PID和进程的PID*/
  DWORD dwTid = 0;
  DWORD dwPid = 0;
  dwTid = GetWindowThreadProcessId(hWnd, &dwPid);

③.获得进程和线程的句柄

HANDLE hThrHandle = NULL;
HANDLE hProHandle = NULL;
  hThrHandle = OpenThread(THREAD_ALL_ACCESS, FALSE, dwTid);
  if (NULL == hThrHandle)
  {
    MessageBox(NULL, TEXT("对不起,获取线程句柄失败"), TEXT("Title"), MB_OK);
    return 0;
  }
  hProHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid);
  if (NULL == hProHandle)
  {
    MessageBox(NULL, TEXT("对不起,获取进程句柄失败"), TEXT("Title"), MB_OK);
    return 0;
  }

④.挂起线程

 SuspendThread(hThrHandle);  //给个线程的句柄,挂起这个线程

⑤.获取寄存器的值

获取寄存器的值,主要是为了我们要获取当前的EIP的值.然后还回去的时候也需要.

CONTEXT context = { 0 };
context.ContextFlags = CONTEXT_FULL;  //比如初始化标志
BOOL bRet = GetThreadContext(hThrHandle, &context);
if (!bRet)
  {
    MessageBox(NULL, TEXT("对不起,获取寄存器信息失败"), TEXT("Title"), MB_OK);
    return 0;
  }

这里需要注意一下,我们初始化的标志,这个在MSDN中是查询不到的,要到定义结构体地方的位置,看注释可以看到.

这里简单看一下,具体怎么组合的,自己详细去看.

⑥.申请远程内存,一会要写入我们的InjectCOde,(也就是把二进制写进去)

 LPVOID lpCode = NULL;
  lpCode = VirtualAllocEx(hProHandle, NULL, 0x1000, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
  if (NULL == lpCode)
  {
    MessageBox(NULL, TEXT("对不起,申请远程内存失败"), TEXT("Title"), MB_OK);
    return 0;
  }

我们最好输出一下,因为一会使用OD调试的时候,要看下内存是否被申请了.

printf("%p \r\n", lpCode);

⑦.注意Release版本和Debug版本的区别

Release版本,调用函数的时候是直接调用

Debug的版本,调用函数的时候,默认会有一层Jmp跳转

看一下图片,我们要调用任何一个函数(Debug版本下)

调用

在我们眼中,看着是直接Call,但是F11进去,则会看到一个Jmp

JMP

所以,对于Debug版本,我们要取出Jmp的地址,这个地址才是真正的函数地址.

而Release版本,则没有,需要用OD调试,大家自己去看看即可.这里不做演示.

而Release版本,则不用怎么麻烦了,直接写函数地址就行(这里为了下方往我们申请的内存中写函数里面的内容准备的,所以如果是Release版本,直接填上函数名即可.)

Debug版本的获取函数地址.

void *lpAddr = (void *)((long)InjectCode + *(long *)((char *)InjectCode +1)+5);

注意: inject和MyAdd都是一个函数,刚才举例子是用的MyAdd,那个函数没有,纯粹是举例子的.至于InjectCode

下方会仔细讲解.这里简单知道就行

至于上面为什么那样写,我们可以暂时知道这样写能获取函数地址即可.因为重点不在这里.在下方EIP注入的地址重定位问题.鉴于时间关系,大家如果想知道的,自己去OD看下就明白了,或者自己单步拆开来看.

⑧.把InjectCode函数,当做代码,写入到我们申请的空间

WriteProcessMemory(hProHandle, lpCode, lpAddr, 100, NULL);//写入100个字节
如果是Release版本,则不用计算Debug那种公式了.我们直接写成下方代码即可
WriteProcessMemory(hProHandle, lpCode, InjectCode, 100, NULL);//写入100个字节

其实到这里就是简单调用API,往远程写了一块内存而已.现在我们中间省略几步,先把框架写出来,

也就是说这个框架不会变动的.至于中间的这几步,因为很重要,为了防止大家不太明白,所以框架先写出来.

下面具体讲解这几步怎么写.当然,最后我会贴出完整代码.

⑨.执行我们的核心代码...

⑩.修改EIP的值,修改为我们的InjectCode的位置,让EIP跳转到InjectCode的位置执行代码

context.Eip = (DWORD)lpCode;
SetThreadContext(hThrHandle, &context);
注意,LPcode是我们申请的远程的内存的首地址,现在是让EIP指向这个地方,
当做代码运行.核心代码,一定要懂.

 11.释放资源

  ResumeThread(hThrHandle);
  CloseHandle(hProHandle);
  CloseHandle(hThrHandle);      不重要,知道就好

现在我们的框架已经写出来了,现在我们要知道

我们让EIP把我们申请的内存的位置当做代码跑,而我们申请的内存,写入的是我们的INJECTCODE的代码,也就是说这个函数中的所有二进制都当做代码去跑了.

那么我们就可以做点我们的事情了.

二丶注入代码要写入什么

①.Call的讲解,和InjectCode的代码

我们上面说了很多InjectCode,那么这个函数到底是写入的什么__declspec(naked) void InjectCode()

{
  __asm
  {
      NOP
      NOP           //对其一下以后使用
      pushad
      pushf

      push 0
      push 0
      push 0
      push 0

      _emit 0ffh      //offh 和 15h相当于Call
      _emit 015h
      _emit 0x01
      _emit 0x02        //这段二进制其实是随便Call 一个地址.  总结出来汇编代码就是 Call [地址]
      _emit 0x03
      _emit 0x04
      popf
      popad

      _emit 0ffh      //前两个相当于JMP  下面是地址,总结出来是 Call [地址]
      _emit 025h

      _emit 0x00      //跳转的位置,随机写入
      _emit 0x00
      _emit 0x00
      _emit 0x00

    
label1:
    _emit 0x1
    _emit 0x2
    _emit 0x3
    _emit 0x4
label2:
      _emit 0x2
      _emit 0x3
      _emit 0x4
      _emit 0x5
  }
}

好,看到上方代码是不是不想往下看了,但是其实很简单,为啥看上面的代码

我们不直接写汇编代码

这是因为,我用的是2013 (我的天终于换成了2013),但是为什么这样写,因为我被坑了,不这样写不能操作.

在VC++6.0中的写法,我下方贴图

其实你把Call 和我写的二进制当做汇编看就行,因为2013的汇编,和VC6.0的汇编二进制代码不一样,因为段的问题,不太一样,所以只能写成那样了

首先,我们介绍下这两个函数的作用吧

第一个Call, 这个直接Call 标号2取内容 其实就是把标号2定义的4个字节,当做一个函数地址取运行了.  假设 标号2的地址是

0x00400020 ,那么对它取内容就是第一个定义的00的位置.但是注意,call 后面跟的是一个4个字节的地址.

所以说我们取内容,然后把里面的值我们通过我们的手法把一个函数地址的值给它,那么不就相当于调用了我们的函数了吗.

如果不懂,看图:

那么现在经过我讲解,知道为什么我们要定义4个 _emit了吗,因为这个要通过我们的手法,写入一个函数的地址,然后让CALL去调用.

那么现在我们介绍下Jmp的作用

②.Jmp的作用

Jmp的作用和上面一样,就是JMP标号,其实就是JMP 对标号取内容的值当做地址去执行

为什么这样做,因为我们写完我们的代码要让它回到以前执行的代码位置处

而正好我们定义了4个_emit 这4个字节可以通过我们上面框架的时候,通过获取寄存器信息的EIP的值,获得的EIP,然后写到这4个字节中

什么意思? 就是我们上面获得了EIP的值了,那么把这个EIP的值,写入到这4个字节中,那么JMP的时候,就JMP这4个字节,不就实现了还原EIP的位置了吗.

看了怎么多的概念,晕了,那么我们现在讲我们的核心代码

 三丶核心代码的编写

我们上面预留出了第九个步骤,为什么,因为这个步骤要知道的知识太多,虽然代码很少.

我们知道,上面的InjectCode,我们要当做代码执行,而我们总共预留出了8个字节的空间,也就是标号1和标号2

那么我们现在要把一个函数地址,写到这个标号中,还有把获取到的EIP的值,也写到这里面,那么当我们第十步的时候

EIP的值会切换到我们写入的这块内存,而我们写入的就是INJECTCODE,也就是说变相的等于EIP切换到我们写的函数

那么现在就回遇到一个问题,执行我们的代码的时候,如果我们给了函数的地址,那么则会执行这个函数,

如果我们还原了,那么则会注入完成之后还原.

有的人可能会想,很简单,我用WriteprocessMemory把这两个值写入到这里不就完了.

那么现在可以写入,也是没问题的,

但是会出现两个问题.(其实也都算一个问题)

Call的时候我们要Call的标号是不是正确的?

给标号的位置写入内存的时候是不是正确的?

好,告诉你们吧,不正确,因为在自己进程中Call一个标号,相当于Call一个常量.

那么在别人进程中也是Call一个常量.但是位置就不一样了

现在我们要解决这个地址重定位问题.

一丶解决Call的时候的问题

我们都知道,Call的时候,是这样的 

Call  dword ptr[00400000] 二进制代码则是 ff 15 00 00 40 00

那么第一步,我们要算出偏移来

Call的地址位置 - InjectCode的地址位置 = call和injectcode位置的偏移

然后这个偏移 +2 的位置则是我们要修改的地址.

为什么要修改,因为你Call的时候不能这样去Call 我们要保留Call 也就是二进制的 ff 15

那么后边的地址,我们要通过我们代码,把它修改为标号的位置.

如果不懂,看下图片.

那么现在修正了位置,我们就可以写我们的代码了.

代码就两句,其实主要是为了让大家懂原理

 long DestValue = (long)(char *)lpCode + 0x1C; //定位到标号位置,转为整数
  WriteProcessMemory(hProHandle, (char *)lpCode + 0xF,//call的位置后面 +1修改
                     &DestValue, sizeof(DestValue),
                     NULL);

看到没,为什么+1c 为什么 + F,就是上面的内容

+1C 就是首地址 + 1C的偏移,您能找到标号的位置.

+F   也就是首地址 + 偏移,找到Call后面的4个字节地址的位置

现在用 WriteprocessMemory则把Call的地址修改为了标号的位置

Call dwptr[正确的标号]

标号:
    00 00 00 00

那么我们的EIP切还的时候,代码正常执行,遇到这段代码,则会去Call标号里面的内容去调用了,是不是.

但是现在它里面额内容我们应该写成函数指针,这样才会调用函数,现在这是让它正确的知道去哪里Call了

而修改标号的内容,也是算偏移

找到标号的位置.把你想要修改的值写上

看代码

 DestValue = (long)GetProcAddress((GetModuleHandle("user32.dll")), "MessageBoxA");
  WriteProcessMemory(hProHandle, (char *)lpCode + 0x1C, &DestValue, sizeof(DestValue),NULL);

知道为啥+ 1c了吧,首地址 + 偏移等于标号位置,标号位置修改为函数地址,当Call的时候则会call这个函数了

那么我们要换回去也是一样的

找到jmp 后面地址的位置, 首地址 + 偏移 + 2  = jmp 后面地址的位置

然后找到另一个标号位置,把这个标号位置,写入到jmp后面,那么就把jmp的地址修改了.

而标号中的内容,我们可以写成以前EIP的位置,那么不就注入完成之后返回了.

完整代码:

#include <stdio.h>
#include <windows.h>

int MyAdd(int n1, int n2)
{
  return n1 + n2;
}
__declspec(naked) void InjectCode()
{
  __asm
  {
      NOP
      NOP           //对其一下以后使用
      pushad
      pushf

      push 0
      push 0
      push 0
      push 0

      _emit 0ffh
      _emit 015h
      _emit 0x01
      _emit 0x02        //这段二进制其实是随便Call 一个地址.
      _emit 0x03
      _emit 0x04
      popf
      popad

      _emit 0ffh
      _emit 025h

      _emit 0x00      //跳转的位置,随机写入
      _emit 0x00
      _emit 0x00
      _emit 0x00

    
label1:
    _emit 0x1
    _emit 0x2
    _emit 0x3    ;写入EIP返回的地址
    _emit 0x4
label2:
      _emit 0x2
      _emit 0x3
      _emit 0x4   ;存放我们要写入的值,可以写入函数地址,也可以写入EIP返回的地址
      _emit 0x5
  }
}
int main(_In_ int _Argc, _In_reads_(_Argc) _Pre_z_ char ** _Argv, _In_z_ char ** _Env)
{
  /*1.获取窗口句柄*/
  __asm
  {
    NOP
  }
  //InjectCode();
  int r = MyAdd(1, 2);
  HWND hWnd = FindWindow(TEXT("SciCalc"), TEXT("计算器"));
  if (NULL == hWnd)
  {
    MessageBox(NULL, TEXT("对不起,找不到窗口"), TEXT("错误"), MB_OK);
    return 0;
  }
  /*2.获得线程的PID和进程的PID*/
  DWORD dwTid = 0;
  DWORD dwPid = 0;
  dwTid = GetWindowThreadProcessId(hWnd, &dwPid);

  /*3.获得进程和线程的句柄*/
  HANDLE hThrHandle = NULL;
  HANDLE hProHandle = NULL;
  hThrHandle = OpenThread(THREAD_ALL_ACCESS, FALSE, dwTid);
  if (NULL == hThrHandle)
  {
    MessageBox(NULL, TEXT("对不起,获取线程句柄失败"), TEXT("Title"), MB_OK);
    return 0;
  }
  hProHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid);
  if (NULL == hProHandle)
  {
    MessageBox(NULL, TEXT("对不起,获取进程句柄失败"), TEXT("Title"), MB_OK);
    return 0;
  }
  /*4.挂起线程*/
  SuspendThread(hThrHandle);   

  /*5.获取寄存器的值*/
  CONTEXT context = { 0 };
  context.ContextFlags = CONTEXT_FULL;  //比如初始化标志
  BOOL bRet = GetThreadContext(hThrHandle, &context);
  if (!bRet)
  {
    MessageBox(NULL, TEXT("对不起,获取寄存器信息失败"), TEXT("Title"), MB_OK);
    return 0;
  }
  /*6.申请内存*/
  LPVOID lpCode = NULL;
  lpCode = VirtualAllocEx(hProHandle, NULL, 0x1000, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
  if (NULL == lpCode)
  {
    MessageBox(NULL, TEXT("对不起,申请远程内存失败"), TEXT("Title"), MB_OK);
    return 0;
  }
  printf("%p \r\n", lpCode);
  /*因为是Debug版本,所以计算一下JMP跳的位置*/
  char * ch1 = ((char *)InjectCode + 1);
  long ch2 = *(long *)ch1;
  void *lpAddr = (void *)((long)InjectCode + *(long *)((char *)InjectCode +1)+5);
  // InjectCode();
  /*7.写入内存*/
  WriteProcessMemory(hProHandle, lpCode, lpAddr, 100, NULL);//写入100个字节
  /*释放资源*/
  /*8.解决重定位的问题*/
  //找到标号的位置,然后找到jmp的位置,在jmp的2个字节后面,写入标号的位置
  //标号的位置  标号 - 首地址  = 偏移 + 指令大小  首地址 + 偏移 = 标号位置
  long DestValue = (long)(char *)lpCode + 0x1C; //定位到标号位置,转为整数
  WriteProcessMemory(hProHandle, (char *)lpCode + 0xF,//call的位置后面 +1修改
                     &DestValue, sizeof(DestValue),
                     NULL);

  DestValue = (long)GetProcAddress((GetModuleHandle("user32.dll")), "MessageBoxA");
  WriteProcessMemory(hProHandle, (char *)lpCode + 0x1C, &DestValue, sizeof(DestValue),NULL);

  DestValue = (long)(char *)lpCode + 0x20;//找到标号位置
  //写入EIP以前的值,然后JMP跳转到地方. 20  标号位置
  WriteProcessMemory(hProHandle, (char *)lpCode + 0x18, &DestValue, sizeof(DestValue), NULL);//找到EIP的位置
  /*9.修改EIP的值,让其跳转*/
  DestValue = (long)context.Eip;
  WriteProcessMemory(hProHandle, (char *)lpCode + 0x20, &DestValue, sizeof(DestValue), NULL);
  context.Eip = (DWORD)lpCode;
  SetThreadContext(hThrHandle, &context);
  ResumeThread(hThrHandle);
  CloseHandle(hProHandle);
  CloseHandle(hThrHandle);

  return 0;
}

如果不懂,请私信留言.关于地址重定位问题,当然不止这一个办法,比如上次我们写的汇编代码注入,也是解决了地址重定位问题

当然这个还可以写成汇编版本,留作作业,也可以把Messagebox变成Loadlibrary,那么则会执行一个Dll,具体功能你自己在Dll里面编写即可.

这些我会在星期六星期天放到作业当中,自己做一下

下几节课讲解APC注入,以及异常.

课堂资料:

链接:http://pan.baidu.com/s/1hr4ukdA 密码:rlju

原创不易,请爱心点赞评论,转发.如果不会,请下方留言.

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏腾讯数据库技术

Online DDL过程介绍

50930
来自专栏我有一个梦想

vc++ 在程序中运行另一个程序的方法

在vc++ 程序中运行另一个程序的方法有三个: WinExec(),ShellExcute()和CreateProcess() 三个SDK函数: WinExec...

57490
来自专栏IT杂记

INET_ATON()函数在MySQL5.6版本和5.7版本的差异

问题 ### The error occurred while setting parameters ### SQL: insert into t_gatewa...

31890
来自专栏青玉伏案

iOS开发之SQLite-C语言接口规范(一)——Ready And Open Your SQLite

  为什么要搞一搞SQLite的C语言接口规范呢? 因为在做iOS开发中难免会遇到操作数据库的情况,你可以使用第三方的FMDB等,或者使用CoreData。但我...

24750
来自专栏爱撒谎的男孩

用户管理模块之用户注册

53350
来自专栏开发技术

cassandra高级操作之分页的java实现(有项目具体需求)

  接着上篇博客,我们来谈谈java操作cassandra分页,需要注意的是这个分页与我们平时所做的页面分页是不同的,具体有啥不同,大家耐着性子往下看。

14810
来自专栏Java架构沉思录

分布式ID常见解决方案

在分布式系统中,往往需要对大量的数据如订单、账户进行标识,以一个有意义的有序的序列号来作为全局唯一的ID。

70920
来自专栏黑泽君的专栏

day29_Hibernate学习笔记_01

  Hibernate:是一个数据持久化层的ORM框架。   Object:对象,java对象,此处特指JavaBean。   Relational:关系,二维...

8420
来自专栏雨尘分享

2018 - iOS 面试题汇总一般面试题BAT面试题

5.1K30
来自专栏数据库

高级盲注—floor,rand,group by报错注入

大家好,我是你们的老朋友Alex。最近一直在学习SQL注入,发现了很多很多有趣的东西。我就分享我的一篇有关floor,rand,group by报错注入的笔记吧...

31590

扫码关注云+社区

领取腾讯云代金券