2020年的时候,出现过两道构造最小ELF文件的题目:
前者要求构造一个共享库文件,通过 LD_PRELOAD=<upload> /bin/true
指定该文件以执行。(第一关要求大小<300字节,第二关要求大小<196字节)
后者要求构造一个可执行程序,执行后输出该ELF文件的MD5值。
两个题都很有意思,但出于了解如何构造最小ELF的目的,选择了相对简单点的第一个题 - golf.so,作为本文的目标。
本文按如下步骤,最终构造出一个170字节的满足golf.so题目要求的ELF文件:
- 创建一个ELF壳:利用已有汇编模板构造一个ELF可执行程序
- 创建一个动态链接库文件:动态链接库文件比一般可执行文件多了一条类型为
PT_DYNAMIC
的Program Header
- 缩减至192字节:将shellcode并入各个 Header 中
- 缩减至170字节:将_dynamic段并入 Header ,并缩减ELF头大小(e_ehsize),及利用DT_INIT
创建一个ELF壳
使用gcc直接编译生成的可执行程序的文件大小较大,因为含有许多不必须的字段。所以比较好的方法是,在了解了ELF文件头各个字段的含义后,使用汇编或其他方法来构造一个ELF文件。
参考 Tiny ELF 32/64 with nasm 中展示的32位和64位最小ELF的汇编代码,照猫画虎,创建一个弹shell的64位ELF可执行程序的壳:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| ; nasm -f bin -o tiny64 tiny64.asm BITS 64 org 0x400000
ehdr: ; Elf64_Ehdr db 0x7f, "ELF", 2, 1, 1, 0 ; e_ident times 8 db 0 dw 2 ; e_type dw 0x3e ; e_machine dd 1 ; e_version dq _start ; e_entry dq phdr - $$ ; e_phoff dq 0 ; e_shoff dd 0 ; e_flags dw ehdrsize ; e_ehsize dw phdrsize ; e_phentsize dw 1 ; e_phnum dw 0 ; e_shentsize dw 0 ; e_shnum dw 0 ; e_shstrndx ehdrsize equ $ - ehdr
phdr: ; Elf64_Phdr dd 1 ; p_type dd 5 ; p_flags dq 0 ; p_offset dq $$ ; p_vaddr dq $$ ; p_paddr dq filesize ; p_filesz dq filesize ; p_memsz dq 0x1000 ; p_align phdrsize equ $ - phdr
_start: xor rdx, rdx mov rdi, "/bin/sh" push rdi mov rdi, rsp push rdx push rdi mov rsi,rsp mov rax, 59 syscall ; execve("/bin/sh",["/bin/sh"],...)
filesize equ $ - $$
|
执行结果:
1 2 3 4 5 6
| $ nasm -f bin -o tiny64 test.s $ chmod u+x tiny64 $ ./tiny64 $ exit $ wc -c tiny64 149 tiny64
|
成功执行了,但是当前编译的结果是一个可执行程序,而golf.so题目的要求是通过 LD_PRELOAD
指定的一个动态链接库文件。于是下一步,我们需要在Program Header中构造一个 .dynamic 节。
创建一个动态链接ELF
Dynamic section中详细介绍了动态链接文件的构造方法,跟一般可执行文件的主要区别在于:
- 需要构造一个类型为 PT_DYNAMIC 的Program Header
- 需要以 _dynamic 为符号构造一个段,这个段用于存放Dynamic table entry
- 构造Dynamic table entry中需要的其他数据,如_hash段
通过Dynamic section中表格得知,对于shared object(动态链接库文件),必须具备的六个Dynamic table entry分别是:DT_NULL, DT_HASH, DT_STRTAB, DT_SYMTAB, DT_STRSZ, DT_SYMENT
。于是参考其他ELF文件中的.dynamic节和_DYNAMIC,构造了如下ELF的汇编:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
| BITS 64 org 0x0
ehdr: ; Elf64_Ehdr db 0x7f, "ELF", 2, 1, 1, 0 ; e_ident times 8 db 0 dw 3 ; e_type : ET_DYN dw 0x3e ; e_machine dd 1 ; e_version dq _start ; e_entry dq 0x40 ; e_phoff dq 0 ; e_shoff dd 0 ; e_flags dw 0x40 ; e_ehsize dw 0x38 ; e_phentsize dw 2 ; e_phnum dw 0 ; e_shentsize ; dw 0 ; e_shnum ; dw 0 ; e_shstrnd ;
phdr_loadable: ; Elf64_Phdr dd 1 ; p_type dd 7 ; p_flags dq 0 ; p_offset dq $$ ; p_vaddr dq $$ ; p_paddr dq loadable_size ; p_filesz dq loadable_size ; p_memsz dq 0x1000 ; p_align
phdr_dynamic: ; Elf64_Phdr dd 2 ; p_type dd 7 ; p_flags dq loadable_size ; p_offset dq _dynamic ; p_vaddr dq _dynamic ; p_paddr dq dynamic_size ; p_filesz dq dynamic_size ; p_memsz dq 0x8 ; p_align
_start: xor rdx, rdx mov rdi, "/bin/sh" push rdi mov rdi, rsp push rdx push rdi mov rsi,rsp mov rax, 59 syscall ;execve("/bin/sh",["/bin/sh"],...)
loadable_size equ $ - ehdr
_dynamic: dq 6FFFFEF5h,_hash ;DT_HASH dq 5, 0 ;DT_STRTAB dq 6, 0 ;DT_SYMTAB dq 0Ah, 0 ;DT_STRSZ dq 0Bh, 0 ;DT_SYMENT dq 0Ch, _start ;DT_INIT dq 0,0 ;DT_NULL
dynamic_size equ $ - _dynamic
_hash: dd 1 dd 1 dd 1 dd 0 dq 0 dd 0 dd 0
|
编译命令及结果如下:
1 2 3 4 5
| $ nasm -f bin -o test test.s $ LD_PRELOAD=./test /bin/true $ exit $ wc -c test 349 test
|
此时生成的动态链接库文件大小为349,离第一关的目标300还差一些。于是考虑是不是还有可以删除的部分。
经过测试,将DT_HASH、DT_STRTAB、DT_STRSZ、DT_SYMENT、DT_NULL删除后,该动态库也能正常工作。删掉后,得到满足第一关的ELF:
1 2 3 4 5
| $ nasm -f bin -o test test.s $ LD_PRELOAD=./test /bin/true $ exit $ wc -c test 237 test
|
继续精简 - 192字节版本
通过编辑覆盖 ELF Header 和 Program Header 中的内容,确认不会影响ELF执行的部分并标记。然后将shellcode拆散,分别布置到 Header 中标记的各个位置。各段 shellcode 通过 jmp 指令来回跳转,最终完成弹 shell 的功能。
将 shellcode 拆散后,放入各个 Header 中,对应的汇编代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
| BITS 64 org 0x0
ehdr: ; Elf64_Ehdr db 0x7f, "ELF", 2, 1, 1, 0 ; e_ident times 8 db 0 dw 3 ; e_type : ET_DYN dw 0x3e ; e_machine dd 1 ; e_version dq _start ; e_entry dq 0x40 ; e_phoff ;dq 0xffffeeee ; e_shoff ;dd 0xffffeeee ; e_flags _start: mov rdi, "/bin/sh" jmp gadget1 dw 0x40 ; e_ehsize dw 0x38 ; e_phentsize dw 2 ; e_phnum ;dw 0xffff ; e_shentsize ; ;dw 0xffff ; e_shnum ; ;dw ; e_shstrnd ; gadget1: push rdi mov rdi, rsp jmp gadget2
phdr_loadable: ; Elf64_Phdr dd 1 ; p_type dd 7 ; p_flags dq 0 ; p_offset dq $$ ; p_vaddr ;dq $$ ; p_paddr ; gadget2: xor rdx, rdx push rdx push rdi jmp gadget3 db 0x0 dq loadable_size ; p_filesz dq loadable_size ; p_memsz ; ;dq 0x1000 ; p_align ; gadget3: mov rsi,rsp jmp gadget4 db 0, 0, 0
phdr_dynamic: ; Elf64_Phdr dd 2 ; p_type dd 7 ; p_flags dq loadable_size ; p_offset dq _dynamic ; p_vaddr ;dq 0 ; p_paddr gadget4: mov rax, 59 syscall db 0 dq dynamic_size ; p_filesz ;dq 0 ; p_memsz ; ;dq 0xffffeeee ; p_align ;
loadable_size equ $ - ehdr
_dynamic: dq 0Ch, _start ;DT_INIT dq 6, 0 ;DT_SYMTAB
dynamic_size equ $ - _dynamic
|
编译运行,得到192字节版本的ELF:
1 2 3 4 5 6 7 8
| ➜ nasm -o test test.s ➜ chmod +x ./test ➜ LD_PRELOAD=./test /bin/true $ ls test test.s $ exit ➜ wc -c test 192 test
|
再度精简 - 170字节版本
继续缩减工作,主要从以下3各方面考虑:
- 覆写ELF Header中的e_entry:在 _dynamic 段存在时,可通过DT_INIT指定程序入口地址,所以ELF Header中的e_entry可以被覆盖为任意值
- 缩小ELF Header大小:通过删除ELF header中的e_shentsize 、e_shnum 、e_shstrnd 三个字段,ELF Header(e_ehsize)的大小可以缩小6字节
- 将_dynamic融入到phdr_dynamic中(刘大爷的ELF文件中看到的小trick)
按照以上思路精简后,ELF文件对应的汇编代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| BITS 64 org 0x0
ehdr: ; Elf64_Ehdr db 0x7f, "ELF", 2, 1, 1, 0 ; e_ident times 8 db 0 dw 3 ; e_type : ET_DYN dw 0x3e ; e_machine dd 1 ; e_version ;dq _start ; e_entry gadget1: push rdi mov rdi, rsp jmp gadget2 db 0, 0 dq ehdrsize ; e_phoff ;dq 0xffffeeee ; e_shoff ;dd 0xffffeeee ; e_flags _start: mov rdi, "/bin/sh" jmp gadget1 dw ehdrsize ; e_ehsize dw 0x38 ; e_phentsize dw 2 ; e_phnum ;dw 0xffff ; e_shentsize ;dw 0xffff ; e_shnum ;dw ; e_shstrnd
ehdrsize equ $ - ehdr
phdr_loadable: ; Elf64_Phdr dd 1 ; p_type dd 7 ; p_flags dq 0 ; p_offset dq $$ ; p_vaddr ;dq $$ ; p_paddr gadget2: xor rdx, rdx push rdx push rdi jmp gadget3 db 0x0 dq loadable_size ; p_filesz dq loadable_size ; p_memsz ;dq 0x1000 ; p_align gadget3: mov rsi,rsp jmp gadget4 db 0, 0, 0
phdr_dynamic: ; Elf64_Phdr dd 2 ; p_type dd 7 ; p_flags _dynamic: dq 0x6 ; p_offset dq _dynamic ; p_vaddr dq 0xc ; p_paddr dq _start ; p_filesz dq 0 ; p_memsz ;dq 0x8 ; p_align gadget4: mov rax, 59 syscall db 0 loadable_size equ $ - ehdr
|
编译运行,得到170字节版本的ELF:
1 2 3 4 5 6 7 8
| ➜ nasm -o test test.s ➜ chmod +x ./test ➜ LD_PRELOAD=./test /bin/true $ ls test test.s $ exit ➜ wc -c test 170 test
|
更小的?- 136字节
在不断缩减文件大小的过程中,了解ELF文件格式的目的已达成。而这个136字节有点trick,就没继续做了。
参考golf.so最小解:**golf.so (Plaid CTF 2020) - The race to the smallest .so**
根据作者的表述,该题最小的elf应该是:24(elf header中不可改的部分)+2*56(PT_LOAD和PT_DYNAMIC两个program header的大小)= 136字节。tql,以后有空再玩~
了解ELF文件格式
ELF-64 Object File Format 中详细讲述了ELF文件中各个部分的定义,一个ELF文件主要分为4个部分:
- ELF header:根基,用于定位其他几个部分
- Program header table:运行视图
- content:代码或数据
- Section header table:链接视图
我们需要记住的就是三个header各自的作用及互相之间的关系,并粗略了解每个header中的内容。
作用:可以将 ELF Header 看作 ELF 格式的核心,Program header table 用于指导将文件加载到内存时如何布局的,Section header table是用于链接过程时寻找定位的。
关系:三者之间的关系,可以简单地理解为,通过 ELF Header 能定位到 Program header table 和 Section header table 在文件中的位置,以及 Program/Section header table中各有几项内容。
内容:每个 header table 项中的内容则不尽相同,在需要时视具体情况具体分析。以本文中golf.so为例,对于Section header table,由于构造的ELF不需要链接到其他文件,所以可以将其完全删除。而在Program header table中,为了构造动态链接库文件,需要包含 PT_LOAD 和 PT_DYNAMIC 两项内容。
三个header对应的结构体内容,最好能大体上熟记于心。
ELF Header对应的结构体如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| typedef struct { unsigned char e_ident[16]; Elf64_Half e_type; Elf64_Half e_machine; Elf64_Word e_version;
Elf64_Addr e_entry; Elf64_Off e_phoff; Elf64_Off e_shoff;
Elf64_Word e_flags; Elf64_Half e_ehsize;
Elf64_Half e_phentsize; Elf64_Half e_phnum;
Elf64_Half e_shentsize; Elf64_Half e_shnum; Elf64_Half e_shstrndx; } Elf64_Ehdr;
|
比较重要的几个点:
- e_entry - 指明程序加载到内存后,从哪个地址开始执行
- e_phoff - program header在文件中的偏移
- e_phentsize - 每个program header table的大小
- e_phnum - 在program header中一共有多少个program header table
program header table 由多个 Program Header 结构体组成,加载ELF文件时会根据这些 header 内容布置进程虚拟内存空间。一个 Program Header 对应的结构体如下:
1 2 3 4 5 6 7 8 9 10
| typedef struct{ Elf64_Word p_type; Elf64_Word p_flags; Elf64_Off p_offset; Elf64_Addr p_vaddr; Elf64_Addr p_paddr; Elf64_Xword p_filesz; Elf64_Xword p_memsz; Elf64_Xword p_align; } Elf64_Phdr;
|
在对ELF进行链接时会使用到这里面的数据,而在构造最小ELF时,可以忽略这块内容。
1 2 3 4 5 6 7 8 9 10 11 12
| typedef struct{ Elf64_Word sh_name; Elf64_Word sh_type; Elf64_Xword sh_flags; Elf64_Addr sh_addr; Elf64_Off sh_offset; Elf64_Xword sh_size; Elf64_Word sh_link; Elf64_Word sh_info; Elf64_Xword sh_addralign; Elf64_Xword sh_entsize; } Elf64_Shdr;
|
参考文章
golf.so Writeup
腾讯极客挑战赛丨从“碰撞”到“爆破”,42次尝试终破纪录
打造史上最小可执行ELF文件(45字节)