谜题:打造极小ELF文件输出文件(在Linux环境中精简ELF64文件)
接前文《谜题:打造极小ELF文件输出文件(使用汇编语言通过系统调用来实现)》
在完成了一个232字节
的程序后,发现距离186字节
的目标还是有一些距离。接下来就要深入研究ELF文件的细节了。
[root@i-a77ugr2f tmp]# readelf -h open
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x4000b0
Start of program headers: 64 (bytes into file)
Start of section headers: 0 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 2
Size of section headers: 64 (bytes)
Number of section headers: 0
Section header string table index: 0
先通过readelf
命令来看一下程序的ELF文件头信息,这里输出的信息都是从ELF文件头中提取得到的。
需要重点关注一下ELF文件头(64字节)和程序头(56*2=112字节)的大小。在64位ELF文件的定义上,文件头和程序头的大小是有明确定义的。
我们可以计算出实际机器指令的大小:总大小-文件头-程序头=232-64-112=56字节。而这56字节的指令我们前面已经通过各种技巧进行了最简化。
那么,接下里的思路只有两个:
- 想办法将代码段中的机器指令填充到ELF头和程序头中
- 想办法将程序头与文件头进行合并
如上图所示,这是前面得到的232字节的ELF可执行文件的16进制内容,并对文件头和程序头中的字段进行了标注。(备注参考资料)
通过查阅相关资料,我们尝试判断并验证哪些字段是无用的。可以通过下面的cutelf.sh
脚本,在使用0xff
覆盖一些字节后,观察程序是否仍能正常运行。
cutelf.sh
#!/bin/sh
test_byte_ff() {
# modify Ehdr
seek_lst=({4..15} {20..23} {40..53} {58..63})
for sk in ${seek_lst[@]}; do
echo -ne "\xff" | dd of=open bs=1 count=1 seek=${sk} conv=notrunc > /dev/null 2>&1
done
# modify Phdr
seek_lst=({88..95} {112..119} {144..151} {168..175})
for sk in ${seek_lst[@]}; do
echo -ne "\xff" | dd of=open bs=1 count=1 seek=${sk} conv=notrunc > /dev/null 2>&1
done
}
test_byte_ff
经过一番努力,我们知道了在文件头和程序头中哪些字节可以被篡改,且不影响程序功能。然后按照思路一,尝试将代码段中的机器指令填充到ELF文件头和程序头中。
由于ELF文件头和程序头中可以被篡改的字节不是连续的,可以使用汇编指令jmp
对应的机器指令0xeb
来进行跳转。0xeb
后面可以跟一个字节,表示向后跳转的字节数。
[root@i-a77ugr2f tmp]# objdump -d open.o
open.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <_start>:
0: b0 02 mov $0x2,%al # 1
2: 48 8b 7c 24 10 mov 0x10(%rsp),%rdi # 2
7: 0f 05 syscall # 3
9: 48 89 c7 mov %rax,%rdi # 4
c: 48 31 c0 xor %rax,%rax # 5
f: be 00 00 00 00 mov $0x0,%esi # 6
14: 66 ba ff ff mov $0xffff,%dx # 7
18: 0f 05 syscall # 8
1a: 48 89 c2 mov %rax,%rdx # 9
1d: 48 31 c0 xor %rax,%rax # 10
20: b0 01 mov $0x1,%al # 11
22: 48 31 ff xor %rdi,%rdi # 12
25: 40 b7 01 mov $0x1,%dil # 13
28: 0f 05 syscall # 14
2a: 48 31 c0 xor %rax,%rax # 15
2d: b0 03 mov $0x3,%al # 16
2f: 0f 05 syscall # 17
31: b0 3c mov $0x3c,%al # 18
33: 0f 05 syscall # 19
我们通过objdump
命令可以从open.o
文件中粗略查看,ELF文件的代码段中机器指令对应的汇编指令。
再配合jmp
的跳转方案,就可以试着将代码段中的部分机器指令填充到ELF文件头和程序头中了。
cutelf.sh
#!/bin/sh
mv_bytes() {
# 覆盖 e_ident[7~13],将0xb0(176)开始的7个字节(第1、2条指令),复制到0x07(7)开始的位置
dd if=open of=open bs=1 skip=176 count=7 seek=7 conv=notrunc
# 覆盖 e_ident[14~15],将0x0e(14)开始的两个字节覆盖为(eb 04),表示从下一个字节算起,向后跳转0x04(4)个字节
echo -ne "\xeb\x04" | dd of=open bs=1 count=2 seek=14 conv=notrunc
# 覆盖 e_version[0~1],将0xb7(183)开始的2个字节(第3条指令),复制到0x14(20)开始的位置
dd if=open of=open bs=1 skip=183 count=2 seek=20 conv=notrunc
# 覆盖 e_version[2~3],将0x16(22)开始的两个字节覆盖为(eb 10),表示向后跳转0x10(16)个字节
echo -ne "\xeb\x10" | dd of=open bs=1 count=2 seek=22 conv=notrunc
# 覆盖 e_shoff[0-7], e_flags[0-2],将0xb9(185)开始的11个字节(第4、5、6条指令),复制到0x28(40)开始的位置
dd if=open of=open bs=1 skip=185 count=11 seek=40 conv=notrunc
# 覆盖 e_flags[3], e_ehsize[0],将0x33(51)开始的两个字节覆盖为(eb 05),表示向后跳转0x05(5)个字节
echo -ne "\xeb\x05" | dd of=open bs=1 count=2 seek=51 conv=notrunc
# 覆盖 e_shentsize, e_shnum,将0xc4(196)开始的4个字节(第7条指令),复制到0x3a(58)开始的位置
dd if=open of=open bs=1 skip=196 count=4 seek=58 conv=notrunc
# 覆盖 e_shstrndx,将0x3e(62)开始的两个字节覆盖为(eb 18),表示向后跳转0x18(24)个字节
echo -ne "\xeb\x18" | dd of=open bs=1 count=2 seek=62 conv=notrunc
# 覆盖 p_paddr[0~4],将0xc8(200)开始的5个字节(第8、9条指令),复制到0x58(88)开始的位置
dd if=open of=open bs=1 skip=200 count=5 seek=88 conv=notrunc
# 覆盖 p_paddr[5~6],将0x5d(93)开始的两个字节覆盖为(eb 11),表示向后跳转0x11(17)个字节
echo -ne "\xeb\x11" | dd of=open bs=1 count=2 seek=93 conv=notrunc
# 覆盖 p_align[0~4],将0xcd(205)开始的5个字节(第10、11条指令),复制到0x70(112)开始的位置
dd if=open of=open bs=1 skip=205 count=5 seek=112 conv=notrunc
# 覆盖 p_align[5~6],将0x75(117)开始的两个字节覆盖为(eb 19),表示向后跳转0x19(25)个字节
echo -ne "\xeb\x19" | dd of=open bs=1 count=2 seek=117 conv=notrunc
# 覆盖 p_paddr[0~5],将0xd2(210)开始的6个字节(第12、13条指令),复制到0x90(144)开始的位置
dd if=open of=open bs=1 skip=210 count=6 seek=144 conv=notrunc
# 覆盖 p_paddr[6~7],将0x96(150)开始的两个字节覆盖为(eb 10),表示向后跳转0x10(16)个字节
echo -ne "\xeb\x10" | dd of=open bs=1 count=2 seek=150 conv=notrunc
# 覆盖 p_align[0~7] 和后面的代码段,将0xd8(216)开始的13个字节(第14-19条指令),复制到0xa8(168)开始的位置
dd if=open of=open bs=1 skip=216 count=48 seek=168 conv=notrunc
# 从0xb5(181)开始截去之后的字节
dd if=open of=open bs=1 count=1 skip=181 seek=181
# 覆盖 e_entry[0~1],调整程序入口地址 0x4000b0->0x400007,将0x18(24)的字节修改为0x07
echo -ne "\x07" | dd of=open bs=1 count=1 seek=24 conv=notrunc
}
mv_bytes > /dev/null 2>&1
以上代码建议就着上文的图片一起看,再配合注释更容易理解。
[root@i-a77ugr2f tmp]# bash cutelf.sh
[root@i-a77ugr2f tmp]# ./open /etc/passwd
# 可以正常输出
[root@i-a77ugr2f tmp]# ll open
-rwxr-xr-x 1 root root 181 Oct 31 00:15 open
执行cutelf.sh
脚本后,就获得了仅181字节
的极小ELF文件,符合谜题要求。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步