构造一个最小ELF文件

2020年的时候,出现过两道构造最小ELF文件的题目:

前者要求构造一个共享库文件,通过 LD_PRELOAD=<upload> /bin/true 指定该文件以执行。(第一关要求大小<300字节,第二关要求大小<196字节)

后者要求构造一个可执行程序,执行后输出该ELF文件的MD5值。

两个题都很有意思,但出于了解如何构造最小ELF的目的,选择了相对简单点的第一个题 - golf.so,作为本文的目标。

本文按如下步骤,最终构造出一个170字节的满足golf.so题目要求的ELF文件:

  1. 创建一个ELF壳:利用已有汇编模板构造一个ELF可执行程序
  2. 创建一个动态链接库文件:动态链接库文件比一般可执行文件多了一条类型为 PT_DYNAMIC 的Program Header
  3. 缩减至192字节:将shellcode并入各个 Header 中
  4. 缩减至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中详细介绍了动态链接文件的构造方法,跟一般可执行文件的主要区别在于:

  1. 需要构造一个类型为 PT_DYNAMIC 的Program Header
  2. 需要以 _dynamic 为符号构造一个段,这个段用于存放Dynamic table entry
  3. 构造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   # 成功获得shell
$ 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各方面考虑:

  1. 覆写ELF Header中的e_entry:在 _dynamic 段存在时,可通过DT_INIT指定程序入口地址,所以ELF Header中的e_entry可以被覆盖为任意值
  2. 缩小ELF Header大小:通过删除ELF header中的e_shentsize 、e_shnum 、e_shstrnd 三个字段,ELF Header(e_ehsize)的大小可以缩小6字节
  3. 将_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

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]; /* ELF identification */
Elf64_Half e_type; /* Object file type */
Elf64_Half e_machine; /* Machine type */
Elf64_Word e_version; /* Object file version */

Elf64_Addr e_entry; /* Entry point address */
Elf64_Off e_phoff; /* Program header offset */
Elf64_Off e_shoff; /* Section header offset */

Elf64_Word e_flags; /* Processor-specific flags */
Elf64_Half e_ehsize; /* ELF header size */

Elf64_Half e_phentsize; /* Size of program header entry */
Elf64_Half e_phnum; /* Number of program header entries */

Elf64_Half e_shentsize; /* Size of section header entry */
Elf64_Half e_shnum; /* Number of section header entries */
Elf64_Half e_shstrndx; /* Section name string table index */
} Elf64_Ehdr;

比较重要的几个点:

  • e_entry - 指明程序加载到内存后,从哪个地址开始执行
  • e_phoff - program header在文件中的偏移
  • e_phentsize - 每个program header table的大小
  • e_phnum - 在program header中一共有多少个program header table

Program Header

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; /* Type of segment */ /* PT_LOAD, PT_DYNAMIC, PT_INTERP, 等 */
Elf64_Word p_flags; /* Segment attributes */ /* PF_X, PF_W, PF_R, 等 */
Elf64_Off p_offset; /* Offset in file */
Elf64_Addr p_vaddr; /* Virtual address in memory */
Elf64_Addr p_paddr; /* Reserved */
Elf64_Xword p_filesz; /* Size of segment in file */
Elf64_Xword p_memsz; /* Size of segment in memory */
Elf64_Xword p_align; /* Alignment of segment */
} Elf64_Phdr;

Section Header

在对ELF进行链接时会使用到这里面的数据,而在构造最小ELF时,可以忽略这块内容。

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct{
Elf64_Word sh_name; /* Section name */
Elf64_Word sh_type; /* Section type */
Elf64_Xword sh_flags; /* Section attributes */
Elf64_Addr sh_addr; /* Virtual address in memory */
Elf64_Off sh_offset; /* Offset in file */
Elf64_Xword sh_size; /* Size of section */
Elf64_Word sh_link; /* Link to other section */
Elf64_Word sh_info; /* Miscellaneous information */
Elf64_Xword sh_addralign; /* Address alignment boundary */
Elf64_Xword sh_entsize; /* Size of entries, if section has table */
} Elf64_Shdr;

参考文章

golf.so Writeup

腾讯极客挑战赛丨从“碰撞”到“爆破”,42次尝试终破纪录

打造史上最小可执行ELF文件(45字节)