謎題:打造極小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文件,符合謎題要求。

浙公網安備 33010602011771號