简易 ShellCode 虽然可以正常被执行,但是还存在很多的问题,因为上次所编写的 ShellCode 采用了硬编址的方式来调用相应API函数的,那么就会存在一个很大的缺陷,如果操作系统的版本不统一就会存在调用函数失败甚至是软件卡死的现象,下面我们通过编写一些定位程序,让 ShellCode 能够动态定位我们所需要的API函数地址,从而解决上节课中 ShellCode 的通用性问题。

### 查找 Kernel32.dll 基址

首先我们需要通过汇编的方式来实现动态定位 Kernel32 中的基地址,你或许会有个疑问? 为什么要查找 Kernel32.dll 的地址而不是 User32.dll,这是因为我们最终的目的是调用 MessageBox 这个函数,而该函数位于 User32.dll 这个动态链接库里,默认情况下是无法直接调用的,为了能够调用这个函数,我们就需要调用 LoadLibraryA 函数来加载 User32.dll 这个模块,而 LoadLibraryA 又位于 kernel32.dll 中。

恰巧的是 Kernel32.dll 这个模块只要是 PE 文件都会默认被加载 ,因此我们只需要找到 LoadLibraryA 函数,即可加载任意的动态链接库,并调用任意的函数啦。

由于我们需要动态获取 LoadLibraryA() 以及 ExitProcess() 这两个函数的地址,而这两个函数又是存在于 kernel32.dll 中的,因此这里需要先找到 kernel32.dll 的地址,然后通过对其进行解析,从而查找那两个函数的地址。

这里有一个公式,可以动态的查找到 Kernel32.dll 的地址,如下:

  1. 通过段选择字FS在内存中找到当前的线程环境块TEB。
  2. 线程环境块偏移位置为0x30的地方存放着指向进程环境块PEB的指针。
  3. 进程环境块偏移为 0x0c 存放着指向 PEB_LDR_DATA 的结构体指针。
  4. PEB_LDR_DATA 偏移 0x1c 的地方存放着指向模块初始化链表的头指针。
  5. 初始化链表中,按顺序存放着PE装入运行时初始化模块的相关信息。

既然有了固定的公式,接下我们就使用WinDBG调试器来手工完成对 Kernel32.dll 地址的定位:

1.首先我们运行 WinDBG调试器,然后按下【Ctrl + K】选择文件(File) -> 选择内核调试(Kernel Debug) -> 本地调试(Local) 点击确定。打开后会看到如下界面,直接在最底部输入两条命令,来加载一下符号文件,否则无法进行查看。

2.通过段选择字FS在内存中找到当前的线程环境块TEB。这里可以利用本地调试,输入!teb 指令:

线程环境块偏移位置为 0x30 的地方存放着指向进程环境块PEB的指针。结合上图可见,PEB的地址为0x7ffd7000

3.进程环境块中偏移位置为 0x0c 的地方存放着指向 PEB_LDR_DATA 结构体的指针,其中存放着已经被进程装载的动态链接库的信息。

4.接着 PEB_LDR_DATA 结构体偏移位置为 0x1c 的地方存放着指向模块初始化链表的头指针 InInitializationOrderModuleList

5.模块初始化链表 InInitializationOrderModuleList 中按顺序存放着PE装入运行时初始化模块的信息,第一个链表节点是 ntdll.dll,第二个链表结点就是kernel32.dll。比如可以先看看

InInitializationOrderModuleList 中的内容:

上图中的 0x00191f28 保存的是第一个链节点的指针,解析一下这个结点,可发现如下地址:

上图中 0x7c92000为 ntdll.dll 的基地址,而 0x00191fd0 则是下一个模块的指针,继续跟随 0x00191fd0

观察发现第二个节点偏移 0x08 个字节正是 kernel32.dll的基地址,其地址为 0x7c800000。最有我们来验证一下:

6.通过上方的调试我们可得到公式,接着通过编写一段汇编代码来实现自动的遍历出 kernel32.dl 的基址。

	.386p
.model flat,stdcall
option casemap:none include windows.inc
include kernel32.inc
includelib kerbcli.lib
assume fs:nothing .code
main PROC
xor eax,eax
xor edx,edx
mov eax,fs:[30h] ; 得到PEB结构地址
mov eax,[eax + 0ch] ; 得到PEB_LDR_DATA结构地址
mov esi,[eax + 1ch]
lodsd ; 得到KERNEL32.DLL所在LDR_MODULE结构的
; mov eax,[eax] ; Windows 7 以上要将这里打开
mov edx,[eax + 8h] ; 得到BaseAddress,既Kernel32.dll基址
ret
main ENDP
END main

### 计算函数名 hash 摘要

接着需要对我们所用到的字符串进行 hash 压缩处理,为啥要压缩? 原因是如果直接将函数名压栈的话,我们就需要提供更多的空间来存储 ShellCode 代码,为了能够让我们编写的 ShellCode 代码更加的短小精悍,所以我们将要对字符串进行hash处理,将字符串压缩为一个十六进制数,这样只需要比较二者hash值就能够判断目标函数,尽管这样会引入额外的hash算法,但是却可以节省出存储函数名字的空间。

如下代码是使用 Win32 汇编语言的实现过程,并在 MASM 上正常编译,汇编版字符串转换Hash值。

	.386p
.model flat,stdcall
option casemap:none include windows.inc
include kernel32.inc
include msvcrt.inc
includelib kernel32.lib
includelib msvcrt.lib .data
data db "MessageBoxA",0h
Fomat db "0x%x",0
.code
main PROC
xor eax,eax ; 清空eax寄存器
xor edx,edx ; 清空edx寄存器
lea esi,data ; 取出字符串地址
loops:
movsx eax,byte ptr[esi] ; 每次取出一个字符放入eax中
cmp al,ah ; 验证eax是否为0x0即结束符
jz nops ; 为0则说明计算完毕跳转到nops
ror edx,7 ; 不为零,则进行循环右移7位
add edx,eax ; 将循环右移的值不断累加
inc esi ; esi自增,用于读取下一个字符
jmp loops ; 循环执行
nops:
mov eax,edx ; 结果存在eax里面
invoke crt_printf,addr Fomat,eax
ret
main ENDP
END main

当然也可以使用C语言来实现这个转换的过程,这里使用的是VS Express 2013可正常编译通过,如下代码:

#include <stdio.h>
#include <windows.h> DWORD GetHash(char *fun_name)
{
DWORD digest = 0;
while (*fun_name)
{
digest = ((digest << 25) | (digest >> 7));
digest += *fun_name;
fun_name++;
}
return digest;
} int main()
{
DWORD hash;
hash = GetHash("MessageBoxA");
printf("0x%.8x\n", hash);
getchar();
return 0;
}

我们通过传入不同的函数名称,让其计算出我们所需要计算的三个函数的hash值,其计算结果如下所示:

MessageBoxA              0x1e380a6a
ExitProcess 0x4fd18963
LoadLibraryA 0x0c917432

### 解析 kernel32.dll 导出表

在开头的部分我们通过 WinDBG 调试器已经找到了 Kernel32.dll 这个动态链接库的基地址,而Dll文件本质上也是PE文件,在Dll文件中存在一个导出表,其内部记录着该Dll的导出函数。接着我们需要对Dll文件的导出表进行遍历,不断地搜索,从而找到我们所需要的API函数。

同样的,这里有一个定式,可以通过该定式获取到指定的导出表。

  1. 从 kernel32.dll 加载基址算起,偏移 0x3c 的地方就是其PE文件头。
  2. PE文件头偏移 0x78 的地方存放着指向函数导出表的指针。
  3. 导出表偏移0x1c处的指针指向存储导出函数偏移地址(RVA)的列表。
  4. 导出表偏移0x20处的指针指向存储导出函数函数名的列表。

函数的 RVA 地址和名字按照顺序存放在上述两个列表中,我们可以在列表定位任意函数的RVA地址,通过与动态链接库的基地址相加得到其真实的VA,而计算的地址就是我们最终在 ShellCode 中调用时需要的地址。

pushad                                  ; 保护所有寄存器中的内容
mov eax,[ebp+0x3C] ; 指向PE文件头
mov ecx,[ebp+eax+0x78] ; 导出表的指针
add ecx,ebp
mov ebx,[ecx+0x20] ; 导出函数的名字列表
add ebx,ebp
xor edi,edi ; 用edi寄存器作为索引 ; ------- 循环读取导出表函数
next_loop:
inc edi ; 不断自增索引
mov esi,[ebx+edi*4] ; 从列表数组中读取
add esi,ebp ; esi保存的是函数名称所在地址
cdq

### 提取代码 ShellCode

完整代码如下,下方代码是一个定式,这里就只做了翻译,使用编译器编译如下代码,运行后会弹出一个提示框hello lyshark说明我们成功了。

#include <stdio.h>
#include <windows.h> int main()
{
__asm {
// ===将索要调用的函数hash值入栈保存
CLD // 清空标志位DF
push 0x1E380A6A // 压入MessageBoxA-->user32.dll
push 0x4FD18963 // 压入ExitProcess-->kernel32.dll
push 0x0C917432 // 压入LoadLibraryA-->kernel32.dll
mov esi,esp // 指向堆栈中存放LoadLibraryA的地址
lea edi,[esi-0xc] // 后面会利用edi的值来调用不同的函数 // ===开辟内存空间,这里是堆栈空间
xor ebx,ebx
mov bh,0x04 // ebx为0x400
sub esp,ebx // 开辟0x400大小的空间 // ===将user32.dll入栈
mov bx,0x3233
push ebx // 压入字符'32'
push 0x72657375 // 压入字符 'user'
push esp
xor edx,edx // edx=0 // ===查找kernel32.dll的基地址
mov ebx,fs:[edx+0x30] // [TEB+0x30] -> PEB
mov ecx,[ebx+0xC] // [PEB+0xC] -> PEB_LDR_DATA
mov ecx,[ecx+0x1C] // [PEB_LDR_DATA+0x1C] -> InInitializationOrderModuleList
mov ecx,[ecx] // 进入链表第一个就是ntdll.dll
mov ebp,[ecx+0x8] //ebp = kernel32.dll 的基地址 // === hash 的查找相关
find_lib_functions:
lodsd // eax=[ds*10H+esi],读出来是LoadLibraryA的Hash
cmp eax,0x1E380A6A // 与MessageBoxA的Hash进行比较
jne find_functions // 如果不相等则继续查找
xchg eax,ebp
call [edi-0x8]
xchg eax,ebp // ===在PE文件中查找相应的API函数
find_functions:
pushad
mov eax,[ebp+0x3C] // 指向PE头
mov ecx,[ebp+eax+0x78] // 导出表的指针
add ecx,ebp // ecx=0x78C00000+0x262c
mov ebx,[ecx+0x20] // 导出函数的名字列表
add ebx,ebp // ebx=0x78C00000+0x353C
xor edi,edi // 清空edi中的内容,用作索引 // ===循环读取导出表函数
next_function_loop:
inc edi // edi作为索引,自动递增
mov esi,[ebx+edi*4] // 从列表数组中读取
add esi,ebp // esi保存的是函数名称所在的地址
cdq // ===hash值的运算过程
hash_loop:
movsx eax,byte ptr[esi] // 每次读取一个字节放入eax
cmp al,ah // eax和0做比较,即结束符
jz compare_hash // hash计算完毕跳转
ror edx,7
add edx,eax
inc esi
jmp hash_loop
// ===hash值的比较函数
compare_hash:
cmp edx,[esp+0x1C]
jnz next_function_loop // 比较不成功则查找下一个函数
mov ebx,[ecx+0x24] // ebx=序数表的相对偏移量
add ebx,ebp // ebx=序数表的绝对地址
mov di,[ebx+2*edi] // di=匹配函数的序数
mov ebx,[ecx+0x1C] // ebx=地址表的相对偏移量
add ebx,ebp // ebx=地址表的绝对地址
add ebp,[ebx+4*edi] // 添加到EBP(模块地址库)
xchg eax,ebp // 将func addr移到eax中
pop edi // edi是pushad中最后一个堆栈
stosd
push edi
popad cmp eax,0x1e380a6a // 与MessageBox的hash值比较
jne find_lib_functions // ===下方的代码,就是我们的弹窗
function_call:
xor ebx,ebx // 清空eb寄存器
push ebx // 截断字符串0 push 0x2020206b
push 0x72616873
push 0x796c206f
push 0x6c6c6568 // push hello lyshark
mov eax,esp push ebx // push 0
push eax // push "hello lyshark"
push eax // push "hello lyshark"
push ebx // push 0
call [edi-0x04] // call MessageBoxA push ebx // push 0
call [edi-0x08] // call ExitProcess
}
return 0;
}

编译生成可执行文件以后,我们使用OD打开程序,并手工寻找到程序的 OEP 提取出 ShellCode 的机器码,打开UltraEdit 工具,粘贴机器码,然后按下【Alt + C】进入列模式,编辑只保留机器码即可。

编写并提取通用 ShellCode的更多相关文章

  1. 编写并提取简易 ShellCode

    ShellCode 通常是指一个原始的可执行代码的有效载荷,ShellCode 这个名字来源于攻击者通常会使用这段代码来获得被攻陷系统上的交互 Shell 的访问权限,而现在通常用于描述一段自包含的独 ...

  2. 通用shellcode

    所有 win_32 程序都会加载 ntdll.dll 和 kernel32.dll 这两个最基础的动态链接库.如果想要 在 win_32 平台下定位 kernel32.dll 中的 API 地址,可以 ...

  3. 设计和编写一个异步通用Picker选择器,用于时间日期、城市、商品分类的选择

    目录 一.功能规划 二.最底层基础实现 (1)Picker界面和功能实现 (2)不同类型的选择器基础实现 三.数据源层 (1)时间日期 (2)多级同步分类,如:城市 (3)多级异步分类,如:城市 四. ...

  4. 如何编写测试团队通用的Jmeter脚本

    平时学习.工作过程中,编写的一些jmeter脚本,相信大多数都遇到过这个问题.那就是:如果换一台电脑运行,文件路径不一样,会导致运行失败. 前不久,自己就真真切切遇到过一回,A同学写了个脚本用于压测, ...

  5. 66-Flutter移动电商实战-会员中心_编写ListTile的通用方法

    1.界面分析 通过下图我们可以拆分成 4 部分,头部.订单标题区域.订单列表区域.ListTitle同用部分. 2.UI编写 2.1.头部 主要用到了圆形头像裁剪组件-ClipOval 顶部头像区域W ...

  6. matlab逐行读取text文件,编写函数提取需要的文字

    在数学建模中遇到的数据比较难处理,而且给的是text格式,自己想了好长时间才编出来,现在分享一下,可以交流学习 目标的text文件是 只提取里面的数据 需要自编函数 clc,clear path='D ...

  7. C#编写的序列化通用类代码

    using System; using System.IO; using System.IO.Compression; using System.Runtime.Serialization.Forma ...

  8. 通用shellcode代码

    #include <stdio.h>#include <windows.h> int main(){ __asm { CLD //清空标志位DF push 0x1E380A6A ...

  9. Linux下shellcode的编写

    Linux下shellcode的编写 来源  https://xz.aliyun.com/t/2052 EdvisonV / 2018-02-14 22:00:42 / 浏览数 6638 技术文章 技 ...

随机推荐

  1. GO- 使用JSON

    1 json.Marshal  把对象转换为JSON的方法 原型如下 func Marshal(v interface{}) ([]byte, error)这个函数接收任意类型的数据 v,并转换为字节 ...

  2. 8. String to Integer (atoi) ---Leetcode

    Implement atoi to convert a string to an integer. 题目分析: 题目本身很简单就是将一个字符串转化成一个整数,但是由于字符串的千差万别,导致在实现的时候 ...

  3. Pandas的Categorical Data

    http://liao.cpython.org/pandas15/ Docs » Pandas的Categorical Data类型 15. Pandas的Categorical Data panda ...

  4. TynSerial文件序列(还原)

    TynSerial文件序列(还原) 1)下载文件 procedure TForm1.DownFile(filename: string); // 下载文件 var url: SockString; i ...

  5. el-table的type="selection"的使用

    场景:el-table,type="selection"时,重新请求后,设置列表更新前的已勾选项 踩坑:在翻页或者changPageSize之后,table的data会更新,之前勾 ...

  6. 【转载】 什么是P问题、NP问题和NPC问题

    原文地址: http://www.matrix67.com/blog/archives/105 转载地址: https://www.cnblogs.com/marsggbo/p/9360324.htm ...

  7. Facebook libra开发者文档- 2 -Libra Protocol: Key Concepts核心概念

    Libra Protocol: Key Concepts https://developers.libra.org/docs/libra-protocol Libra区块链是一个加密认证的分布式数据库 ...

  8. 阶段5 3.微服务项目【学成在线】_day16 Spring Security Oauth2_18-认证接口开发-接口开发-service

    定义AuthController 实现刚才写的api接口 controller定义热requestMapping 是 / 就可以了. 因为我们的登陆跟路径就是/auth. 这样到login就是 /au ...

  9. sha256C代码例子

    a.c #include <stdio.h>#include <string.h>#include <openssl/sha.h> 1 int main(int a ...

  10. bat命令编写大全

    bat命令编写大全 摘自:https://blog.csdn.net/haibo19981/article/details/52161653 2016年08月09日 12:26:31 爱睡觉的猫L 阅 ...