elf文件中segment 与section之间的关系

330 views
Skip to first unread message

jack

unread,
Oct 16, 2015, 6:20:42 AM10/16/15
to sh...@googlegroups.com
请教下大家,我想在elf文件中注入一部分代码,是直接注入在.text段的首部,section部分的都已经处理好,但是program header与segment之间的关系不是很清晰,所以注入完的elf可执行文件都报segmentation fault错误,而在查看这些文件的sections时都报:readelf:警告:the .dynamic section is not the first section in the dynamic segment. 错误。如何能够完整注入到elf文件里面?谢谢了。

Ray Song

unread,
Oct 18, 2015, 11:22:05 PM10/18/15
to jack, sh...@googlegroups.com
On 2015-10-16, jack wrote:
>请教下大家,我想在elf文件中注入一部分代码,是直接注入在.text段的首部,section部
>分的都已经处理好,但是program header与segment之间的关系不是很清晰,所以注入完的
>elf可执行文件都报segmentation fault错误,而在查看这些文件的sections时都报:
>readelf:警告:the .dynamic section is not the first section in the dynamic
>segment. 错误。如何能够完整注入到elf文件里面?谢谢了。

我的理解是

program header(readelf -l)是供内核使用的,确定可执行文件的加载方式
多数程序用section header(readelf -S),它比program header详细

你是直接在 .text 前添加了若干字节吗?.text 后所有 section 的偏移量会改变,可执行文件中所有绝对地址引用这些部分的地方得修改

当前能想到的涉及绝对地址需要修正的地方:
- section header(通常在.text前) 中把它们的偏移量改掉
- program header 的 PT_DYNAMIC 应与 .dynamic 重合,且其中的条目(readelf -d,有些项对应一个section)偏移量都要修正
- relocation section中的条目,它们指向 GOT (x86为.got .got.plt)
- 函数指针,它们指向 .text
- 引用静态存储区变量地址的代码、静态变量。x86很可能以(mov xxx, 123)绝对地址的方式编码进指令,得修改。.data 中可能包含绝对地址
- .init_array 中的 static constructor 偏移量

另外一些麻烦的地方:
- libc 尚未初始化,你无法使用C函数库。比较好的办法是在 .text section 的入口处把 __libc_start_main 指向 main 的参数改成你自己的
- 很难在各个有用section间找到空隙安插你的代码。可以考虑在 program header 中把 .data .bss 所在的 segment 大小加长到超过ELF文件尾部,在末尾添加代码,并为该segment添加PF_X(可执行)标志。把main参数设到原来ELF尾部你添加代码的地方


--
Ray
http://maskray.me

jack

unread,
Oct 20, 2015, 7:58:14 AM10/20/15
to Ray Song, sh...@googlegroups.com
非常感谢,注入的字节码是直接用汇编写的,直接调用了系统调用接口,没有用任何的第三方库, 那这样的话就需要新增一个section,将它追加到elf文件的尾部,那么对应的就需要调整program header中.data, .bss对应的segment(需要添加一个section header吗?),但是program header中有三个属性之间的关系我不是特别的清楚,一个是offset, filesz, memsz,能否讲解一些,多谢了。

Ray Song

unread,
Oct 20, 2015, 10:27:27 AM10/20/15
to jack, sh...@googlegroups.com
On 2015-10-20, jack wrote:
>非常感谢,注入的字节码是直接用汇编写的,直接调用了系统调用接口,没有用任何的第
>三方库,那这样的话就需要新增一个section,将它追加到elf文件的尾部,那么对应的就
>需要调整program header中.data, .bss对应的segment(需要添加一个section header吗
>?),但是program header中有三个属性之间的关系我不是特别的清楚,一个是offset,
>filesz, memsz,能否讲解一些,多谢了。
>
拿 Arch Linux 最新版本的喵喵开刀……

Name : coreutils
Version : 8.24-1
Description : The basic file, shell and text manipulation utilities of the GNU operating system
Architecture : x86_64

下面给一个示例,效果是改变入口点,多执行一条 jmp 到原来的入口点。

#include <elf.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>

#define ROF(i, a, b) for (int i = (b); --i >= (a); )

int main(int argc, char *argv[])
{
int fd = open(argv[1], O_RDWR);
struct stat st;
fstat(fd, &st);
size_t size = st.st_size + 5; // 比原始文件多5字节,编码一条 jmp 指令到原始入口地址
ftruncate(fd, size);
char *c = (char*)mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
auto *elf = (Elf64_Ehdr*)c; // 文件头即ELF头
auto *phdr = (Elf64_Phdr*)(c+elf->e_phoff); // 文件中program header偏移量
ROF(i, 0, elf->e_phnum)
if (phdr[i].p_type == PT_LOAD) { // 最后一个 PT_LOAD `readelf -l`
phdr[i].p_flags |= PF_X; // 原来用来存 .data .bss 等,为之添加可执行标志
phdr[i].p_filesz = size - phdr[i].p_offset; // 延长至文件尾,这样.bss后面的文件内容也会被载入进该segment,我们的代码在文件末尾
// 注意 p_memsz 按页向上取整会是该 segment 大小(/proc/$pid/maps 查看),原来 segment 末尾部分是 .bss,其内容不在ELF中(特征是p_memsz > p_filesz),加载时会被内核初始化为0,cat 的 p_memsz 较大,覆盖了文件末尾后很大一片区域的虚拟地址,我们的指令与 .bss 一部分重叠。妥善的处理方式是在执行完任务跳转回原始入口地址时把自身清零(重新开辟一段空间,拷贝自身指令到那里,跳转到那里,把原始文件对应 .bss 除清零)
c[size-5] = 0xe9; // JMP 的 opcode
Elf64_Addr new_entry = phdr[i].p_vaddr + size-5 - phdr[i].p_offset;
*(uint64_t*)&c[size-4] = elf->e_entry - (new_entry + 5);
elf->e_entry = new_entry;
break;
}
}


% cp =cat /tmp/a
% ./main /tmp/a # 修改
% gdb -ex 'b *0x60cb20' -ex r /tmp/a
Reading symbols from /tmp/a...(no debugging symbols found)...done.
Breakpoint 1 at 0x60cb20
Starting program: /tmp/a

Breakpoint 1, 0x000000000060cb20 in ?? ()
(gdb) x/i $rip
=> 0x60cb20: jmp 0x402590 # 入口点发生变化且执行了我们的指令
(gdb) ni
0x0000000000402590 in ?? ()
(gdb) x/5i $rip # 原始入口点
=> 0x402590: xor ebp,ebp
0x402592: mov r9,rdx
0x402595: pop rsi
0x402596: mov rdx,rsp
0x402599: and rsp,0xfffffffffffffff0
(gdb)


--
Ray
http://maskray.me

Ray Song

unread,
Oct 20, 2015, 11:44:09 AM10/20/15
to jack, sh...@googlegroups.com
typedef struct
{
Elf64_Word p_type; /* Segment type */
Elf64_Word p_flags; /* Segment flags */
Elf64_Off p_offset; /* Segment file offset */
Elf64_Addr p_vaddr; /* Segment virtual address */
Elf64_Addr p_paddr; /* Segment physical address */
Elf64_Xword p_filesz; /* Segment size in file */
Elf64_Xword p_memsz; /* Segment size in memory */
Elf64_Xword p_align; /* Segment alignment */
} Elf64_Phdr;

program header中每个PT_LOAD (readelf -l 中的 LOAD)会被分配一段虚拟内存。
如果 elf 是一般的可执行文件 (ET_EXEC, readelf -h: Type),加载首地址会是 p_vaddr 指定值按页向下取整。否则,(.so、pie等)被随机加载。
这段虚拟内存的大小取决于 p_memsz

文件中 [p_offset, p_offset+p_filesz) 部分会被拷贝到这段虚拟内存。
当 p_filesz < p_memsz 时,多出来的部分会被填充为0。.bss 所在的 segment 就属于此例,运行时被填充为0,而ELF中并没有相应零字节

以上为个人理解,可能有误,以内核代码 fs/binfmt_elf.c 为准

https://github.com/MaskRay/userspace-exec/blob/master/main.c (状态是x86 x86-64 armel 在某些编译选项下能用,当时DEFCON 23没有更多时间了……) 的 load_elf 参考自 https://grugq.github.io/docs/ul_exec.txt
是上述内核代码的用户态模拟


--
Ray
http://maskray.me

Ray Song

unread,
Oct 20, 2015, 12:05:08 PM10/20/15
to jack, sh...@googlegroups.com
发现 userspace-exec 和你想做的很像
你的功能可以考虑用C实现。写汇编,函数前加 __attribute__((section(".meowmeowmeow"))) 注解。
编译生成 .o 后用 objcopy 从 .meowmeowmeow section 中提取代码。这部分代码必须自包含,不能有指向 .text 的引用

系统调用的 wrapper 不能用 glibc,需要自行实现:参考我的 syscall.h
这段代码得写成 position independent 的,不能夹杂绝对地址。注意不是说用 -fPIC,i386 这么做会在 .text 引入 __x86.get_pc_thunk.bx .meowmeowmeow 引用了 .text
需要开辟内存的话 mmap 一段 MAP_PRIVATE 空间
回到原来的入口地址前应该:

# end of original ELF
# ... your instructions ...
# before jumping back to the entry
call mmap PROT_READ|PROT_WRITE|PROT_EXEC
copy the instructions from L1 to the new region
jump to the new region
L1:
zero the region corresponding to .bss
jump to the entry

否则 .bss 中有一块是你填充的指令,不全为0,可能不是应用程序想要的



>--
>Ray
>http://maskray.me
Reply all
Reply to author
Forward
0 new messages