Mastering Syscall-Level Reversing pt1
1. Introduction: Why Syscall-Level Matters
When I first started reverse engineering, I made a critical mistake: I focused exclusively on high-level API calls. printf, ReadFile, socket, these were my comfort zone. But then I encountered my first piece of malware that bypassed every user-mode hook I had set up. It was calling the kernel directly, laughing at my detection mechanisms.
That’s when I realized: if you want to truly understand what a program is doing, you must go to the source, the system call layer.
Syscall-level reversing is the art of analyzing a program’s direct conversations with the operating system kernel. It strips away all abstractions, all library sugar-coating, and reveals the raw requests a program makes to hardware resources. This is the level where:
- malware evades EDR/AV hooks
- packers manipulate memory protections
- rootkits establish persistence
- anti-debugging tricks operate
through studying samples, CTF challenges and binaries, I’ve developed a methodology that I’m going to share with you rn. This isn’t just theory, I’ll walk you through real binaries, show you the exact commands I use and reveal the tricks that have saved me countless hours in the lab.
2. The Foundation: Understanding System Calls
What exactly is a System Call?
A system call(syscall) is a programmatic way for a user-space application to request a service from the kernel. Think of it as the ultimate API, the boundary between user mode and kernel mode.
The Anatomy of a Syscall: Every syscall consists of three components:
- The syscall number - a unique identifier for the operation
- The arguments - parameters passed to the kernel
- The return value - the kernel’s response
The Syscall Journey
Let me trace what happens when you call write() in C:
write(fd, buf, count) ↓libc wrapper (glibc) ↓Assembly: mov rax, 0x1 (syscall number) mov rdi, fd mov rsi, buf mov rdx, count syscall (trap to kernel) ↓Kernel: sys_write() handler ↓Return to user mode with resultWhy Direct Syscalls Matter: Most programs use libc wrappers. But sophisticated code, especially malware, bypasses these wrappers entirely:
; direct syscall - no libc dependencymov rax, 1mov rdi, 1lea rsi, [message]mov rdx, 12syscallThis approach:
- Eliminates library dependencies
- Avoids function hooking
- Reduces binary size
- Increases stealth
The Syscall Tables
Linux x86-64 Syscall Numbers:
0 = sys_read1 = sys_write2 = sys_open3 = sys_close9 = sys_mmap12 = sys_brk83 = sys_mkdir59 = sys_execve60 = sys_exitcomplete table location: /usr/include/x86_64-linux-gnu/asm/unistd_64.h
Windows Syscall Numbers (Dynamic):
Unlike Linux, Windows syscall numbers change between versions. This is why malware often dynamically resolves them at runtime.
Windows 10 1903: NtCreateFile = 0x55Windows 10 2004: NtCreateFile = 0x50Windows 11: NtCreateFile = 0x52This dynamic nature makes Windows reversing particularly challenging, you must know the exact OS version you’re analyzing.
3. Linux vs Windows: The Architectural Differences
Linux Architecture
Syscall Invocation Methods:
syscallinstruction(x86-64) - modern, fasterint 0x80(x86) - legacy, slowervsyscall/vDSO- special fast-path for certain calls
| Register | Purpose |
|---|---|
| RAX | Syscall number / Return value |
| RDI | Argument 1 |
| RSI | Argument 2 |
| RDX | Argument 3 |
| R10 | Argument 4 |
| R8 | Argument 5 |
| R9 | Argument 6 |
Here’s an example - sys_execve:
mov rax, 59 ; sys_execvemov rdi, path ; filenamemov rsi, argv ; argv arraymov rdx, envp ; envp arraysyscallWindows Architecture
Syscall Invocation Methods:
syscall(x64) - primary methodsysenter(x86) - fast system callint 2e(x86) - legacy interrupt
Register Convention(x64):
| Register | Purpose |
|---|---|
| EAX | Syscall number |
| RCX | Return address (populated by kernel) |
| R10 | original RCX (first argument) |
| RDX | argument 2 |
| R8 | argument 3 |
| R9 | argument 4 |
| Stack | additional arguments |
Example - NtWriteFile:
mov eax, 0x50 ; Syscall number (Windows 10 2004)mov r10, rcx ; Save first argumentmov rdx, Handle ; File handlemov r8, Event ; Eventmov r9, ApcRoutine ; APC routine; Additional args on stacksyscallKey Differences for Reversing
| Aspect | Linux | Windows |
|---|---|---|
| Syscall stability | Stable across versions | Changes per build |
| Number source | Static headers | Dynamic resolution needed |
| Wrapper layer | glibc | ntdll.dll |
| Debugging tools | strace, gdb | process monitor, WinDbg |
| anti-debug techniques | ptrace checks | NtQueryInformationProcess |
4. Essential Tools for the Journey
Over time, I have curated a toolkit that works for every syscall reversing scenario. Here’s what I use:
Linux Tools
- strace - the syscall tracer
Terminal window strace -e trace=file -o syscalls.log ./binary
my go-to for initial analysis. The -e flag filters specific syscall categories.
- ltrace - library call tracer
Terminal window ltrace -e printf,write ./binary
essential for differentiating library calls from raw syscalls.
- GDB with Syscall Breakpoints
(gdb) catch syscall(gdb) catch syscall execve(gdb) info syscallsI use this for deep inspection of syscall arguments
- objdump - static analysis
Terminal window objdump -d -M intel binary | grep -A 10 syscall
find all syscall instructions in the binary
- sysdig - kernel level tracing
Terminal window sysdig proc.name=binary
for when you need kernel-level visibility.
Linux Deep Dive: step by step analysis
let me walk you through a real analysis(demonstration). It’s for a suspicious binary that was bypassing a file-monitoring agent
Phase 1: Initial reconnaissance file information:
$ file suspicious_binarysuspicious_binary: ELF 64-bit LSB executable, x86-64, statically linkedstatically linked means no external libc. Likely using direct syscalls
String analysis:
$ strings suspicious_binary | grep -i syscall# No outputPhase 2: Dynamic tracing with strace
$ strace -o full_trace.log ./suspicious_binary$ head -20 full_trace.logoutput:
execve("./suspicious_binary", ["./suspicious_binary"], 0x7ffe... ) = 0arch_prctl(ARCH_SET_FS, 0xdeadbeef...) = 0mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f...mprotect(0xdead... , 0x1000, PROT_READ|PROT_EXEC) = 0write(1, "Decrypting...\n", 14) = 14openat(AT_FDCWD, "/tmp/.cache", O_RDWR|O_CREAT, 0644) = 3read(3, "..." , 0x1000) = 0x1000write(3, "..." , 0x2000) = 0x2000close(3) = 0ptrace(PTRACE_TRACEME, 0, 0, 0) = -1 EPERM (Operation not permitted)Red flags:
arch_prctl- setting up thread-local storage which is suspicious for a simple binarymprotectwithPROT_EXEC- likely self modifying codeopenatin/tmp/.cache- hidden file operationsptracecall - anti-debugging attempt
Phase 3: Focusing on suspicious syscalls
$ strace -e trace=open, openat, read, write, mmap, mprotect, ptrace ./suspicious_binaryDetailed analysis of openat:
openat(AT_FDCWD, "/tmp/.cache", O_RDWR|O_CREAT, 0644) = 3the file descriptor 3 is returned. Let’s see what happens to it.
Phase 4: Deep inspection with GDB
$ gdb ./suspicious_binary(gdb) catch syscall openatCatchpoint 1 (syscall 'openat')(gdb) catch syscall mmapCatchpoint 2 (syscall 'mmap')(gdb) runBreakpoint hit - openat:
Catchpoint 1 (call to syscall openat), 0x0000000000400c50(gdb) info registersrax 0x101 257 ; syscall number for openatrdi 0xffffffffffffff9c ; AT_FDCWD (-100)rsi 0x600d20 "/tmp/.cache"rdx 0x42 66 ; O_RDWR|O_CREATr10 0x1b6 438 ; 0666? Actually 438 decimal(gdb) x/s $rsi0x600d20: "/tmp/.cache"next breakpoint - mmap:
Catchpoint 2 (call to syscall mmap), 0x0000000000400c80(gdb) info registersrax 0x9 9 ; sys_mmaprdi 0x0 NULL ; Let kernel choose addressrsi 0x1000 4096 ; 1 pagerdx 0x3 PROT_READ|PROT_WRITEr10 0x22 MAP_PRIVATE|MAP_ANONYMOUSr8 0xffffffffffffffff ; -1 (no file backing)r9 0x0 0 ; offsetfrom the text, the programming is mmapping anonymous memory, then presumably writing decrypted code there.
Phase 5: Static Analysis with objdump
$ objdump -d -M intel suspicious_binary | grep -B5 -A5 syscallsnippet:
400c50: b8 01 01 00 00 mov eax,0x101 400c55: bf 9c ff ff ff mov edi,0xffffff9c 400c5a: 48 be 20 0d 60 00 00 movabs rsi,0x600d20 400c61: 00 00 00 400c64: ba 42 00 00 00 mov edx,0x42 400c69: 0f 05 syscallkey observation: the binary uses mov eax, 0x257 instead of mov rax, 0x257 - this is common optimization. The syscall number 0x257 = 599
Phase 6: Building the execution flow Using the trace and assembly, I mapped the execution:
- write “Decrypting…” to stdout
- open
/tmp/.cachefor reading/writing - read encrypted data from the file
- mmap anonymous memory
- decrypt data (custom algorithm based on XOR)
- write decrypted data to mmap region
- mprotect region to PROT_EXEC
- jump to decrypted code
- ptrace check (anti-debug)
- payload execution
Phase 7: bypassing anti-debug The ptrace anti-debug was simple:
mov rax, 101 ; ptracemov rdi, 0 ; PTRACE_TRACEMEsyscalltest rax, raxjnz exitbypass in GDB:
(gdb) catch syscall ptrace(gdb) commands> set rax = 0> continue> endPhase 8: Extracting the Payload at the mmap breakpoint:
(gdb) x/100xb $rax ; After mmap returns0x7ffff7ff0000: 0x90 0x90 0x90 0x90 0x48 0xb8 0x2f 0x62...I dumped the payload:
(gdb) dump memory payload.bin 0x7ffff7ff0000 0x7ffff7ff1000What we learned: The binary was a downloader that:
- checked for debuggers
- decrypted a second-stage payload
- executed it from memory(fileless) this is an example why syscall-level analysis is critical. The high-level API calls would have shown nothing.
Next reads
View all →26 Jun
Mastering Syscall-Level Reversing pt2; The Art of Unhooking & Direct Syscalls
The hook is the problem
9 Sept
Mini-Readelf: Gluing It All Together
The capstone. Four parts of pieces, headers, sections, symbols, relocations, joined into one tool that reads any ELF. The only new mechanic is the sh_link chain: offset into a table that holds offsets into a table that holds strings.
5 Sept
Relocations: How PIE Binaries Fix Their Addresses
A PIE binary can't write final addresses because ASLR moves it. The linker leaves placeholders and the loader patches them after mapping. That's a relocation: R_X86_64_RELATIVE, GLOB_DAT and JUMP_SLOT.
Get posts by email
One email when I publish, not a drip, not weekly. Sign up and I'll only write when there's something new.
You won't get mail just for signing up. Unsubscribe any time.