Tiny "Manually" Created Elf Giving Segmentation Fault

Tiny manually created ELF giving segmentation fault


I don't know what segments or sections are missing or what offsets/addresses are wrong.

Your binary has PT_INTERP segment, which makes the Linux kernel think it's a dynamically linked binary. But it has no PT_DYNAMIC segment, which ld-linux expects.

Removing all references to INTERPRETER_{SEGMENT,SECTION} and adjusting number of program headers and sections turns this into a fully-static binary, that works:

nasm t.s
./t
Hello, world!

readelf -l t

Elf file type is EXEC (Executable file)
Entry point 0x4000b0
There are 2 program headers, starting at offset 64

Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040
0x0000000000000070 0x0000000000000070 R E 8
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x00000000000000fe 0x00000000000000fe R E 200000

Section to Segment mapping:
Segment Sections...
00
01 .text

Manually created ELF executable crashes with SIGSEGV

With your binary, I am getting different output from GDB:

(gdb) r
Starting program: /tmp/sample.elf.bad
During startup program terminated with signal SIGKILL, Killed.

Looking at the binary:

readelf -l sample.elf

Elf file type is EXEC (Executable file)
Entry point 0x8048080
There are 1 program headers, starting at offset 52

Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000054 0x08048080 0x00000000 0x0000c 0x0000c R E 0x1000

Here you are asking the kernel to mmap a segment with file offset 0x54 at virtual address 0x08048080.

Since these two numbers do not equal each other modulo page size, the kernel refuses:

strace ./sample.elf
execve("./sample.elf", ["./sample.elf"], [/* 42 vars */] <unfinished ...>
+++ killed by SIGKILL +++
Killed

Above strace means that the kernel tried to create the process, didn't like what it saw, and terminated it with prejudice. Not a single instruction of your binary was executed.

Fixing the LOAD virtual address and the entry point to be 0x08048054 produces desired working executable:

strace ./sample.elf
execve("./sample.elf", ["./sample.elf"], [/* 42 vars */]) = 0
[ Process PID=23172 runs in 32 bit mode. ]
_exit(0) = ?
+++ exited with 0 +++

Here is the hexdump for it:

hd ./sample.elf
00000000 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 10 |.ELF............|
00000010 02 00 03 00 01 00 00 00 54 80 04 08 34 00 00 00 |........T...4...|
00000020 00 00 00 00 00 00 00 00 34 00 20 00 01 00 28 00 |........4. ...(.|
00000030 00 00 00 00 01 00 00 00 54 00 00 00 54 80 04 08 |........T...T...|
00000040 00 00 00 00 0c 00 00 00 0c 00 00 00 05 00 00 00 |................|
00000050 00 10 00 00 b8 01 00 00 00 bb 00 00 00 00 cd 80 |................|
00000060

Get the address that caused segmentation fault from core dump using C


My question is, how can I get the address that caused the core dump
in C?

Short answer:

There are two ways to interpret this question.

  1. What was address of the faulting instruction?

  2. What was the address that was out of bounds?

Elf core dumps keep all of the meta information in notes, which are
stored in a note segment. The notes are of different types.

To answer #1, we need to grab the registers. Look at the elf header to
find the program header table. Walk the program header table to find
the note table (type PT_NOTE). Walk the note table to find a note of
type NT_PRSTATUS. The payload of this note is a struct
elf_prstatus
, which can be found in linux/elfcore.h. One of the
fields of this struct is all of the general purpose registers. Grab
%rip and you are done.

For #2, we do something similar. This time we are looking for a note
of type NT_SIGINFO. The payload of this note is a siginfo_t structure
defined in signal.h. For applicable signals (SIGILL, SIGFPE, SIGSEGV,
SIGBUS), the field si_addr will contain the address you tried to
access but couldn't.

More information is below. In the example core dump, rip is 0x400560, the instruction address that tried to do an illegal access. This is displayed with the rest of the general purpose registers.

The memory the program tried to access is at 0x03. This is displayed with the rest of the signal information.

The long answer:

I think BFD has 25 years of cruft on it, so I wouldn't use it just to dump the contents of a core file on a linux box. Maybe if you had to write some kind of general purpose code that needs to work with a bunch of formats, but even then I'm not sure that's how I would go today.

The elf spec is pretty well written and it is not hard to just walk through the tables of program headers or section headers as needed. All of the process meta information in a core file is contained in a set of notes in a PT_NOTE program segment that can be parsed out in just a few lines of straight C code.

I wrote a little program to grab the registers out of a x86_68 core file and print some of the meta data. I put it on github. The logic for getting a note payload is in this function:

void *get_note(void *vp, int nt_type){
Elf64_Ehdr *eh=vp;
for(int i=0; i<eh->e_phnum; ++i){
Elf64_Phdr *ph=(vp+eh->e_phoff+i*eh->e_phentsize);
if(ph->p_type!=PT_NOTE){
continue;
}
void *note_table=(vp + ph->p_offset);
void *note_table_end=(note_table+ph->p_filesz);
Elf64_Nhdr *current_note=note_table;
while(current_note<(Elf64_Nhdr *)note_table_end){
void *note_end=current_note;
note_end += 3*sizeof(Elf64_Word);
note_end += roundup8(current_note->n_namesz);
if(current_note->n_type==nt_type){
return note_end;
}
note_end += roundup8(current_note->n_descsz);
current_note=note_end;
}
}
return 0;
}

The function is handed a pointer to the elf file and a note type and returns a pointer the payload of the associated note, if it exists. The various possible note types are in elf.h. The note types I actually see in core files on my machine are:

#define NT_PRSTATUS 1       /* Contains copy of prstatus struct */
#define NT_FPREGSET 2 /* Contains copy of fpregset struct */
#define NT_PRPSINFO 3 /* Contains copy of prpsinfo struct */
#define NT_AUXV 6 /* Contains copy of auxv array */
#define NT_X86_XSTATE 0x202 /* x86 extended state using xsave */
#define NT_SIGINFO 0x53494749 /* Contains copy of siginfo_t,
size might increase */
#define NT_FILE 0x46494c45 /* Contains information about mapped
files */

Most of these structures are in headers under /usr/include/linux. The xsave structure is a couple KB of floating point information described in Ch 13 of the intel manual. It has the SSE, AVX, and MPX, registers in it.

The NT_FILE payload doesn't seem to have an associated struct in a header, but it is described in a kernel comment (fs/binfmt_elf.c):

/*
* Format of NT_FILE note:
*
* long count -- how many files are mapped
* long page_size -- units for file_ofs
* array of [COUNT] elements of
* long start
* long end
* long file_ofs
* followed by COUNT filenames in ASCII: "FILE1" NUL "FILE2" NUL...
*/

The changes for parsing the elf file for a 32 bit system are pretty trivial. Use the corresponding Elf32_XXX structures and round up by 4 instead of 8 for the variable sized fields.

I've been adding stuff to this little program last couple of days. Currently it does the file header, segment headers, general registers, program status, program info and a backtrace. I'll add support for the rest of the notes as I get time. Here is the current output:

 $ ./read_pc -biprst core
General Registers:
r15 0x000000000000000000 r14 0x000000000000000000
r13 0x0000007ffc20d36a50 r12 0x000000000000400430
rbp 0x0000007ffc20d36950 rbx 0x000000000000000000
r11 0x000000000000000246 r10 0x000000000000000000
r9 0x000000000000000002 r8 0x000000000000000000
rax 0x000000000000000003 rcx 0x00000000007ffffffe
rdx 0x0000007f5817523780 rsi 0x000000000000000001
rdi 0x000000000000000001 ss 0x00000000000000002b
rip 0x000000000000400560 cs 0x000000000000000033
eflags 0x000000000000010246 rsp 0x0000007ffc20d36950
fs_base 0x0000007f5817723700 gs_base 0x000000000000000000
ds 0x000000000000000000 es 0x000000000000000000
fs 0x000000000000000000 gs 0x000000000000000000
orig_rax 0x00ffffffffffffffff

Program status:
signo 11 signal code 0 errno 0
cursig 11 sigpend 000000000000000000 sigheld 000000000000000000
pid 27547 ppid 26600 pgrp 27547 sid 26600
utime: 0.000000 stime 0.000000
cutime: 0.000000 cstime 0.000000
fpvalid: 1


Signal Information:
signo: 11 errno 0 code 1
addr 0x3 addr_lsb 0 addr_bnd ((nil), (nil))


Process Information:
state 0 (R) zombie 0 nice 0 flags 0x400600
uid 1000 gid 1000 pid 27547 ppid 26600 pgrp 27547 sid 26600
fname: foo
args: ./foo


Backtrace:
rip = 0x000000000000400560
rip = 0x000000000000400591
rip = 0x0000000000004005a1


Program Headers:
Type Offset Virt Addr PhysAddr
FileSiz MemSize Flags Align
NOTE 0x00000000000004a0 0x0000000000000000 0000000000000000
0x0000000000000b98 0x0000000000000000 0x000000
LOAD 0x0000000000002000 0x0000000000400000 0000000000000000
0x0000000000001000 0x0000000000001000 R X 0x001000
LOAD 0x0000000000003000 0x0000000000600000 0000000000000000
0x0000000000001000 0x0000000000001000 X 0x001000
LOAD 0x0000000000004000 0x0000000000601000 0000000000000000
0x0000000000001000 0x0000000000001000 WX 0x001000
LOAD 0x0000000000005000 0x00000000018bf000 0000000000000000
0x0000000000021000 0x0000000000021000 WX 0x001000
LOAD 0x0000000000026000 0x00007f581715e000 0000000000000000
0x0000000000001000 0x00000000001c0000 R X 0x001000
LOAD 0x0000000000027000 0x00007f581731e000 0000000000000000
0x0000000000000000 0x00000000001ff000 0x001000
LOAD 0x0000000000027000 0x00007f581751d000 0000000000000000
0x0000000000004000 0x0000000000004000 X 0x001000
LOAD 0x000000000002b000 0x00007f5817521000 0000000000000000
0x0000000000002000 0x0000000000002000 WX 0x001000
LOAD 0x000000000002d000 0x00007f5817523000 0000000000000000
0x0000000000004000 0x0000000000004000 WX 0x001000
LOAD 0x0000000000031000 0x00007f5817527000 0000000000000000
0x0000000000001000 0x0000000000026000 R X 0x001000
LOAD 0x0000000000032000 0x00007f5817722000 0000000000000000
0x0000000000003000 0x0000000000003000 WX 0x001000
LOAD 0x0000000000035000 0x00007f581774a000 0000000000000000
0x0000000000002000 0x0000000000002000 WX 0x001000
LOAD 0x0000000000037000 0x00007f581774c000 0000000000000000
0x0000000000001000 0x0000000000001000 X 0x001000
LOAD 0x0000000000038000 0x00007f581774d000 0000000000000000
0x0000000000001000 0x0000000000001000 WX 0x001000
LOAD 0x0000000000039000 0x00007f581774e000 0000000000000000
0x0000000000001000 0x0000000000001000 WX 0x001000
LOAD 0x000000000003a000 0x00007ffc20d16000 0000000000000000
0x0000000000022000 0x0000000000022000 WX 0x001000
LOAD 0x000000000005c000 0x00007ffc20d9c000 0000000000000000
0x0000000000002000 0x0000000000002000 X 0x001000
LOAD 0x000000000005e000 0x00007ffc20d9e000 0000000000000000
0x0000000000002000 0x0000000000002000 R X 0x001000
LOAD 0x0000000000060000 0xffffffffff600000 0000000000000000
0x0000000000001000 0x0000000000001000 R X 0x001000
All worked

Why do I get a segmentation fault when writing to a char *s initialized with a string literal, but not char s[] ?

See the C FAQ, Question 1.32

Q: What is the difference between these initializations?

char a[] = "string literal";

char *p = "string literal";

My program crashes if I try to assign a new value to p[i].

A: A string literal (the formal term
for a double-quoted string in C
source) can be used in two slightly
different ways:

  1. As the initializer for an array of char, as in the declaration of char a[] , it specifies the initial values
    of the characters in that array (and,
    if necessary, its size).
  2. Anywhere else, it turns into an unnamed, static array of characters,
    and this unnamed array may be stored
    in read-only memory, and which
    therefore cannot necessarily be
    modified. In an expression context,
    the array is converted at once to a
    pointer, as usual (see section 6), so
    the second declaration initializes p
    to point to the unnamed array's first
    element.

Some compilers have a switch
controlling whether string literals
are writable or not (for compiling old
code), and some may have options to
cause string literals to be formally
treated as arrays of const char (for
better error catching).

What is the smallest x86_64 Hello World ELF binary?


I tried to write the smallest possible x86_64 ELF hello world program by hand

You should provide a source for your program, so we can fix it.

gdb says: During startup program terminated with signal SIGSEGV

This is GDB telling you that it called fork/execve to create the target program, and expected the kernel to notify GDB that the program is now ready to be debugged. Instead, the kernel notified GDB that the program has died with SIGSEGV, without ever reaching its first instruction.

GDB didn't expect that. Why would this happen?

This happens when the kernel looks at your executable, and says "I can't create a running program out of that".

Why is that the case here? Because this LOAD segment:

Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000031 0x0000000000000031 R E 0x2

is asking the kernel to map 0x31 bytes from offset 0 in the file to virtual address 0. But the kernel (rightfully) refuses such nonsense request, and terminates the program with SIGSEGV before returning from execve.

You could probably avoid this by making the file ET_DYN instead of ET_EXEC -- that would change the meaning of your program header from "map this segment at 0" to "map this segment anywhere".

You could definitely avoid this by keeping the ET_EXEC, but changing the .p_vaddr and .p_paddr of the segment to something like 0x10000.

TL;DR: Your program and file headers must make sense to the kernel, or you'll never get off the ground.

In ELF, why do the headers need to be in one segment?


But I don't understand why Linux needs that headers to be loaded at run time.

It doesn't.

What are they used for? If they are needed for the process to run, couldn't Linux load it by himself?

To answer all of these questions, you need to look at the Linux kernel source.

In the source, you can see that in fact program headers do not need to be a part of any PT_LOAD segment, and that the kernel will read them all on its own.

Changing your original program like so:

diff -u exe.asm.orig exe.asm
--- exe.asm.orig 2021-02-07 18:54:34.449336515 -0800
+++ exe.asm 2021-02-07 18:53:19.773532451 -0800
@@ -24,9 +24,9 @@
programHeader:
dd 1 ; p_type
dd 7 ; p_flags
- dq 0 ; p_offset
- dq $$ ; p_vaddr
- dq $$ ; p_paddr
+ dq _start - $$ ; p_offset
+ dq _start ; p_vaddr
+ dq _start ; p_paddr
dq fileSize ; p_filesz
dq fileSize ; p_memsz
dq 0x1000 ; p_align

produces a program which runs fine, but in which the program header is not in the PT_LOAD segment:

 eu-readelf --all exe
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Ident Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: AMD x86-64
Version: 1 (current)
Entry point address: 0x8048078
Start of program headers: 64 (bytes into file)
Start of section headers: 0 (bytes into file)
Flags:
Size of this header: 64 (bytes)
Size of program header entries: 56 (bytes)
Number of program headers entries: 1
Size of section header entries: 0 (bytes)
Number of section headers entries: 0 ([0] not available)
Section header string table index: 0

Section Headers:
[Nr] Name Type Addr Off Size ES Flags Lk Inf Al

Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000078 0x0000000008048078 0x0000000008048078 0x000081 0x000081 RWE 0x1000

I have tried adding padding

You didn't do that correctly. Using your "with padding" source results in the following exe-padding:

...
Entry point address: 0x8049000
...
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x001000 0x0000000008048000 0x0000000008048000 0x000009 0x000009 RWE 0x1000

This binary is started by the kernel, and immediately jumps to the start address 0x8049000, which isn't mapped (since it's not covered by the PT_LOAD segment), resulting in immediate SIGSEGV.

To fix this, you need to adjust the entry address:

diff -u exe-padding.asm.orig exe-padding.asm
--- exe-padding.asm.orig 2021-02-07 18:57:31.800871195 -0800
+++ exe-padding.asm 2021-02-07 19:34:27.303071700 -0800
@@ -8,7 +8,7 @@
dw 2 ; e_type
dw 62 ; e_machine
dd 1 ; e_version
- dq _start ; e_entry
+ dq _start - 0x1000 ; e_entry
dq programHeader - $$ ; e_phoff
dq 0 ; e_shoff
dd 0 ; e_flags

This again produces a working executable. For the record:

eu-readelf --all exe-padding
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Ident Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: AMD x86-64
Version: 1 (current)
Entry point address: 0x8048000
Start of program headers: 64 (bytes into file)
Start of section headers: 0 (bytes into file)
Flags:
Size of this header: 64 (bytes)
Size of program header entries: 56 (bytes)
Number of program headers entries: 1
Size of section header entries: 0 (bytes)
Number of section headers entries: 0 ([0] not available)
Section header string table index: 0

Section Headers:
[Nr] Name Type Addr Off Size ES Flags Lk Inf Al

Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x001000 0x0000000008048000 0x0000000008048000 0x000009 0x000009 RWE 0x1000

P.S. You are linking your 64-bit program at 0x08048000, which is the traditional load address for i*86 (32-bit) executables. x86_64 binaries traditionally start at 0x400000.

Update:

About the first example, p_filesz is still fileSize, I think that should get outside of the boundaries of the file.

That is correct: p_filesz and p_memsz should be reduced by the size of headers (0x78 here). Note that both of these will be rounded up to page size (after adding p_offset), so for this example there is no practical difference.

Update 2:

pastebin.ubuntu.com/p/rgfVMrbcmJ

This results in the following LOAD segment:

Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000078 0x0000000008048000 0x0000000008048000 0x000081 0x000081 RWE 0x1000

This binary will not run (kernel will reject it), because it is asking the kernel to do the impossible: to mmap bytes at offset 0x78 to page start.

If the application performed equivalent mmap call, it would have gotten EINVAL error, because mmap requires that (offset % pagesize) == (addr % pagesize).

ELF program header offset

The ELF specification requires that for demand paged executables, the file offset and virtual address for a segment must indeed match in the lower-order bits.

These restrictions are the same that the mmap() system call places on mappings -- it only accepts mappings at an offset in the file that is a multiple of the page size. When mapping an ELF file, the segments are extended to the nearest page boundary, so the lower-order bits are effectively ignored except for the segment size calculation.

One possible rationale for this is that the underlying device may already be memory-mapped -- such as a frame buffer, or flash memory -- in which case it would impose a substantial overhead to create a mapping with an offset that is not page aligned.



Related Topics



Leave a reply



Submit