Ret to Argc
What happens when you return from a bare program?
x86_64-unknown-linux-gnuCrash Demo
_start// ret.c
int _start() {
return 0;
}$ gcc -nostdlib -static -o ret ret.c
$ ./ret
Segmentation fault (core dumped)
$ echo $?
139retargcWhat happens in a Normal C Program
_start__libc_start_mainmainexitmain_start_start__libc_start_mainmainmain__libc_start_mainexit_start__libc_start_mainWhat ret Sees at _start
ret_start[RSP]RIP[RSP]argc_startargcAt that point, the stack looks like this:
High | AUX, Strings, ... |
| NULL |
^ | envp[n]...envp[0] |
| | NULL |
| argv[n]...argv[0] |
[RSP] | argc |
Low | (Undefined) |ret- the kernel jumps to
_start _startexecutesret- the CPU loads
RIP <- [RSP] - here
[RSP]isargc - in the demo,
argc = 1, so the CPU tries to jump to address0x1
SIGSEGVCan argc Be a Valid Return Address?
argc_startargc- How many arguments can we pass to
_start? - What is the lowest valid executable address we have in the process?
How Many Arguments Can We Pass?
On a typical Linux system:
- argument is capped to 1/4 of the process stack limit
- the default stack limit is often 8 MiB (which is configurable via
ulimit -s) - each argument costs at least 9 bytes of stack space (a pointer plus a null terminator)
230,000What is the lowest valid executable address?
0x4000004,194,304230,000vm.mmap_min_addr655360x100000x10000vm.mmap_min_addr0x00x10000Building the Demo
0x10000exit(42)retbare_ret.s
; bare_ret.s
.intel_syntax noprefix
.global _start
.equ TARGET, 0x10000
.section .text
_start:
mov rax, 9 ; sys_mmap
mov rdi, TARGET
mov rsi, 4096
mov rdx, 7 ; PROT_READ | PROT_WRITE | PROT_EXEC
mov r10, 0x32 ; MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED
mov r8, -1
xor r9d, r9d
syscall
cmp rax, -4095
jae .failed
; b8 3c 00 00 00 mov eax, 60
; bf 2a 00 00 00 mov edi, 42
; 0f 05 syscall
movabs rbx, 0x002abf0000003cb8
mov qword ptr [TARGET + 0], rbx
mov dword ptr [TARGET + 8], 0x050f0000
ret
.failed:
mov eax, 60
mov edi, 1
syscalllauncher.c
// launcher.c
#define _GNU_SOURCE
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define TARGET_ARGC 0x10000
int main(void) {
const char *prog = "./bare_ret";
const size_t argc_target = TARGET_ARGC;
char **argv = calloc(argc_target + 1, sizeof(char *));
if (!argv) {
perror("calloc argv");
return 1;
}
argv[0] = (char *)prog;
for (size_t i = 1; i < argc_target; i++) {
argv[i] = (char *)"";
}
argv[argc_target] = NULL;
char *envp[] = { NULL };
execve(prog, argv, envp);
fprintf(stderr, "execve failed: %s\n", strerror(errno));
free(argv);
return 1;
}$ gcc -nostdlib -static -o bare_ret bare_ret.s
$ gcc -o launcher launcher.c
$ ./launcher && echo $?
42What Actually Happens
_start42launcherallocates anargvarray with0x10000entries, counting the program nameexecvestartsbare_retwithargc = 0x10000- the kernel places that
argcvalue at[RSP]before entering_start _startcallsmmapto create an executable page at0x10000_startwrites machine code forexit(42)into that page_startexecutesret- the CPU pops
0x10000from[RSP]intoRIP - execution continues at
0x10000, which performs theexit(42)syscall
And that is the end of the story.
Edited on 2026-04-09