Calling NULL Pointer Hello World in C

If you ever programmed in C, you know you can’t deference a NULL pointer. Consider the following program:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <unistd.h>
#include <sys/syscall.h>

__attribute__((section(".zerotext")))
static void foo() {
syscall(SYS_write, 1, "Hello World!\n", 13);
syscall(SYS_exit, 0);
}

int main() {
void (*f)() = 0;
f();
}

This should segmentation fault. Let’s run it.

1
2
$ ./a.out
Hello World!

It prints “Hello World!”, foo is called. How is it possible? In this article, I will demonstrate to achieve this.

Dereferencing NULL Pointer

Dereferencing NULL pointer is quite easy,

1
2
void *p = mmap(0, sysconf(_SC_PAGESIZE), PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_PRIVATE | MAP_FIXED | MAP_ANONYMOUS, -1, 0);

Since MAP_FIXED flag is used, mmap will map exactaly 0, i.e., NULL address or fail. Usually this returns -1 which indicates MMAP_FAILED, but if you run this code with root privilege or vm.mmap_min_addr=0, it will return a valid NULL adress. Then you can write a hello wrold assembly, compile it to opcodes, copy them to NULL address and call NULL, you get a hello world from NULL. But I want my program to load to NULL address directly. To achieve this, we need linker script.

Loading Code into NULL Address with Linker Script

Notice the __attribute__((section(".zerotext"))) above foo function, that tells the compiler to put foo into a custom section zerotext. By default, codes are put into .text section, initialized globals .data section and uninitialized globals .bss section. Linker will order these sections and form program image. Usually you don’t have to modify them, but sometimes you do need to control program image layout, for example bare metal embeded systems, libopencm3 is a good example for that.

To control program image layout, you need to a modified linker script.

First dump default linker script with ld -v --verbose >aarch64.ld.

1
2
3
4
5
6
7
8
9
10
11
OUTPUT_FORMAT("elf64-littleaarch64", "elf64-bigaarch64",
"elf64-littleaarch64")
OUTPUT_ARCH(aarch64)
ENTRY(_start)
SEARCH_DIR("=/usr/local/lib/aarch64-linux-gnu"); SEARCH_DIR("=/lib/aarch64-linux-gnu"); SEARCH_DIR("=/usr/lib/aarch64-linux-gnu"); SEARCH_DIR("=/usr/local/lib"); SEARCH_DIR("=/lib"); SEARCH_DIR("=/usr/lib"); SEARCH_DIR("=/usr/aarch64-linux-gnu/lib");
SECTIONS
{
/* Read-only sections, merged into text segment: */
PROVIDE (__executable_start = SEGMENT_START("text-segment", 0x400000)); . = SEGMENT_START("text-segment", 0x400000) + SIZEOF_HEADERS;
.interp : { *(.interp) }
/* ... */

We are going to modify SECTIONS. . means current address, and . = SEGMENT_START("text-segment", 0x400000) + SIZEOF_HEADERS; means to make the current address equals to SEGMENT_START("text-segment", 0x400000) + SIZEOF_HEADERS, that might be something greater than 0x400000. As it’s the first line, that is the program’s lowest load address.

The .interp : { *(.interp) } means to create an .interp section, and put all secions named .interp into it.

To load our .zerotext section to 0 is straightforward, just put .zerotext : { *(.zerotext) } into the first line of SECTIONS,

1
2
3
4
5
6
SECTIONS
{
.zerotext : { *(.zerotext) }
/* Read-only sections, merged into text segment: */
PROVIDE (__executable_start = SEGMENT_START("text-segment", 0x400000)); . = SEGMENT_START("text-segment", 0x400000) + SIZEOF_HEADERS;
.interp : { *(.interp) }

Compiling and Running the Program

Let’s compile our code with linker script we created aboved and run it with root privilege.

1
$ gcc  -T aarch64.ld -nostartfiles -emain -static -fno-pie -fno-stack-protector main.c

Here are what the compile flags used for.

-T aarch64.ld to specify the modified linker script.

-nostartfiles to not use standard system startup files with links. Startup files are usually crt1.o, crti.o, and crtn.o, depending on the architecture. They are responsible for calling initialize and finilize code from .init .fini segements, prepare argc, argv and call main etc. Read the dumped linker script you will see ENTRY(_start) line, it means the program entry point is _start function, which is provided by startup files. Entry point is the first function called after the program is loaded into memory. Since we are not using startup files, we use -emain to override the linker script’s entry point to use main as entry point.

-static to make the program statically linked. If this option is not present, the linker will emit an error says “PHDR segment not covered by LOAD segment”. I’m not a toolchain expert, I think that’s because we place our zerotext at top and break some defaults for PHDR related stuff. And if you are doing something like this, you are going to make the program statically linked anyway. If the program is dynamically linked, it is unlikely the dynamic linker will load our program into the desired address.

And some minor flags. -fno-pie to prevent static-pie and ASLR ralated stuff from loading our program into undesired address, and -fno-stack-protector because I think it is necessary.

Finally run the program.

1
2
$ sudo ./a.out
Hello World!