🌼 什么是二进制漏洞
二进制漏洞是可执行文件(PE、ELF文件等)因编码时考虑不周,造成的软件执行了非预期的功能。由于二进制漏洞大都涉及到系统层面,所以危害程度比较高。比如经典的office栈溢出漏洞(CVE-2012-0158)、(CVE-2017-11882)以及(CVE-2017-11882)的补丁绕过漏洞(CVE-2018-0802)等,都是危险程度极高的0day和1day漏洞。所以,二进制漏洞的挖掘和分析就显得尤为重要,本篇文章将对常见的二进制漏洞进行简要的介绍和分析。
🌴 常见的二进制漏洞介绍
-
栈溢出漏洞(Stack-Overflow)
-
堆溢出漏洞(Heap-Overflow)
-
释放后重引用漏洞(Use-After-Free)
-
双重释放漏洞(Double-Free)
-
越界访问漏洞(Out-of-bounds)
🌀二进制漏洞挖掘常用工具
- GDB: Linux调试中必要用到的
- gdb-peda、pwndbg、gef: GDB调试插件
- pwntools:写exp和poc的利器
- libc-databases:可以通过泄露的libc的某个函数地址查出远程系统是用的哪个libc版本
- checksec:可以很方便的知道elf程序的安全性和程序的运行平台
- objdump :可以很快的知道elf程序中的关键信息
- readelf:可以很快的知道elf程序中的关键信息
- ida pro:反汇编工具
- ROPgadget:强大的rop利用工具
- one_gadget:可以快速的寻找libc中的调用exec(‘bin/sh’)的位置
🐥 为什么学习二进制安全?
首先提出一个问题,不知道大家有没有碰到过在写代码中遇到过以下这样的问题:
- 明明看起来代码写的毫无问题,但是却在main函数之前就已经报错。
- 碰到一款好的软件,但是广告太烦无法关闭。
- 你想知道一款软件的功能是如何实现的,却苦于没有源代码。
- 软件出现崩溃,想要找到原因却无从下手。
- 电脑中了病毒,杀毒软件却无法查杀。
- 刚刚发布的产品,马上就被人破解了。上面只是提出一部分常见的问题。在学习二进制安全后你可以这样解决以上的问题。
🌵 学习后可以做些什么?
(1). 因为二进制安全需要深入理解计算机的运行机制,操作系统原理。所以碰到此类问题,你可以很快定位到错误位置,为你的编码工作提供强有力的支撑。
(2). 碰到这类软件的时候,你可以通过逆向工程的方式,将其中的广告页面替换,甚至直接将弹出广告的函数直接ret或用nop填充掉。还给自己一个清静没有广告的办公环境。
(3). 当你想知道这个功能如何实现的时候,你既可以静态分析其函数的实现逻辑,也可以动态跟踪它的数据流向,从而完整了解该功能是如何实现的。
(4). 当软件崩溃是不是无从下手,但是学了二进制安全之后你可以使用调试器接管异常,去发掘漏洞产生的位置,定位到模块或者函数的某一条语句。然后提供给厂商修复或者自己二次开发使用补丁修复漏洞。
(5). 当中了杀软无法识别的病毒是不是很恐慌,学习了二进制安全后就可以直接手动分析病毒做了些什么,并且将其执行的操作逆转,拯救中毒的计算机操作系统。
(6). 刚刚发布的软件马上就被别人破解了,没有收入了。是不是很郁闷,当学习了二进制安全后,你可以对你的软件加混淆、加花指令、加反调试、加代码虚拟化等等手段,让破解者的破解成本增加到无法承受,从而放弃破解。
🐼 如何学习二进制安全
上面我们已经讨论过了为什么要学,学了可以做什么。那么接下来我们要讨论的就是如何学习。当初学者兴致勃勃的准备开始学习的时候会发现,虽然书籍众多,却无从下手。一个不小心还会误入歧途,成为一个只懂得是什么,却不懂得为什么的庸人。接下来我们要做的就是,缕清这条线,让其清晰明了。
首先,我们作为二进制研发人员必须要懂的语言有三门半,为什么说是三门半呢,因为这四门语言是C、C++、Python以及汇编。C和C++属于一脉相承因此只算一门半语言,因为这两门语言的学习是连续的,不需要像学习Python或汇编一样重新学习语法结构。那么接下来我来说明一下为什么要学习这三门半语言。首先是C和C++,先不说开发杀毒引擎或者其他大型工程,仅仅有一点最为实际。那就是IDA pro所翻译出的伪代码是C语言格式的。另外在以后漏洞的学习中,除了汇编语言,C和C++是开发shellcode最为高效的语言。而且相比于汇编也更容易理解。并且也许我们想招写一些具有特殊功能的小玩意,比如远(病)控(毒)软(木)件(马),C和C++也一定是你最优秀的选择。或者我们需要写一些驱动代码,比如实现一个类似于火绒剑的监控软件,毕竟,你总不能去用易语言开发吧。现在的系统也不兼容啊对不对。
那么介绍完C和C++的用处,接下来说明下Python的作用。其实Python就像是一个万能胶水,哪里有用粘哪里。它可以辅助我们快捷的开发一些辅助分析工具或者Exploit。也可以充当解放双手释放劳动力解决重复操作的机器人。总之,Python除了性能感人,其他都不会让你失望的。最后说明一下汇编语言的作用。其实这是最不用解释的了。毕竟,动态分析中没有任何一款工具可以把代码还原成伪代码执行,我们分析过程中所接触的全都是汇编代码。
上面扯了一大段为什么要学习这些语言,那么下面来说明下如何学习这些语言。其实很简单,就是看书。当然如果觉得乏味,靠谱的教学视频也是可以的。
C语言有很多本书可以选择,但是我只推荐一本,避免选择困难。这本书叫做《C Primer Plus》,这本书的作者叫Stephen Prata,目前这本书已经翻译成中文版了,可以直接在网上购买。这本书详尽的说明了C语言语法你所需要了解的一切。推荐花30天时间简单学习,然后就开始学习C++。C++的学习我推荐使用《C++ Primer》作为教材。推荐学习时间依然是30天。
你以为我会让你接下来直接开始学Python?你太天真了。
C和C++在简单看完这两本书的情况,你只是知道,却不会使用。这时候你最需要的就是练习了。如果以前数学学的还不错,那就搞本《算法导论》,练习里面的算法与数据结构。如果基础差一点,就买一本国内出版的零基础学算法一类的书练习里面的例子。这个阶段我的推荐学习时长为60天。对,你没看错,和学习语法的时间一样长。
当你练习了这么久之后,就可以继续学习语言了。没错,终于到Python了,买本《Python核心编程》吧。你已经有了不错的基础,相信我,一周之内看完这本书你没问题的。不需要你记住所有库的 使用,Python的库实在太多了,你只需要记住大概有个什么库可以解决相关问题,需要的时候再去网上查就好了。
最后是我们的汇编语言,最艰苦的日子到了,这是个很难学的东西。我学习的时候也感觉头冷,但是很不幸,这是一门必要的语言。
汇编语言我推荐使用《汇编语言 基于x86处理器》这本书来学习。也许很多人都是学习的王爽老师的那本《汇编语言》入门的。我是我还是想推荐这本,因为这本书是我见过最容易理解的汇编语言教材。至此,语言阶段结束。 当我们学习完语言基础之后,我们需要学习的就是系统编程了,这个阶段我们只需要看两本书,但是别高兴的太早,因为这两本书都是大部头,好在我们不是专业开发人员,所以不需要看的那么细致。
第一本《Windows程序设计》,这一本主要讲解的是GUI编程。我们也就是写写安全工具,所以简单看看就行。但是一些以后可能会发现一些奇淫技巧,比如界面藏私钥这种操作。遇到了在了解就行。第二本是《Windows核心编程》,这本书主要讲的是Win32 API编程,需要好好了解一下,因为Windows下的恶意代码一类的,万变不离其宗,最后还是要走API这条路。
这个阶段的推荐学习时间是90天。学习完系统编程,我们要学习的是Windows下的文件结构,也就是PE(Portable Executable)结构,这个结构对于我们分析病毒还是漏洞都有着至关重要的作用,因此需要详细了解,讲解这个结构的书非常多,
我推荐使用《Windows PE权威指南》,这样既学习了PE结构,又复习了汇编语言。推荐时长30天。学习了这么多基础,铺垫了这么久。我们终于要开始学逆向了。软件逆向我依然只推荐一本书《c++反汇编与逆向分析技术揭秘》,软件逆向看这一本书就够了,但是看完之后建议看一些综合类的实战书籍,比如《加密与解密4》,然后我们还需要对Windows的调试原理有些了解,这里使用《Windows高级调试》以及《软件调试》来学习。
推荐学习时长90天
。
最后我们要选择的就是细分领域了,当然,也可以全学。
恶意代码分析我推荐《恶意代码分析实战》,
漏洞分析我推荐《0day安全:软件漏洞分析技术》入门,
《漏洞战争》进阶。《shellcoder编程揭秘》来了解漏洞利用中的奇淫技巧。很不幸的是,我还没有发现一本值得推荐漏洞挖掘的书籍。此阶段建议学习时长90天。
至此,我们的安全学习就已经踏上了正轨,需要在工作中不断精进,不断成长。
0x01栈溢出漏洞原理
栈溢出漏洞属于缓冲区漏洞的一种,实例如下:
#include <stdio.h>
#include <string.h>
int main()
{
char *str = "AAAAAAAAAAAAAAAAAAAAAAAA";
vulnfun(str);
return;
}
int vulnfun(char *str)
{
char stack[10];
strcpy(stack,str); // 这里造成溢出!
}
编译后使用windbg运行
直接运行到了地址0x41414141,这个就是字符串AAAA,就是变量str里面的字符串通过strcpy拷贝到栈空间时,没有对字符串长度做限制,导致了栈溢出,最后覆盖到了返回地址,造成程序崩溃。
溢出后的栈空间布局如下:
栈溢出原理图
0x02 堆溢出漏洞原理
使用以下代码演示堆溢出漏洞
#include <windows.h>
#include <stdio.h>
int main ( )
{
HANDLE hHeap;
char *heap;
char str[] = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
hHeap = HeapCreate(HEAP_GENERATE_EXCEPTIONS, 0x1000, 0xffff);
getchar(); // 用于暂停程序,便于调试器加载
heap = HeapAlloc(hHeap, 0, 0x10);
printf("heap addr:0x%08x\n",heap);
strcpy(heap,str); // 导致堆溢出
HeapFree(hHeap, 0, heap); // 触发崩溃
HeapDestroy(hHeap);
return 0;
}
由于调试堆和常态堆的结构不同,在演示代码中加入getchar函数,用于暂停进程,方便运行heapoverflowexe后用调试器附加进程。debug版本和Release版本实际运行的进程中各个内存结构和分配过程也不同,因此测试的时候应该编译成release版本。
运行程序,使用windbg附加调试(一定要附加调试),g运行后程序崩溃
上面的ecx已经被AAAA字符串覆盖掉了,最后在引用该地址的时候导致崩溃,通过前面的栈回溯定位到了main函数入口,找到复制字符串的函数下断点
此时堆块已经分配完毕,对应的分配地址位于0x007104a0,0x007104a0是堆块数据的起始地址,并非堆头信息的起始地址,对于已经分配的堆块,开头有8字节的HEAP_ENTRY结构,因此heap的HEAP_ENTRY结构位于0x007104a0-8=0x710498。
在windbg上查看两个堆块的信息,这两个堆块目前处于占用状态,共有0x10大小空间
0:000> !heap -p -a 0x710498
address 00710498 found in
_HEAP @ 710000
HEAP_ENTRY Size Prev Flags UserPtr UserSize - state
00710498 0005 0000 [00] 007104a0 00010 - (busy)
在windbg中,使用!heap查看HeapCreate创建的整个堆块信息,可以发现堆块heap后面还有一个空闲堆块0x007104c0:
0:000> !heap
Heap Address NT/Segment Heap
560000 NT Heap
800000 NT Heap
710000 NT Heap
0:000> !heap -a 710000
HEAPEXT: Unable to get address of ntdll!RtlpHeapInvalidBadAddress.
Index Address Name Debugging options enabled
3: 00710000
Segment at 00710000 to 00720000 (00001000 bytes committed)
Flags: 40001064
ForceFlags: 40000064
Granularity: 8 bytes
Segment Reserve: 00100000
Segment Commit: 00002000
DeCommit Block Thres: 00000200
DeCommit Total Thres: 00002000
Total Free Size: 00000164
Max. Allocation Size: 7ffdefff
Lock Variable at: 00710248
Next TagIndex: 0000
Maximum TagIndex: 0000
Tag Entries: 00000000
PsuedoTag Entries: 00000000
Virtual Alloc List: 0071009c
Uncommitted ranges: 0071008c
00711000: 0000f000 (61440 bytes)
FreeList[ 00 ] at 007100c0: 007104c8 . 007104c8
007104c0: 00028 . 00b20 [104] - free
Segment00 at 00710000:
Flags: 00000000
Base: 00710000
First Entry: 00710498
Last Entry: 00720000
Total Pages: 00000010
Total UnCommit: 0000000f
Largest UnCommit:00000000
UnCommitted Ranges: (1)
Heap entries for Segment00 in Heap 00710000
address: psize . size flags state (requested size)
00710000: 00000 . 00498 [101] - busy (497)
00710498: 00498 . 00028 [107] - busy (10), tail fill //heap的占用堆块
007104c0: 00028 . 00b20 [104] free fill //空闲堆块
00710fe0: 00b20 . 00020 [111] - busy (1d)
00711000: 0000f000 - uncommitted bytes.
在复制字符串的时候,原本只有0x10大小的堆块,填充过多的字符串的时候就会覆盖到下方的空闲堆块007104c0,在复制前007104c0空闲堆块的HEAP_FREE_ENTRY结构数据如下:
0:000> dt _HEAP_FREE_ENTRY 0x007104c0
ntdll!_HEAP_FREE_ENTRY
+0x000 HeapEntry : _HEAP_ENTRY
+0x000 UnpackedEntry : _HEAP_UNPACKED_ENTRY
+0x000 Size : 0x6298
+0x002 Flags : 0x16 ''
+0x003 SmallTagIndex : 0xac ''
+0x000 SubSegmentCode : 0xac166298
+0x004 PreviousSize : 0xcfb9
+0x006 SegmentOffset : 0 ''
+0x006 LFHFlags : 0 ''
+0x007 UnusedBytes : 0 ''
+0x000 ExtendedEntry : _HEAP_EXTENDED_ENTRY
+0x000 FunctionIndex : 0x6298
+0x002 ContextValue : 0xac16
+0x000 InterceptorValue : 0xac166298
+0x004 UnusedBytesLength : 0xcfb9
+0x006 EntryOffset : 0 ''
+0x007 ExtendedBlockSignature : 0 ''
+0x000 Code1 : 0xac166298
+0x004 Code2 : 0xcfb9
+0x006 Code3 : 0 ''
+0x007 Code4 : 0 ''
+0x004 Code234 : 0xcfb9
+0x000 AgregateCode : 0x0000cfb9`ac166298
+0x008 FreeList : _LIST_ENTRY [ 0x7100c0 - 0x7100c0 ]
0:000> dt _LIST_ENTRY 0x007104c0+8
ntdll!_LIST_ENTRY
[ 0x7100c0 - 0x7100c0 ]
+0x000 Flink : 0x007100c0 _LIST_ENTRY [ 0x7104c8 - 0x7104c8 ]
+0x004 Blink : 0x007100c0 _LIST_ENTRY [ 0x7104c8 - 0x7104c8 ]
覆盖后0x007104c0空闲块的HEAP_FREE_ENTRY结构数据如下:
0:000> g
(2c08.234): Access violation - code c0000005 (!!! second chance !!!)
eax=007e04a0 ebx=007e0498 ecx=41414141 edx=007e0260 esi=007e04b8 edi=007e0000
eip=7775919d esp=0019fdb0 ebp=0019fea8 iopl=0 nv up ei ng nz na po cy
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010283
ntdll!RtlpFreeHeap+0x5bd:
7775919d 8b11 mov edx,dword ptr [ecx] ds:002b:41414141=????????
0:000> dt _HEAP_FREE_ENTRY 0x007104c0
ntdll!_HEAP_FREE_ENTRY
+0x000 HeapEntry : _HEAP_ENTRY
+0x000 UnpackedEntry : _HEAP_UNPACKED_ENTRY
+0x000 Size : ??
+0x002 Flags : ??
+0x003 SmallTagIndex : ??
+0x000 SubSegmentCode : ??
+0x004 PreviousSize : ??
+0x006 SegmentOffset : ??
+0x006 LFHFlags : ??
+0x007 UnusedBytes : ??
+0x000 ExtendedEntry : _HEAP_EXTENDED_ENTRY
+0x000 FunctionIndex : ??
+0x002 ContextValue : ??
+0x000 InterceptorValue : ??
+0x004 UnusedBytesLength : ??
+0x006 EntryOffset : ??
+0x007 ExtendedBlockSignature : ??
+0x000 Code1 : ??
+0x004 Code2 : ??
+0x006 Code3 : ??
+0x007 Code4 : ??
+0x004 Code234 : ??
+0x000 AgregateCode : ??
+0x008 FreeList : _LIST_ENTRY
Memory read error 007104c0
整个空闲堆头信息都被覆盖了,包括最后的空闲链表中的前后向指针都被成了0x41414141,后面调用HeapFree释放堆块的时候,就会将buf2和后面的空闲堆块0x007104c0合并,修改两个空闲堆块的前后向指针就会引用0x41414141,最后造成崩溃。
如果把上面释放堆块的操作换成分配堆块HeapAlloc(),也会导致崩溃,因为在分配堆块的时候会去遍历空闲链表指针,会造成地址引用异常,当内存中已经分配多个堆块的时候,可能覆盖到的就是已经分配到的堆块,此时可能就是覆盖HEAP_ENTRY结构,而不是HEAP_FREE_ENTRY结构。
堆溢出原理图
0x03 堆调试技巧
微软提供了一些调试选项用于辅助堆调试,可以通过windbg提供的gflag.exe或者!gflag命令来设置:
htc:堆尾检查,是否发生溢出
hfc:堆释放检查
hpc:堆参数检查
hpa:启用页堆
htg:堆标志
ust:用户态栈回溯
对heapoverflow.exe添加堆尾检查和页堆,去掉堆标志:
gflags.exe -i F:\vulns\Release\heapoverflow +htc +hpa +htg
堆尾检查
主要是在每个堆块的尾部,用户数据之后添加8字节,通常是连续的2个0xabababab,该数据段被破坏就可能发生了溢出。
对heapoverflow.exe开启hpc和htc,用windbg加载对heapoverflow程序,附加进程无法在堆尾添加额外标志,使用以下命令开启堆尾检查和堆参数检查:
0:000> !gflag +htc +hpc
Current NtGlobalFlag contents: 0x00000070
htc - Enable heap tail checking
hfc - Enable heap free checking
hpc - Enable heap parameter checking
0:000:x86> g
HEAP[heapoverflow.exe]: Heap block at 001E0498 modified at 001E04B0 past requested size of 10
(13d0.3c9c): WOW64 breakpoint - code 4000001f (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
ntdll_77710000!RtlpBreakPointHeap+0x13:
777f07c7 cc int 3
执行命令g后,按下回车键程序会断下来
0:000:x86> kb
# ChildEBP RetAddr Args to Child
00 0019fd18 777dd85b 00000000 001e0000 001e0498 ntdll_77710000!RtlpBreakPointHeap+0x13
01 0019fd30 77793e9c 001e0000 00000000 77786780 ntdll_77710000!RtlpCheckBusyBlockTail+0x1a2
02 0019fd4c 777ef9f1 7772e4dc 9ceeef49 001e0000 ntdll_77710000!RtlpValidateHeapEntry+0x633d9
03 0019fda4 7775991d 001e04a0 9ceeec45 001e0498 ntdll_77710000!RtlDebugFreeHeap+0xbf
04 0019fea8 77758b98 001e0498 001e04a0 001e04c1 ntdll_77710000!RtlpFreeHeap+0xd3d
*** WARNING: Unable to verify checksum for F:\vulns\Release\heapoverflow.exe
05 0019fefc 00401094 001e0000 00000000 001e04a0 ntdll_77710000!RtlFreeHeap+0x758
WARNING: Stack unwind information not available. Following frames may be wrong.
06 001e0000 01000709 ffeeffee 00000000 001e00a4 heapoverflow+0x1094
07 001e0004 ffeeffee 00000000 001e00a4 001e00a4 0x1000709
08 001e0008 00000000 001e00a4 001e00a4 001e0000 0xffeeffee
HEAP[heapoverflow.exe]: Heap block at 001E0498 modified at 001E04B0 past requested size of 10
上面一句调试输出信息的意思是,在大小为0x10的堆块0x001E0498的0x001E04B0覆盖破坏了,0x10大小的空间加上堆头的8字节一共0x18字节,0x001E04B0-0x001E0498=0x18,也就是说0x001E04B0位于堆块数据的最后一个字节上,基于上面的信息,可以分析出程序主要是因为向0x10的堆块中复制过多数据导致的堆溢出。
页堆
在调试漏洞的时候,经常需要定位导致漏洞的代码和函数,比如导致堆溢出的字节复制指令rep movsz等,前面的堆尾检查方式主要是堆被破坏的场景,不利于定位导致漏洞的代码。为此。引入了页堆的概念,开启后,会在堆块中增加不可访问的栅栏页,溢出覆盖到栅栏页就会触发异常。
开启页堆:
gflags.exe -i F:\vulns\Release\heapoverflow +hpa
用windbg加载heapoverflow,运行!gflag命令开启了页堆,然后g运行后在cmd按下回车键断下
0:000> g
(46c.b74): Access violation - code c0000005 (!!! second chance !!!)
eax=00000021 ebx=01795ff0 ecx=00000004 edx=77d364f4 esi=0012ff38 edi=01796000
eip=00401084 esp=0012ff10 ebp=01790000 iopl=0 nv up ei pl nz na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00010202
image00400000+0x1084:
00401084 f3a5 rep movs dword ptr es:[edi],dword ptr [esi]
0:000> dd esi
0012ff38 41414141 41414141 41414141 41414141
0012ff48 00407000 00401327 00000001 01699fb0
0012ff58 0169bf70 00000000 00000000 7ffdd000
0012ff68 c0000005 00000000 0012ff5c 0012fb1c
0012ff78 0012ffc4 00402c50 004060b8 00000000
0012ff88 0012ff94 76281174 7ffdd000 0012ffd4
0012ff98 77d4b3f5 7ffdd000 77cb48a4 00000000
0012ffa8 00000000 7ffdd000 c0000005 76292b35
0:000> dc edi
01796000 ???????? ???????? ???????? ???????? ????????????????
01796010 ???????? ???????? ???????? ???????? ????????????????
01796020 ???????? ???????? ???????? ???????? ????????????????
01796030 ???????? ???????? ???????? ???????? ????????????????
01796040 ???????? ???????? ???????? ???????? ????????????????
01796050 ???????? ???????? ???????? ???????? ????????????????
01796060 ???????? ???????? ???????? ???????? ????????????????
01796070 ???????? ???????? ???????? ???????? ????????????????
可以发现程序在复制A字符串的时候触发了异常,程序复制到0x11字节的时候被断下,此时异常还未破坏到堆块,直接定位导致溢出的复制指令rep movs
0:000> kb
ChildEBP RetAddr Args to Child
WARNING: Stack unwind information not available. Following frames may be wrong.
0012ff48 00401327 00000001 01699fb0 0169bf70 image00400000+0x1084
*** ERROR: Symbol file could not be found. Defaulted to export symbols for C:\Windows\system32\kernel32.dll -
0012ff88 76281174 7ffdd000 0012ffd4 77d4b3f5 image00400000+0x1327
0012ff94 77d4b3f5 7ffdd000 77cb48a4 00000000 kernel32!BaseThreadInitThunk+0x12
0012ffd4 77d4b3c8 0040b000 7ffdd000 00000000 ntdll!RtlInitializeExceptionChain+0x63
0012ffec 00000000 0040b000 7ffdd000 00000000 ntdll!RtlInitializeExceptionChain+0x36
0:000> ub image00400000+0x1327
image00400000+0x1301:
00401301 e847120000 call image00400000+0x254d (0040254d)
00401306 e8af0e0000 call image00400000+0x21ba (004021ba)
0040130b a150994000 mov eax,dword ptr [image00400000+0x9950 (00409950)]
00401310 a354994000 mov dword ptr [image00400000+0x9954 (00409954)],eax
00401315 50 push eax
00401316 ff3548994000 push dword ptr [image00400000+0x9948 (00409948)]
0040131c ff3544994000 push dword ptr [image00400000+0x9944 (00409944)]
00401322 e8d9fcffff call image00400000+0x1000 (00401000)
根据栈回溯,调用rep movs的上一层函数位于image00400000+0x1084的上一条指令,也就是00401322,此处调用了00401000函数,很容易发现这是主入口函数:
0:000> uf 00401000
image00400000+0x1000:
00401000 83ec24 sub esp,24h
00401003 b908000000 mov ecx,8
00401008 53 push ebx
00401009 55 push ebp
0040100a 56 push esi
0040100b 57 push edi
0040100c be44704000 mov esi,offset image00400000+0x7044 (00407044)
00401011 8d7c2410 lea edi,[esp+10h]
00401015 f3a5 rep movs dword ptr es:[edi],dword ptr [esi]
00401017 68ffff0000 push 0FFFFh
0040101c 6800100000 push 1000h //堆块大小压入
00401021 6a04 push 4
00401023 a4 movs byte ptr es:[edi],byte ptr [esi]
00401024 ff150c604000 call dword ptr [image00400000+0x600c (0040600c)]//调用HeapCreate创建堆块
0040102a 8be8 mov ebp,eax
0040102c a16c704000 mov eax,dword ptr [image00400000+0x706c (0040706c)]
00401031 48 dec eax
00401032 a36c704000 mov dword ptr [image00400000+0x706c (0040706c)],eax
00401037 7808 js image00400000+0x1041 (00401041)
image00400000+0x1039:
00401039 ff0568704000 inc dword ptr [image00400000+0x7068 (00407068)]
0040103f eb0d jmp image00400000+0x104e (0040104e)
image00400000+0x1041:
00401041 6868704000 push offset image00400000+0x7068 (00407068)
00401046 e896000000 call image00400000+0x10e1 (004010e1)
0040104b 83c404 add esp,4
image00400000+0x104e:
0040104e 6a10 push 10h
00401050 6a00 push 0
00401052 55 push ebp
00401053 ff1508604000 call dword ptr [image00400000+0x6008 (00406008)] //调用HeapAlloc分配0x10的堆块
00401059 8bd8 mov ebx,eax //分配的堆块地址
0040105b 53 push ebx
0040105c 6830704000 push offset image00400000+0x7030 (00407030)
00401061 e84a000000 call image00400000+0x10b0 (004010b0)
00401066 8d7c2418 lea edi,[esp+18h]
0040106a 83c9ff or ecx,0FFFFFFFFh
0040106d 33c0 xor eax,eax
0040106f 83c408 add esp,8
00401072 f2ae repne scas byte ptr es:[edi]
00401074 f7d1 not ecx //获取str长度
00401076 2bf9 sub edi,ecx
00401078 53 push ebx
00401079 8bc1 mov eax,ecx
0040107b 8bf7 mov esi,edi //str = 0x20
0040107d 8bfb mov edi,ebx //分配的堆块只有0x10
0040107f 6a00 push 0
00401081 c1e902 shr ecx,2
00401084 f3a5 rep movs dword ptr es:[edi],dword ptr [esi]
00401086 8bc8 mov ecx,eax
00401088 55 push ebp
00401089 83e103 and ecx,3
0040108c f3a4 rep movs byte ptr es:[edi],byte ptr [esi] //0x20 < 0x10 循环复制导致溢出
0040108e ff1504604000 call dword ptr [image00400000+0x6004 (00406004)]
00401094 55 push ebp
00401095 ff1500604000 call dword ptr [image00400000+0x6000 (00406000)]
0040109b 5f pop edi
0040109c 5e pop esi
0040109d 5d pop ebp
0040109e 33c0 xor eax,eax
004010a0 5b pop ebx
004010a1 83c424 add esp,24h
004010a4 c3 ret
0x04整数溢出漏洞原理
整数分为有符号和无符号两类,有符号数以最高位作为符号位,正整数最高位为1,负整数最高位为0,不同类型的整数在内存中有不同的取值范围,unsigned int = 4字节,int = 4字节,当存储的数值超过该类型整数的最大值就会发生溢出。
在一些有符号和无符号转换的过程中最有可能发生整数溢出漏洞。
基于栈的整数溢出
基于栈的整数溢出的例子:
#include "stdio.h"
#include "string.h"
int main(int argc, char *argv){
int i;
char buf[8]; // 栈缓冲区
unsigned short int size; // 无符号短整数取值范围:0 ~ 65535
char overflow[65550];
memset(overflow,65,sizeof(overflow)); // 填充为“A”字符
printf("请输入数值:\n");
scanf("%d",&i); //输入65540
size = i;
printf("size:%d\n",size); // 输出系统识别出来的size数值 4
printf("i:%d\n",i); // 输出系统识别出来的i数据 65540
if (size > 8) //边界检查
return -1;
memcpy(buf,overflow,i); // 栈溢出
return 0;
}
代码中size变量是无符号短整型,取值范围是0~65535,输入的值大于65535就会发生溢出,最后得到size为4,这样会通过边界检查,但是用memcpy复制数据的时候,使用的是int类型的参数i,这个值是输入的65540,就会发生栈溢出:
基于堆的整数溢出
基于堆的整数溢出的例子:
#include "stdio.h"
#include "windows.h"
int main(int argc, char * argv)
{
int* heap;
unsigned short int size; // 无符号短整数取值范围:0 ~ 65535
char *pheap1, *pheap2;
HANDLE hHeap;
printf("输入size数值:\n");
scanf("%d",&size);
hHeap = HeapCreate(HEAP_GENERATE_EXCEPTIONS, 0x100, 0xfff); //创建一个堆块
if (size <= 0x50)
{
size -= 5; //输入2,size=-3=65533,
printf("size:%d\n",size);
pheap1 = HeapAlloc(hHeap, 0, size); // pheap1会分配过大的堆块,导致溢出!
pheap2 = HeapAlloc(hHeap, 0, 0x50);
}
HeapFree(hHeap, 0, pheap1);
HeapFree(hHeap, 0, pheap2);
return 0;
}
代码中的size是unsigned short int类型,当输入小于5,size减去5会得到负数,但由于unsigned short int取值范围的限制无法识别负数,得到正数65533,最后分配得到过大的堆块,溢出覆盖了后面的堆管理结构:
0x05 格式化字符串漏洞原理
格式化漏洞产生的原因主要是对用户输入的内容没有做过滤,有些输入数据都是作为参数传递给某些执行格式化操作的函数的,比如:printf,fprintf,vprintf,sprintf。
恶意用户可以使用%s和%x等格式符,从堆栈和其他内存位置输出数据,也可以使用格式符%n向任意地址写入数据,配合printf()函数就可以向任意地址写入被格式化的字节数,可能导致任意代码执行,或者读取敏感数据。
以下面的代码为例讲解格式化字符串漏洞原理:
#include <stdio.h>
#include <string.h>
int main (int argc, char *argv[])
{
char buff[1024]; // 设置栈空间
strncpy(buff,argv[1],sizeof(buff)-1);
printf(buff); //触发漏洞
return 0;
}
可以发现当输入数据包含%s和%x格式符的时候,会意外输出其他数据:
用ollydbg附加调试程序,执行前需要先设置命令行参数,调试-参数-命令行:test-%x
在运行程序后,传递给printf的参数只有test-%x,但是他把输入参数test-%x之后的另一个栈上数据当做参数传给了printf函数,因为printf基本类型是:
printf(“格式化控制符”,变量列表);
传递给printf的参数只有一个,但是程序默认将栈上的下一个数据作为参数传递给了printf函数,刚好下一个数据是strcpy()函数的目标地址,就是buff变量,buff刚好指向test-%x的地址0x0019fec4,所以程序会输出0x0019fec4,如果后面再加一个%x就会将src参数的值也输出了,这样就可以遍历整个栈上数据了。
除了利用%x读取栈上数据,还可以用%n写入数据修改返回地址来实现漏洞利用。
0x06 双重释放漏洞原理
Double Free漏洞是由于对同一块内存进行二次释放导致的,利用漏洞可以执行任意代码,编译成release示例代码如下:
#include <stdio.h>
#include "windows.h"
int main (int argc, char *argv[])
{
void *p1,*p2,*p3;
p1 = malloc(100);
printf("Alloc p1:%p\n",p1);
p2 = malloc(100);
printf("Alloc p2:%p\n",p2);
p3 = malloc(100);
printf("Alloc p3:%p\n",p3);
printf("Free p1\n");
free(p1);
printf("Free p3\n");
free(p3);
printf("Free p2\n");
free(p2);
printf("Double Free p2\n"); //二次释放
free(p2);
return 0;
}
在二次释放p2的时候就会发生程序崩溃,但是并不是每次出现Double Free都会发生崩溃,要有堆块合并的动作发生才会发生崩溃
#include <stdio.h>
#include "windows.h"
int main (int argc, char *argv[])
{
void *p1,*p2,*p3;
p1 = malloc(100);
printf("Alloc p1:%p\n",p1);
p2 = malloc(100);
printf("Alloc p2:%p\n",p2);
p3 = malloc(100);
printf("Alloc p3:%p\n",p3);
printf("Free p2\n");
free(p2);
printf("Double Free p2\n");
free(p2);
printf("Free p1\n");
free(p1);
printf("Free p3\n");
free(p3);
return 0;
}
双重释放原理图
在释放过程中,邻近的已经释放的堆块存在合并操作,这会改变原有堆头信息,之后再对其地址引用释放就会发生访问异常。
0x07释放后重引用漏洞原理
通过以下代码理解UAF漏洞原理:
#include <stdio.h>
#define size 32
int main(int argc, char **argv) {
char *buf1;
char *buf2;
buf1 = (char *) malloc(size);
printf("buf1:0x%p\n", buf1);
free(buf1);
// 分配 buf2 去“占坑”buf1 的内存位置
buf2 = (char *) malloc(size);
printf("buf2:0x%p\n\n", buf2);
// 对buf2进行内存清零
memset(buf2, 0, size);
printf("buf2:%d\n", *buf2);
// 重引用已释放的buf1指针,但却导致buf2值被篡改
printf("==== Use After Free ===\n");
strncpy(buf1, "hack", 5);
printf("buf2:%s\n\n", buf2);
free(buf2);
}
buf2 “占坑”了buf1 的内存位置,经过UAF后,buf2被成功篡改了
程序通过分配和buf1大小相同的堆块buf2实现占坑,似的buf2分配到已经释放的buf1内存位置,但由于buf1指针依然有效,并且指向的内存数据是不可预测的,可能被堆管理器回收,也可能被其他数据占用填充,buf1指针称为悬挂指针,借助悬挂指针buf1将内存赋值为hack,导致buf2也被篡改为hack。
如果原有的漏洞程序引用到悬挂指针指向的数据用于执行指令,就会导致任意代码执行。
在通常的浏览器UAF漏洞中,都是某个C++对象被释放后重引用,假设程序存在UAF的漏洞,有个悬挂指针指向test对象,要实现漏洞利用,通过占坑方式覆盖test对象的虚表指针,虚表指针指向虚函数存放地址,现在让其指向恶意构造的shellcode,当程序再次引用到test对象就会导致任意代码执行。
UAF漏洞利用原理图
0x08 数组越界访问漏洞
先区分一下数组越界漏洞和溢出漏洞:数组越界访问包含读写类型,溢出属于数据写入;部分溢出漏洞本质确实就是数组越界漏洞。
数组越界就像是倒水的时候倒错了杯子,溢出就像是水从杯子里溢出来。
下面代码为例分析数组越界访问漏洞:
#include "stdio.h"
int main(){
int index;
int array[3] = {0x11, 0x22, 0x33};
printf("输入数组索引下标:");
scanf("%d", &index);
printf("输出数组元素:array[%d] = 0x%x\n", index, array[index]); //数组越界读操作
//array[index] = 1 ; //数组越界写操作
return 0;
}
执行生成的程序,然后分别输入12345,输出结果如上,当输入的数组下标分别是12的时候,会得到正常数值,但是从索引3开始就超出了原来的数组array的范围,比如输入5,就会数组越界访问array数组,导致读取不在程序控制范围内的数值。
使用ollydbg调试发现array[5]就是从array开始的第六个数据0x4012A9,已经读取到了array之外的数据,如果越界访问距离过大,就会访问到不可访问的内存空间,导致程序崩溃。
0x09类型混淆漏洞原理
类型混淆漏洞(Type Confusion)一般是将数据类型A当做数据类型B来解析引用,这就可能导致非法访问数据从而执行任意代码,比如将Unit转成了String,将类对象转成数据结构。
类型混淆漏洞是现在浏览器漏洞挖掘的主流漏洞,这类漏洞在java,js等弱类型语言中非常常见。
下面的代码,A类被混淆成B类,就可能导致私有域被外部访问到:
class A {
private int value;
};
class B {
public int value;
};
B attack = AcastB(var); //将A类型混淆转成B类型
attack.value = 1; //导致可以访问私有域
以IE/Edge类型混淆漏洞(CVE-2017-0037)为例讲解,漏洞原因是函数处理时,没有对对象类型进行严格检查,导致类型混淆。
PoC如下:在PoC中定义了一个table,标签中定义了表id为th1,在boom()中引用,然后是setInterval设定事件。
运行PoC,用Windbg附加并加载运行出现崩溃
从崩溃点可以看到eax作为指针,引用了一个无效地址,导致崩溃,而上一条指令是一个call,这个无效的返回值来自这个call,在这个call处下断点,ecx作为参数,存放的对象是一个Layout::FlowItem::`vftable虚表
这里读取虚表中+4的值,为0时this指针赋值v1,随后v1+16后返回,因此,Layout::FlowItem::`vftable所属指针的这个情况是正常的,函数会正常返回进入后续处理逻辑
让程序继续运行,会再次调用该函数,此时ecx并不是一个虚表对象,而是一个int Array对象,这里我们可以通过条件断点来跟踪两个对象的创建过程,重点关注两个对象创建的函数,一个是FlowItem::`vftable对应的虚表对象,另一个是引发崩溃的int Array对象。这两个函数的返回值,也就是eax寄存器中存放的指向这两个创建对象的指针。
通过跟踪可以看到第一次调用Readable函数时ecx是一个正常的FlowItem对象,而第二次调用的时候ecx是一个int Array Object。Layout::Patchable >::Readable函数是处理虚表对象的函数,由于boom()函数中引用th1.align导致Readable函数得到第二次引用,由于没有进行对象属性检查,导致第二次调用时将table对象传入,最终发生类型混淆崩溃。
0x10竞争条件漏洞原理
竞争条件(Race Condition)是由于多个线程/对象/进程同时操作同一资源,导致系统执行违背原有逻辑设定的行为,这类漏洞在linux,内核层面非常多见,在windows和web层面也存在。
互斥锁的出现就是为了解决此类漏洞问题,保证某一对象在特定资源访问时,其他对象不能操作此资源。
竞争条件”发生在多个线程同时访问同一个共享代码、变量、文件等没有进行锁操作或者同步操作的场景中。 ——Wikipedia-computer_science
比如如下代码:
#-*-coding:utf-8-*-
import threading
COUNT = 0
def Run(threads_name):
global COUNT
read_value = COUNT
print "COUNT in Thread-%s is %d" % (str(threads_name), read_value)
COUNT = read_value + 1
def main():
threads = []
for j in range(10):
t = threading.Thread(target=Run,args=(j,))
threads.append(t)
t.start()
for i in range(len(threads)):
threads[i].join()
print("Finally, The COUNT is %d" % (COUNT,))
if __name__ == '__main__':
main()
执行结果如下:
按照我们的预想,结果应该都是10,但是发现结果可能存在非预期解,原因就在于我们没有对变量COUNT做同步制约,导致可能Thread-7在读COUNT,还没来得及更改COUNT,Thread-8抢夺资源,也来读COUNT,并且将COUNT修改为它读的结果+1,由此出现非预期。