Linux_64与Linux_86的区别
linux_64
与linux_86
的区别主要有两点:首先是内存地址的范围由32位变成了64位。但是可以使用的内存地址不能大于0x00007fffffffffff
,否则会抛出异常。其次是函数参数的传递方式发生了改变,x86中参数都是保存在栈上,但在x64中的前六个参数依次保存在RDI,RSI,RDX,RCX,R8和 R9
中,如果还有更多的参数的话才会保存在栈上。
例子:
1 | // level3 |
开启ALSR并编译:
1 | gcc -fno-stack-protector level3.c -o level3 |
对于这个程序,只需要构造溢出让rip指向callsystem()
就可以获取shell。因为程序本身在内存中的地址不是随机的,所以不用担心函数地址发生改变。接下来就是要找溢出点了。
键入:pattern create 150
生成字符串,然后输入使程序崩溃
PC指针并没有指向类似于0x41414141
那样地址,而是停在了vulnerable_function()
函数中。因为程序使用的内存地址不能大于0x00007fffffffffff
,否则会抛出异常。但是,虽然PC不能跳转到那个地址,我们依然可以通过栈来计算出溢出点。因为ret相当于pop rip
指令,所以我们只要看一下栈顶的数值就能知道PC跳转的地址了。
在GDB里,x是查看内存的指令,随后的gx代表数值用64位16进制显示。因为使用了peda,所以也可以直接通过栈顶的字符串计算offset
得到溢出偏移为136字节,构造payload,跳转一个小于0x00007fffffffffff
的地址,看看这次能否控制pc的指针。
可以发现已经成功控制PC的指针了,构造exp如下:
1 | #!/usr/bin/python |
使用工具寻找gadgets
在x64中前六个参数依次保存在RDI,RSI,RDX,RCX,R8和 R9
寄存器里,如果还有更多的参数的话才会保存在栈上。所以我们需要寻找一些类似于pop rdi; ret
的这种gadget
来进行传参。如果是简单的gadgets
,我们可以通过objdump
来查找。但当我们打算寻找一些复杂的gadgets的时候,还是借助于一些查找gadgets的工具比较方便。
ROPEME、Ropper、ROPgadget、rp++
这些工具功能上都相似。
例子:
1 |
|
编译:
1 | gcc -fno-stack-protector level4.c -o level4 -ldl |
首先目标程序会打印system()
在内存中的地址,这样的话就不需要我们考虑ASLR的问题了,只需要想办法触发buffer overflow
然后利用ROP执行system(“/bin/sh”)
。但为了调用system(“/bin/sh”)
,我们需要找到一个gadget
将rdi
的值指向/bin/sh
的地址。于是我们使用ROPGadget
搜索一下level4
中所有pop ret
的gadgets
。
键入命令:ROPgadget --binary level4 --only "pop|ret"
如果没有理想的结果(没有找到想要的gadget),我们可以寻找libc.so
中的gadgets
。因为程序本身会load libc.so
到内存中并且会打印system()
的地址。所以当我们找到gadgets
后可以通过system()
计算出偏移量后调用对应的gadgets
。
键入命令:ROPgadget --binary libc.so.6 --only "pop|ret" | grep "rdi"
构造ROP链:
1 | payload = "\x00"*136 + p64(pop_ret_addr) + p64(binsh_addr) + p64(system_addr) |
另外,因为我们只需调用一次system()函数就可以获取shell,所以我们也可以搜索不带ret的gadgets来构造ROP链
键入命令:ROPgadget --binary libc.so.6 --only "pop|call" | grep rdi
通过搜索结果我们发现,0x00000000000f4739 : pop rax ; pop rdi ; call rax
也可以完成我们的目标。首先将ra
x赋值为system()
的地址,rdi
赋值为/bin/sh
的地址,最后再调用call rax
即可。
1 | payload = "\x00"*136 + p64(pop_pop_call_addr) + p64(system_addr) + p64(binsh_addr) |
最终exp:
1 | #!/usr/bin/python |
通用gadgets
因为程序在编译过程中会加入一些通用函数用来进行初始化操作(比如加载libc.so的初始化函数),所以虽然很多程序的源码不同,但是初始化的过程是相同的,因此针对这些初始化函数,我们可以提取一些通用的gadgets加以使用,从而达到我们想要达到的效果。
例子:
1 | // level5 |
编译:
1 | gcc -fno-stack-protector -o level5 level5.c |
可以看到这个程序仅仅只有一个buffer overflow
,也没有任何的辅助函数可以使用,所以我们要先想办法泄露内存信息,找到system()
的地址,然后再传递/bin/sh
到.bss
段, 最后调用system("/bin/sh")
。因为原程序使用了write()
和read()
函数,我们可以通过write()
去输出write.got
的地址,从而计算出libc.so
在内存中的地址。但问题在于write()
的参数应该如何传递,因为x64下前6个参数不是保存在栈中,而是通过寄存器传值。当我们使用ROPgadget
并没有找到类似于pop rdi, ret,pop rsi, re
t这样的gadgets
时。就可以考虑在x64下一些万能的gadgets
。比如说我们用objdump -d ./level5
观察一下__libc_csu_init()
这个函数。一般来说,只要程序调用了libc.so
,程序都会有这个函数用来对libc进行初始化操作。
我们可以看到利用0x40061a
处的代码我们可以控制rbx,rbp,r12,r13,r14和r15
的值,随后利用0x400600
处的代码我们将r13
的值赋值给rdx
, r14
的值赋值给rsi
,r15
的值赋值给edi
,随后就会调用call qword ptr [r12+rbx8]
。这时候我们只要再将rbx
的值赋值为0,再通过精心构造栈上的数据,我们就可以控制pc去调用我们想要调用的函数了(比如说write函数)。执行完call qword ptr [r12+rbx8]
之后,程序会对rbx+=1
,然后对比rbp
和rbx
的值,如果相等就会继续向下执行并ret到我们想要继续执行的地址。所以为了让rbp
和rbx
的值相等,我们可以将rbp
的值设置为1,因为之前已经将rbx
的值设置为0了。大概思路就是这样,我们下来构造ROP链。
我们先构造payload1
,利用write()
输出write
在内存中的地址。注意我们的gadget
是call qword ptr [r12+rbx*8]
,所以我们应该使用write.got
的地址而不是write.plt
的地址。(这里是直接执行函数,所以需要调用真实的函数体,即got表中的write),并且为了返回到原程序中,重复利用buffer overflow
的漏洞,我们需要继续覆盖栈上的数据,直到把返回值覆盖成目标函数的main
函数为止。
1 | # rdi= edi = r15, rsi = r14, rdx = r13 |
当我们exp在收到write()
在内存中的地址后,就可以计算出system()
在内存中的地址了。接着我们构造payload2
,利用read()
将“/bin/sh”读入到.bss
段内存中。
1 | #rdi= edi = r15, rsi = r14, rdx = r13 |
最后我们构造payload3
,调用system()
函数执行“/bin/sh”。
1 | # rdi = bss_addr |
最终的exp:
1 | #!/bin/usr/python |
要注意的是,当我们把程序的io重定向到socket
上的时候,根据网络协议,因为发送的数据包过大,read()
有时会截断payload
,造成payload
传输不完整造成攻击失败。这时候要多试几次即可成功。如果进行远程攻击的话,需要保证ping值足够小才行(局域网)。
You are welcome to share this blog, so that more people can participate in it. If the images used in the blog infringe your copyright, please contact the author to delete them.