本节我们看看如何针对ELF可执行文件实现代码注入,这是一个简单的实例,但却可以有效的揭开冰山的一角。
我想很多人都用过破解软件,破解的一种机制就是通过修改程序的二进制代码,越过软件的授权认证机制,例如下面这个例子:
auth = autoVerifyCode(code); //认证输入验证码是否正确
if (auth == False) {
exit(1); //验证错误直接退出
}
//下面代码是软件功能的正常运行
面对上面验证码的验证模式,一种破解办法就是将auth的值直接修改成真,从而让代码运行时直接跳过if(auth == False) 这条语句,我们本节看看如何实现这样的功能。首先我们先编写一段代码,然后将其编译成可执行的ELF文件:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<stdarg.h>
void error_exit(char const *err) {
printf("error msg:%s", err);
exit(1);
}
int main(int argc, char *argv[]) {
FILE *f;
char *infile, *outfile;
unsigned char *key, *buf;
size_t char *key, *buf;
if (argc != 4) {
error_exit("usage: input_file, output_file, key");
}
infile = argv[1];
outfile = argv[2];
f = fopen(unsigned char*)argv[3];
if (!f) {
error_exit("failed to open file");
}
fseek(f, 0, SEEK_END);
n = ftell(f);
fseek(f, 0, SEEK_SET);
buf = malloc(n);
if (!buf) {
error_exit("out of memory");
}
if (fread(buf, 1, n, f) != n) {
error_exit("fail to read file");
}
fclose(f);
j = 0;
for (i = 0; i < n - 1; i++) { //这里是问题点,应该是i<n,这种情况叫做off-one-byte-error
buf[i] ^= key[j];
j = (j+1) % strlen(key);
}
f = fopen(outfile, "wb");
if (!f) {
error_exit("fail to open file");
}
if (fwrite(buf, 1, n, f) != n) {
error_exit("Failed to write file");
}
fclose(f);
return 0;
}
上面代码的基本逻辑是,程序接收一个输入文件用于加密,然后指定加密后的输出文件,最后指定加密用的密钥。加密算法非常简单,就是用密钥中的字符与输入文件中每个字符做xor操作。但是在代码中存在一个典型的one-byte-off 错误,也就是for(i = 0; i < n-1; i++)其中的i<n-1应该是i<n,这个错误会使得被加密文件的最后一个字符没有被加密,我们看一个例子。
在本地创建一个文件: vim encrypted.txt,然后在里面输入字符串“hello world”,接着查看其十六进制内容:
xxd encrypted.txt
上面代码执行后所得结果如下:
我们看到文件的最后一个字节是0x0a,然后我们将前面的代码编译,将编译结果用于加密上面的文件,用于编译的Makefile文件内容如下:
CC=gc
AS=nasm
OBJ=xor_encrypt
.PHONY: all clean
all: $(OBJ)
xor_enrypt: xor_encrypt.c
$(CC) -o xor_encrypt -O2 xor_encrypt.c
clean:
rm -rf $(OBJ)
注意上面脚本中前面有”空格“的行,对应”空格“其实是TAB键。完成上面脚本后,使用make命令就能编译出ELF可执行文件,然后使用如下命令对文本进行加密:
./xor_encrypt encrypted.txt encrypted foobar
其中encrypted 是要输出的加密文件, foobar就是用于加密的密钥,执行后我们可以看到encrypted文件在本地目录,接下来我们继续使用xxd查看加密文件的16进制内容:
xxd encrypted
上面命令执行后所得结果如下:
注意看上图,加密后文本内所有字符都变了,唯独最后一个字符没变,这是因为前面提到的one-byte-off错误引起。假设我们现在没有源码,要想改正这个错误,我们唯一能做的就是直接修改编译出来的二进制文件。
由于bug的原因是语句i < n - 1,如果它能改成i <= n-1,那么问题就可以纠正。假设在没有源码的情况下,我们必须直接修改编译好的二进制文件,我们先反汇编以便找到需要修改的地方:
objdum -M intel -d xor_encrypted
上面命令运行后会得到一大堆反汇编指令,从头往下拉你会看到如下内容:
上图中选中的那段就对应for循环的内容。cmp r12, rbx 对应i < n-1,也就是n-1的数值存放在寄存器r12, i变量的值存放在寄存器rbx。其中指令jne表示不相等就跳转,也就是先将r12的值与寄存器rbx的值比较,如果两者不相等就跳转进入for对应的循环体。
由于代码的错误是循环执行少了一次,正确的逻辑是如果i的值与n-1相等时也要跳转,因此我们需要修改jne,让代码的逻辑变成不相等时跳转,转换为大于等于时都跳转,也就是把语句i < n-1变成i <= n-1,对应的就是当n-1 “大于等于”i的值时就要跳转到循环体。由于n-1对应寄存器r12,因此我们需要将指令jne转换成jae,这条语句的意思是”大于等于时跳转“,我们注意看,jne指令对应的字节码是75 ,d9对应当前所在位置的偏移,因此我们需要在编译出来的二进制文件中找到数值75 d9,这里我们使用文件hexedit:
hexedit xor_encrypted
在打开的内容后按下”/“按钮,也就是有斜杆对应那个按钮,然后输入要查找的数值字符串:
按下回车我们就跳转到对应指令出,由于75对应指令jne, 73对应指令jae因此,我们把跳转到的数值75直接修改成73,然后按ctrl+x,在弹出的Save changes中输入Y,这样修改就完成了,我们再次使用反汇编看看修改后的结果:
可以看到jne指令成功的被修改成jae指令,最后我们再次运行该程序看看结果:
./xor_encrypt encryptd.txt encrypted foobar
xxd encrypted
两个命令输入后可以得到如下结果:
可以看到,最后一个字符被成功加密了,这就是一个简单的代码注入ELF可执行文件的例子。