Forenser RBP
Forenser RBP
Platform: spbctf | Category: Forensics | Type: Challenge | Difficulty: Easy | OS: NA | Author: XESXOR | Date: 2026-04-18 | Status: Solved Techniques: frame_pointer_traversal, rbp_chain_walking, stack_dump_analysis, va_range_from_filename
Summary
Task: raw process stack dump (0x21000 bytes) from /proc/PID/maps region 0x7ffc1efb8000-0x7ffc1efd9000, find saved RBP of the function that returns to main. Solution: walk the x86_64 RBP linked-list chain (push rbp; mov rbp, rsp) and take the last saved RBP still pointing inside the stack region — that is main's RBP.
Forenser RBP — spbctf (pwn.spbctf.ru canary series)
Description
Hi! Take this file: pid-4018-7ffc1efb8000-7ffc1efd9000 It's a dump of a process's stack. What's the saved RBP value of the function that will return to main? It will be the last saved RBP that points inside stack.
English summary: We get a raw memory dump named pid-4018-7ffc1efb8000-7ffc1efd9000 (135168 bytes = 0x21000). The filename encodes the PID and the virtual address range of the mapping — this is a full copy of the process's [stack] region. We must identify the saved RBP of the function directly called by main, i.e. the value main pushed on entry. The hint states it is the last saved RBP in the frame-pointer chain that still lies inside the stack region.
Analysis
Filename convention
The filename pid-4018-7ffc1efb8000-7ffc1efd9000 follows the familiar /proc/<pid>/maps dumping pattern:
base = 0x7ffc1efb8000
end = 0x7ffc1efd9000
size = end - base = 0x21000 = 135168 bytes # matches file size exactly
So offset 0 in the file corresponds to virtual address base, and any 8-byte aligned qword whose value lies in [base, end) is a candidate stack pointer.
x86_64 frame pointer chain
With frame pointers enabled, each function prologue executes:
asm read-only • copy allowedpush rbp mov rbp, rsp
This creates a singly-linked list on the stack:
[current rbp] -> saved caller rbp
[current rbp+8] -> return address
Walking the chain upward (addresses grow, since stack grows downward and RBP values of outer frames are higher), we can reconstruct the call stack. The chain terminates when saved RBP:
- points outside the stack region (e.g. into
.data/.bss— the classic marker left by_start/__libc_start_main), or - is
0/ garbage.
Identifying main
main's return address points into libc (__libc_start_call_main).main's saved RBP is a sentinel planted by the CRT — typically a pointer into the binary image (not inside the stack).- Therefore, the last frame in the chain whose saved RBP still lies inside the stack is
mainitself. The RBP value at that frame is the answer, because that is precisely the RBP that the callee ofmainpushed on entry.
Solution
Step 1 — Confirm the VA range
file size = 0x21000 bytes
= end - base = 0x7ffc1efd9000 - 0x7ffc1efb8000
So the dump is the entire stack mapping, file-offset ↔ VA mapping is linear and direct.
Step 2 — Collect candidate RBP values
Scan every 8-byte aligned qword in the dump. Keep the ones whose value falls inside [base, end) and is itself 8-byte aligned — these are the potential frame pointers.
Step 3 — Walk every candidate as a chain
Starting from each candidate, follow rbp -> *(rbp) repeatedly while the next pointer is still inside the stack region and is strictly greater than the current address (RBP chain strictly grows in x86_64). Take the longest chain — that is the real call stack.
Step 4 — Read the answer
Print each frame with its saved_rbp and ret (qword at rbp+8). The frame whose return address points into main is the callee-of-main; its saved RBP value is what main pushed on entry — the answer.
Full working script
python read-only • copy allowed#!/usr/bin/env python3 import struct base = 0x7ffc1efb8000 end = 0x7ffc1efd9000 size = end - base data = open('stack.bin', 'rb').read() assert len(data) == size, f"expected {size:#x} bytes, got {len(data):#x}" def rq(a): """Read qword at virtual address a, or None if out of range.""" o = a - base if 0 <= o <= size - 8: return struct.unpack('<Q', data[o:o+8])[0] return None # 1. Gather all qwords that look like pointers into the stack. cand_vals = set() for off in range(0, size, 8): v, = struct.unpack('<Q', data[off:off+8]) if base <= v < end and v % 8 == 0: cand_vals.add(v) def walk(start): """Follow the saved-RBP chain starting at `start`.""" chain = [start] cur = start seen = {cur} while True: nxt = rq(cur) if nxt is None or not (base <= nxt < end): return chain, nxt # chain exits the stack region if nxt in seen or nxt <= cur: # cycle or non-monotonic -> bogus return chain, nxt seen.add(nxt) chain.append(nxt) cur = nxt # 2. Longest chain wins — that's the real RBP linked list. chain, exit_val = max((walk(s) for s in cand_vals), key=lambda x: len(x[0])) for a in chain: print(f"rbp=0x{a:x} saved_rbp=0x{rq(a):x} ret=0x{rq(a+8):x}") print(f"chain exits with saved_rbp=0x{exit_val:x}") # The last RBP in the chain is main's RBP == saved RBP pushed by main's callee. print(f"\nAnswer: 0x{chain[-1]:x}")
Output
rbp=0x7ffc1efd63e0 saved_rbp=0x7ffc1efd6550 ret=0x7f592ca6e980 # libc frame
rbp=0x7ffc1efd6550 saved_rbp=0x7ffc1efd65a0 ret=0x401616 # binary
rbp=0x7ffc1efd65a0 saved_rbp=REDACTED ret=0x401f39 # binary -> returns into main
rbp=REDACTED saved_rbp=0x402030 ret=0x7f592c8d6cca # main -> __libc_start_call_main
Frame interpretation:
- Frame [3]
rbp=REDACTEDismain: its saved RBP0x402030is inside the binary image (not the stack — classic CRT sentinel), and its return address lands in libc (__libc_start_call_main). - Frame [2]
rbp=0x7ffc1efd65a0is the function called directly bymain: it pushedmain's RBP =REDACTED, and its return address0x401f39is insidemain. - The saved RBP value sitting in frame [2] (i.e. the last RBP in the chain that still points inside the stack) is the answer.