flat assembler
Message board for the users of flat assembler.
Index
> Linux > ELF executable + relocations/fixups for ASLR? Goto page Previous 1, 2, 3, 4 Next |
Author |
|
Tomasz Grysztar 30 Jan 2019, 23:03
Small correction: PT_GNU_RELRO is actually an area inside another segment (a PT_LOAD one). Because of how segments are defined in fasm, such special kind of segment is merged with the next "regular" segment that follows it. So you need to define another segment after GNURELRO segment, and the GNURELRO one is going to be mapped as part of that later segment in memory.
Now if you look at Furs's GCC-made example, GNU_RELRO is actually a part of PT_LOAD segment that has RW attributes. So I was wrong when I wrote earlier that loader must be temporarily applying writable attribute. The segment is writable from the start, it is only after the relocations are applied that it is set to "read only". The description of PT_GNU_RELRO is actually quite accurate here. Therefore that GCC executable does not in fact relocate the executable segment, it only applies relocations to an RW segment and then - because of PT_GNU_RELRO mapping - gets it changed to read-only. Back to fasm: when I wrote that in my sample you can replace WRITABLE with GNURELRO, it only worked because there is actually a regular writable segment that follows. And to make this work cleanly, you should make it like this: Code: segment readable gnurelro start: mov ecx,dword 0 fixup = $ - 4 mov edx,msg.len mov eax,4 mov ebx,1 int 0x80 mov eax,1 xor ebx,ebx int 0x80 segment readable executable writeable ; this is the actual segment that "gnurelro" one resides in segment readable msg db 'Hello world!',0xA .len = $ - msg The simple formatter of fasm 1 is not really a good framework to play freely with all these settings, it is limited by its linear segment syntax. But with fasmg's macros I could make a different framework with more flexible options - perhaps in my tutorial's second chapter I'm going to make something like that. |
|||
30 Jan 2019, 23:03 |
|
revolution 31 Jan 2019, 04:13
With 1.73.07 the previous code I posted doesn't compile. It gives the message "error: invalid use of symbol." Which is a good thing. We are alerted to the fact that the address isn't knowable at compile time.
So one tiny change is applied; the RVA operator. Full code below shows this in action Code: format ELF dynamic 0 at 0 entry start SYS_EXIT = 1 SYS_WRITE = 4 STD_OUTPUT = 1 segment readable writeable hello_world: db 'Running at 0x' running_at_hex db '00000000 Stack at 0x' stack_at_hex db '00000000',10 hello_world_len = $ - hello_world segment executable irp reg,eax,ebx,ecx,edx,esi,edi,ebp { if used mov_#reg#_eip mov_#reg#_eip: mov reg,[esp] retn end if } macro addr reg,offset { call mov_#reg#_eip lea reg,[reg + rva (offset - $)] ;use lea to avoid changing the flags } start: addr ecx,running_at_hex addr edx,0 call write_hex addr ecx,stack_at_hex mov edx,esp call write_hex mov eax,SYS_WRITE mov ebx,STD_OUTPUT addr ecx,hello_world mov edx,hello_world_len int 0x80 mov eax,SYS_EXIT xor ebx,ebx int 0x80 write_hex: ;ecx = address ;edx = value .next_nibble: mov eax,edx shr eax,28 cmp al,10 sbb al,0x69 das mov [ecx],al inc ecx shl edx,4 jnz .next_nibble retn Code: Running at 0x56623000 Stack at 0xFFBA2850 Running at 0x56648000 Stack at 0xFF925310 Running at 0x56592000 Stack at 0xFFD23FA0 Running at 0x565E8000 Stack at 0xFFF24500 Running at 0x565E4000 Stack at 0xFFF77020 Running at 0x56651000 Stack at 0xFFC6DB00 Running at 0x56586000 Stack at 0xFFAB5F80 Running at 0x56652000 Stack at 0xFF877930 Running at 0x56620000 Stack at 0xFFC43AC0 Running at 0x565A8000 Stack at 0xFFE8CEA0 The code is PIC, the executable is PIE. There are no relocations. Next up ... to create a relocation segment and see what is happening. |
|||
31 Jan 2019, 04:13 |
|
revolution 31 Jan 2019, 11:59
Going back to Furs example
Furs wrote: GCC with -fno-PIC seems to generate relocatable code: However, disassembly of the code shows it hasn't had the relocations applied. Code: c 00000000565A5510 00000000565A5510 mov eax,[530000C300002014] ; [530000C300002014]=? ;... c 00000000565A5504 00000000565A5504 mov [00B68DC300002014],eax ; [00B68DC300002014]=? Code: Relocation section '.rel.dyn' at offset 0x330 contains 10 entries: Offset Info Type Sym.Value Sym. Name 00000505 00000008 R_386_RELATIVE 00000511 00000008 R_386_RELATIVE ;... |
|||
31 Jan 2019, 11:59 |
|
revolution 31 Jan 2019, 12:50
If you need more evidence. Looking at the memory map for a file that includes another .so library I see this before the code has started running
Code: 08048000-08049000 rwxp 00000000 fd:00 13118232 /home/revolution/so.test 08049000-0804a000 rwxp 00000000 fd:00 13118232 /home/revolution/so.test 0804a000-0804c000 rwxp 00000000 00:00 0 0804c000-0804f000 r-xp 00000000 fd:00 13118232 /home/revolution/so.test f76dc000-f76de000 r--p 00000000 00:00 0 [vvar] f76de000-f76e0000 r-xp 00000000 00:00 0 [vdso] f76e0000-f7703000 r-xp 00000000 fd:00 19278360 /lib/i386-linux-gnu/ld-2.23.so f7703000-f7705000 rwxp 00022000 fd:00 19278360 /lib/i386-linux-gnu/ld-2.23.so ffd83000-ffda4000 rwxp 00000000 00:00 0 [stack] Code: 08048000-08049000 rwxp 00000000 fd:00 13118232 /home/revolution/so.test 08049000-0804a000 rwxp 00000000 fd:00 13118232 /home/revolution/so.test 0804a000-0804c000 rwxp 00000000 00:00 0 0804c000-0804f000 r-xp 00000000 fd:00 13118232 /home/revolution/so.test 08f42000-08f63000 rwxp 00000000 00:00 0 [heap] f6400000-f6421000 rwxp 00000000 00:00 0 f6421000-f6500000 ---p 00000000 00:00 0 f653d000-f653e000 ---p 00000000 00:00 0 f653e000-f6d3e000 rwxp 00000000 00:00 0 f6d3e000-f6d3f000 ---p 00000000 00:00 0 f6d3f000-f7541000 rwxp 00000000 00:00 0 f7541000-f76f1000 r-xp 00000000 fd:00 19278362 /lib/i386-linux-gnu/libc-2.23.so f76f1000-f76f3000 r-xp 001af000 fd:00 19278362 /lib/i386-linux-gnu/libc-2.23.so f76f3000-f76f4000 rwxp 001b1000 fd:00 19278362 /lib/i386-linux-gnu/libc-2.23.so f76f4000-f76f7000 rwxp 00000000 00:00 0 f76f7000-f76fe000 r-xp 00000000 fd:00 19278379 /lib/i386-linux-gnu/librt-2.23.so f76fe000-f76ff000 r-xp 00006000 fd:00 19278379 /lib/i386-linux-gnu/librt-2.23.so f76ff000-f7700000 rwxp 00007000 fd:00 19278379 /lib/i386-linux-gnu/librt-2.23.so f7700000-f7719000 r-xp 00000000 fd:00 19278361 /lib/i386-linux-gnu/libpthread-2.23.so f7719000-f771a000 r-xp 00018000 fd:00 19278361 /lib/i386-linux-gnu/libpthread-2.23.so f771a000-f771b000 rwxp 00019000 fd:00 19278361 /lib/i386-linux-gnu/libpthread-2.23.so f771b000-f771d000 rwxp 00000000 00:00 0 f7741000-f7743000 rwxp 00000000 00:00 0 f7743000-f777d000 r-xp 00000000 fd:00 18765758 /usr/local/lib/libftd2xx.so.1.4.6 f777d000-f777e000 r-xp 00039000 fd:00 18765758 /usr/local/lib/libftd2xx.so.1.4.6 f777e000-f777f000 rwxp 0003a000 fd:00 18765758 /usr/local/lib/libftd2xx.so.1.4.6 f777f000-f7780000 rwxp 00000000 00:00 0 f7780000-f7782000 r--p 00000000 00:00 0 [vvar] f7782000-f7784000 r-xp 00000000 00:00 0 [vdso] f7784000-f77a7000 r-xp 00000000 fd:00 19278360 /lib/i386-linux-gnu/ld-2.23.so f77a7000-f77a8000 r-xp 00022000 fd:00 19278360 /lib/i386-linux-gnu/ld-2.23.so f77a8000-f77a9000 rwxp 00023000 fd:00 19278360 /lib/i386-linux-gnu/ld-2.23.so ffb90000-ffbb1000 rwxp 00000000 00:00 0 [stack] So all the loader is doing for us is to load the main exe and the interpreter. It then jumps to the interpreter entry point and its job is done. |
|||
31 Jan 2019, 12:50 |
|
revolution 31 Jan 2019, 15:00
Here is code to relocate the executable. It is only a proof of concept so it has some ugly "features".
Code: format ELF dynamic 0 at 0 entry do_relocs macro r [inst] { common inst local .loc .loc = rva $ - 4 virtual relocs dd .loc end virtual } virtual at relocs_data relocs:: end virtual SYS_EXIT = 1 SYS_WRITE = 4 STD_OUTPUT = 1 segment readable writeable hello_world: db 'Running at 0x' running_at_hex db '00000000 Stack at 0x' stack_at_hex db '00000000',10 hello_world_len = $ - hello_world segment executable writeable start: r mov ecx,rva running_at_hex r mov edx,0 call write_hex r mov ecx,rva stack_at_hex mov edx,esp call write_hex mov eax,SYS_WRITE mov ebx,STD_OUTPUT r mov ecx,rva hello_world mov edx,hello_world_len int 0x80 mov eax,SYS_EXIT xor ebx,ebx int 0x80 write_hex: ;ecx = address ;edx = value .next_nibble: mov eax,edx shr eax,28 cmp al,10 sbb al,0x69 das mov [ecx],al inc ecx shl edx,4 jnz .next_nibble retn do_relocs: ;this code must be PIC call .get_eip sub ecx,rva $ cld lea esi,[ecx + rva relocs_data] .reloc_loop: lodsd cmp eax,-1 jz start add [ecx + eax],ecx jmp .reloc_loop .get_eip: mov ecx,[esp] ret segment readable ;align 4 ;enabling this instruction causes fasm v1.73.07 to crash relocs_data: virtual relocs sizeof.relocs = $ - $$ end virtual repeat sizeof.relocs / 4 load reloc dword from relocs:relocs_data + (% - 1) * 4 dd reloc end repeat dd -1 It still needs the rva in there. But if fasm could generate a reloc section like it does for PE then the rva requirement could be lifted and assume that it will be relocated. This would also work for a normal executable (not dynamic) but since Linux won't use ASLR on such files there isn't any point. I also discovered a crash bug in fasm.. Try enabling the line with the "align 4". It segfaults. |
|||
31 Jan 2019, 15:00 |
|
Tomasz Grysztar 31 Jan 2019, 15:36
revolution wrote: I also discovered a crash bug in fasm.. Try enabling the line with the "align 4". It segfaults. |
|||
31 Jan 2019, 15:36 |
|
revolution 03 Feb 2019, 18:40
I think the best approach here is to have our own relocation code running, as in my example above. Even of we are linking to other libraries and the interpreter would do the relocations for us, it gives us less flexibility and requires us to manipulate the segments to match precisely the "proper" format.
The relocation code is only 11 instructions. We can set the write permissions according to our own requirements. We don't need any interpreter when our code is stand-alone and only needs the kernel. The only remaining task is to have the assembler automatically mark all the relocatable address and write the table for us. |
|||
03 Feb 2019, 18:40 |
|
Tomasz Grysztar 03 Feb 2019, 18:54
revolution wrote: The only remaining task is to have the assembler automatically mark all the relocatable address and write the table for us. |
|||
03 Feb 2019, 18:54 |
|
revolution 03 Feb 2019, 19:02
I'll see about making some patches to fasm. If my workload remains light then I think I can make some time for it.
|
|||
03 Feb 2019, 19:02 |
|
Tomasz Grysztar 03 Feb 2019, 19:07
Another option could be to use object ELF output and a home-brew custom linker.
|
|||
03 Feb 2019, 19:07 |
|
revolution 04 Feb 2019, 01:21
Do you mean to write our own custom interpreter .so file?
|
|||
04 Feb 2019, 01:21 |
|
Tomasz Grysztar 04 Feb 2019, 05:56
I mean, in a simplest case, not a true linker but a converter that would translate ELF object into ELF executable with relocation table.
|
|||
04 Feb 2019, 05:56 |
|
revolution 04 Feb 2019, 17:42
Okay, so a post-processor that does the conversion. I don't see the advantage of that over having fasm create it during the assembly stage. It would be one extra step during the build phase.
|
|||
04 Feb 2019, 17:42 |
|
revolution 08 Feb 2019, 09:03
I'm quite disappointed with the way Linux sets permissions. The following code should segfault.
Code: format ELF executable 0 entry start SYS_EXIT = 1 SYS_WRITE = 4 STD_OUTPUT = 1 segment readable should_not_see: db "I'm running just fine",10 should_not_see_len = $ - should_not_see start: mov eax,SYS_WRITE mov ebx,STD_OUTPUT mov ecx,should_not_see mov edx,should_not_see_len int 0x80 mov eax,SYS_EXIT or ebx,-1 int 0x80 That code wrote: I'm running just fine So any and all data allocated by the program can also be executed. Including the stack. The only exceptions I can find are for file mappings that are opened as read-only and the vvar segment mapped from the kernel. Everything else is freely executable. Last edited by revolution on 08 Feb 2019, 17:20; edited 1 time in total |
|||
08 Feb 2019, 09:03 |
|
Tomasz Grysztar 08 Feb 2019, 17:16
This is where Linux is in fact similar to Windows, where "executable" attribute of PE section is not really respected unless you have IMAGE_DLLCHARACTERISTICS_NX_COMPAT flag set.
In Linux equivalent of this flag is inclusion of a segment of type PT_GNU_STACK. The attributes of this segment are supposed to define access permissions of the stack, but for some reason this also enables the NX feature for ELF segments overall. A single line is enough to set this up (like, for example, in fasmg source): Code: segment readable writeable gnustack |
|||
08 Feb 2019, 17:16 |
|
revolution 08 Feb 2019, 17:38
Tomasz Grysztar wrote: This is where Linux is in fact similar to Windows, where "executable" attribute of PE section is not really respected unless you have IMAGE_DLLCHARACTERISTICS_NX_COMPAT flag set. |
|||
08 Feb 2019, 17:38 |
|
revolution 08 Feb 2019, 17:48
Works
Code: format ELF executable 0 entry start SYS_EXIT = 1 SYS_WRITE = 4 STD_OUTPUT = 1 segment gnustack segment readable should_not_see: db "I'm running just fine",10 should_not_see_len = $ - should_not_see start: mov eax,SYS_WRITE mov ebx,STD_OUTPUT mov ecx,should_not_see mov edx,should_not_see_len int 0x80 mov eax,SYS_EXIT or ebx,-1 int 0x80 That code wrote: Segmentation fault (core dumped) Also execute-only sections are not writeable, but they are still readable. Code: format ELF executable 0 entry start SYS_EXIT = 1 SYS_WRITE = 4 STD_OUTPUT = 1 segment gnustack segment executable should_not_see: db "I'm running just fine",10 should_not_see_len = $ - should_not_see start: mov eax,[start] ;we can still read from execute-only sections push eax ;we can still write to the stack pop eax ;we can still read from the stack mov eax,SYS_WRITE mov ebx,STD_OUTPUT mov ecx,should_not_see mov edx,should_not_see_len int 0x80 mov eax,SYS_EXIT or ebx,-1 int 0x80 That code wrote: I'm running just fine |
|||
08 Feb 2019, 17:48 |
|
revolution 08 Feb 2019, 18:39
Since there is a way to get truly non-writeable executable sections I was inspired to write this as a proof of concept.
Code: format ELF dynamic 0 at 0 entry do_relocs ;the program entry address is overridden with this macro r [inst] { common inst local .loc .loc = rva $ - 4 virtual relocs dd .loc end virtual } virtual at do_relocs.relocs_data relocs:: end virtual SYS_EXIT = 1 SYS_WRITE = 4 STD_OUTPUT = 1 segment readable writeable hello_world: db 'Running at 0x' running_at_hex db '00000000 Stack at 0x' stack_at_hex db '00000000',10 hello_world_len = $ - hello_world segment executable start: r mov ecx,rva running_at_hex r mov edx,rva hello_world and not 0xfff call write_hex r mov ecx,rva stack_at_hex mov edx,esp call write_hex mov eax,SYS_WRITE mov ebx,STD_OUTPUT r mov ecx,rva hello_world mov edx,hello_world_len int 0x80 mov eax,SYS_EXIT xor ebx,ebx int 0x80 write_hex: ;ecx = address ;edx = value .next_nibble: mov eax,edx shr eax,28 cmp al,10 sbb al,0x69 das mov [ecx],al inc ecx shl edx,4 jnz .next_nibble retn segment gnustack ;ideally the following should just be "segment fixups" and the rest of ;the section is written by the assembler, but for now we do this manually segment executable label Elf32_Ehdr.e_entry dword at 0x18 label Elf32_Ehdr.e_phoff dword at 0x1c label Elf32_Ehdr.e_phentsize word at 0x2a label Elf32_Ehdr.e_phnum word at 0x2c label Elf32_Phdr.p_type dword at 0x00 label Elf32_Phdr.p_offset dword at 0x04 label Elf32_Phdr.p_paddr dword at 0x0c label Elf32_Phdr.p_memsz dword at 0x14 label Elf32_Phdr.p_flags dword at 0x18 label Elf32_auxv_t.a_type dword at 0 label Elf32_auxv_t.a_val dword at 4 ;p_type PT_LOAD = 1 ;p_flags PF_X = 1 shl 0 PF_W = 1 shl 1 PF_R = 1 shl 2 ;protection flags PROT_READ = 0x1 PROT_WRITE = 0x2 PROT_EXEC = 0x4 ;syscall SYS_MPROTECT = 125 SYS_EXIT_GROUP = 252 ;auxv AT_NULL = 0 AT_PHDR = 3 Elf32_auxv_t.size = 8 ;local END_OF_RELOCATIONS = -1 PAGE_ALIGNMENT = not 0xfff ;the following code section can run anywhere without needing ;any RVAs or relocations, it can be copied into the output as-is do_relocs: pop eax ;argument count (argc) push eax cld lea esi,[esp + 4 + (eax + 1) * 4] ;skip the args and the final null .skip_environment: lodsd test eax,eax ;last entry in environment? jnz .skip_environment .scan_auxv: lodsd mov ecx,eax ;ecx = Elf32_auxv_t.a_type lodsd ;eax = Elf32_auxv_t.a_val cmp ecx,AT_NULL ;end of auxv table? jz .fail cmp ecx,AT_PHDR jnz .scan_auxv mov edi,eax and edi,PAGE_ALIGNMENT mov esi,PROT_READ + PROT_WRITE + PROT_EXEC call .protect mov ebx,[edi + Elf32_Ehdr.e_phoff] mov ecx,[edi + ebx + Elf32_Phdr.p_paddr] sub ecx,[edi + ebx + Elf32_Phdr.p_offset] ;ecx = program base xor edx,edx mov dl,.program_entry - do_relocs add edx,[edi + Elf32_Ehdr.e_entry] sub edi,ecx lea esi,[edi + edx + .relocs_data - .program_entry] mov eax,edx .reloc_loop: add [edi + eax],edi lodsd cmp eax,END_OF_RELOCATIONS jnz .reloc_loop push dword[edi + edx] ;after retn we run the program add edi,ecx xor esi,esi ;restore to original permissions .protect: movzx ebp,[edi + Elf32_Ehdr.e_phnum] ;ebp = count of Phdr entries .loop_Phdr: dec ebp js .retn movzx eax,[edi + Elf32_Ehdr.e_phentsize] imul eax,ebp add eax,edi add eax,[edi + Elf32_Ehdr.e_phoff] ;eax = current Elf32_Phdr cmp [eax + Elf32_Phdr.p_type],PT_LOAD jnz .loop_Phdr mov ecx,[eax + Elf32_Phdr.p_flags] xor edx,edx assert PROT_EXEC or PF_X = PROT_READ or PF_R & PROT_WRITE = PF_W repeat 3 shr ecx,1 rcl edx,1 end repeat or edx,esi ;set the requested flags mov ebx,[eax + Elf32_Phdr.p_paddr] mov ecx,[edi + Elf32_Ehdr.e_phoff] sub ebx,[edi + ecx + Elf32_Phdr.p_paddr] add ebx,[edi + ecx + Elf32_Phdr.p_offset] mov ecx,[eax + Elf32_Phdr.p_memsz] add ecx,ebx and ebx,PAGE_ALIGNMENT sub ecx,ebx ;len add ebx,edi ;addr xor eax,eax mov al,SYS_MPROTECT int 0x80 test eax,eax jz .loop_Phdr .fail: xor eax,eax mov al,SYS_EXIT_GROUP or ebx,-1 int 0x80 .retn: retn .program_entry: dd rva start ;the program entry address is transferred to here, and it gets relocated .relocs_data: virtual relocs sizeof.relocs = $ - $$ end virtual repeat sizeof.relocs shr 2 load reloc dword from relocs:do_relocs.relocs_data + (% - 1) * 4 dd reloc end repeat dd END_OF_RELOCATIONS It runs in three main stages.
|
|||
08 Feb 2019, 18:39 |
|
revolution 09 Feb 2019, 09:29
It makes no difference what base address we use in the dynamic executable, the loader still places it into the same range. Using setarch to disable ASLR "setarch `uname -m` -R <progname>"
Code: format ELF dynamic 0 at 0 Running at 0x56555000 Stack at 0xFFFFCF20 ... format ELF dynamic 0 at 0x10000 Running at 0x56555000 Stack at 0xFFFFCF20 ... format ELF dynamic 0 at 0xffff0000 Running at 0x56555000 Stack at 0xFFFFCF20 ... So it would appear as though we are always stuck in the address range 0x56555000 to 0x56654000. However this isn't so. We can get another address range, but it requires changing a setting. Once again using setarch we can us the 3gb switch "setarch `uname -m` -3R <progname>" Code: Running at 0x41000000 Stack at 0xBFFFEF20 ... Running at 0x41000000 Stack at 0xBFFFEF20 I find the chosen address ranges disappointing. It cuts a slice in the address space and will limit the maximum size of any memory allocation. I haven't looked for the source code but it seems that it is just applying this formula. Code: (max_address_space / 3) and 0xfffff000 + 0x1000000 + (rand and 0xff) shl 12 |
|||
09 Feb 2019, 09:29 |
|
Goto page Previous 1, 2, 3, 4 Next < Last Thread | Next Thread > |
Forum Rules:
|
Copyright © 1999-2024, Tomasz Grysztar. Also on GitHub, YouTube.
Website powered by rwasa.