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可执行程序的壳:
| 12
 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.asmBITS 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  $ - $$
 
 | 
执行结果:
| 12
 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的汇编:
| 12
 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 64org 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
 
 | 
编译命令及结果如下:
| 12
 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:
| 12
 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 中,对应的汇编代码如下:
| 12
 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 64org 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:
| 12
 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文件对应的汇编代码如下:
| 12
 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 64org 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:
| 12
 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对应的结构体如下:
| 12
 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 对应的结构体如下:
| 12
 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时,可以忽略这块内容。
| 12
 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字节)