← Back to Writeups
CUSTOMEasyForensics

Forenser RBP

XESXOR9/2/20265 min read
#forensics#custom#easy#frame_pointer_traversal#rbp_chain_walking#stack_dump_analysis#va_range_from_filename

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
push 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 main itself. The RBP value at that frame is the answer, because that is precisely the RBP that the callee of main pushed 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
#!/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=REDACTED is main: its saved RBP 0x402030 is inside the binary image (not the stack — classic CRT sentinel), and its return address lands in libc (__libc_start_call_main).
  • Frame [2] rbp=0x7ffc1efd65a0 is the function called directly by main: it pushed main's RBP = REDACTED, and its return address 0x401f39 is inside main.
  • 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. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍