skip to content

Address-space randomisation on a pre-forking daemon: why does a crashed worker help the attacker?

level: seniorimportance: nice to knowfreq 32%

answer

  1. the draw happens at load, not at fork
  2. children inherit mappings and the cookie
  3. a dead worker costs the attacker nothing
  4. crash or survive is one bit of information
  5. re-loading the image restores the arithmetic

basics

~20 s

Fork copies the parent's address space, so every worker shares one randomisation draw and one stack cookie value. A dead worker is replaced by an identical twin, so the attacker retries against the same secret and can learn it byte by byte.

solid answer

~50 s

Randomisation is drawn when a program image is loaded, not when a process is created. A pre-forking daemon loads once, then serves connections by forking children that inherit the parent's mappings unchanged - same library base, same heap base, and the same stack-guard value, which is seeded once per image and copied along with everything else. The worker pool is therefore one draw replicated, not many independent draws. When a worker dies on a wrong guess and the parent forks a replacement, the attacker has lost nothing and learned something: crash versus survival becomes a read primitive with no disclosure defect at all. Guess one byte of the guard at a time and keep the value that does not kill the child - a few hundred attempts per byte instead of the full search space. If each worker is instead created by loading the image afresh, the kernel draws again and the method collapses.

code

text · 6 lines
text
master   pid 1041   libc base 0x7f2c9a400000   stack guard 0x8f3ab1c4e5d20900
worker   pid 1092   libc base 0x7f2c9a400000   stack guard 0x8f3ab1c4e5d20900
worker   pid 1093   libc base 0x7f2c9a400000   stack guard 0x8f3ab1c4e5d20900
...
worker   pid 1140   libc base 0x7f2c9a400000   stack guard 0x8f3ab1c4e5d20900
         (forked by master after worker 1092 died on a fault)

go deeper

for a junior

Know that address randomisation happens when a program image is loaded, and that copying an existing process does not repeat it, so parent and child sit at the same addresses.

for a middle

Explain what a forked child inherits - mappings, bases and the stack-guard value - and why that makes many workers one secret rather than many.

for a senior

Show the operational consequence: free retries turn crash-versus-survival into a byte-at-a-time read, and name the fix (load the image per worker) along with the process-creation cost it buys with.

for a principal

Be able to tell an owner that the mitigation's value is set by deployment shape rather than by build flags, and price the worker-creation change against the rewrite it might defer.

## Where the draw actually happens The randomised bases are chosen when a program image is loaded into a fresh address space. Creating a process by copying an existing one does not load anything: the child receives a copy-on-write duplicate of the parent's mappings, at the same addresses, with the same permissions. Only replacing the image - loading the executable again in that process - causes a new draw. That one implementation detail decides how much protection a long-lived service actually gets, and it is invisible in any datasheet that says the mitigation is enabled. ## The pre-forking daemon A classic memory-unsafe network daemon starts a parent, loads its libraries, then forks a pool of workers. When a worker exits - cleanly, or by dying on a fault - the parent forks another from itself. Every worker in the pool, and every worker created for the rest of the daemon's uptime, has the identical layout that was drawn the day the service last started. The stack-guard value behaves the same way. It is generated once per process image from randomness supplied by the kernel at load, kept where the running code can find it cheaply, and copied into every child. All workers share one cookie. ## What that hands the attacker Three things, and the third is the one people miss. 1. **Retries are free.** A wrong address costs one worker, and a fresh one appears. Guessing that would be hopeless against a single-shot target becomes a matter of patience against a service designed to survive worker death. 2. **A leak in one interaction is usable in another.** Disclose an address through one connection and use it in a later one against a different worker, because both share the draw. 3. **Crash versus survival is itself an oracle.** With no disclosure defect at all, the attacker overwrites the cookie one byte at a time: try a byte value, and if the child dies the guess was wrong, and if it lives the guess was right. Eight bytes at a couple of hundred tries each is a few thousand connections, not an astronomical number. The same technique then walks memory to locate usable instruction fragments. This is the published blind return-oriented method, and the only precondition it needs is that the replacement child has the parent's layout. ## The condition that breaks it If a worker is created by loading the image afresh rather than by copying the parent, each worker gets its own draw and its own cookie. Then a wrong guess costs the attacker not one worker but the entire accumulated knowledge, and the arithmetic returns to what the mitigation's datasheet implied. That is the concrete design change available here, and it is cheap compared with a rewrite: pay a process-creation cost per worker in exchange for a fresh draw per attempt. Two weaker measures are worth naming honestly. Restarting the parent periodically re-draws for everyone, which shortens the window an attacker has to work within but does not make an individual campaign expensive. Limiting how fast replacements are created raises the wall-clock cost of a byte-at-a-time search without changing its feasibility - useful against somebody with a weekend, irrelevant against somebody with a month. ## The general lesson, which is the reason this leaf exists A mitigation is a price, and the price is set by the *deployment*, not by the compiler flag. The same binary, with the same flags, is priced very differently depending on whether workers are copies or fresh loads. When someone reports that a mitigation is enabled across the estate, the follow-up question is what an attempt costs the attacker and whether a failed attempt costs them anything at all. On a pre-forking daemon the answer is: almost nothing, which is why the two adversaries in the same position get different outcomes. Someone running a public proof of concept written for a different build still just kills workers. Someone willing to spend weeks turns free retries into the address secret the exploit needs.

  • What design change removes this, and what does it cost?
    Create each worker by loading the image afresh instead of copying the parent, so the kernel draws new bases and a new guard value per worker. The cost is process-creation overhead on every worker, which for a connection-per-worker service can be significant; the gain is that a failed attempt destroys everything the attacker has learned.
  • Why is a stack cookie so much weaker on this deployment than the description suggests?
    The cookie's strength is that its value is unknown and a wrong value is fatal. On a forking daemon it is unknown but constant across all children, and a fatal guess costs one child, so the attacker can pay for the value one byte at a time rather than guessing the whole word at once.
  • Does restarting the daemon nightly solve it?
    It bounds the window rather than the campaign. Every restart draws fresh bases and a fresh guard, so knowledge learned before the restart is void, but nothing stops the same byte-at-a-time work being repeated inside the next window if that window is long enough for a few thousand connections.

A combination lock that resets only when the shop opens: if you may try one digit at a time and the door is rehung after each failure, the number of possible combinations stops being the thing protecting it.

saying these in an interview costs you the question

  • Assumes each new process always gets a fresh randomisation draw
  • Thinks a stack cookie differs between forked children
  • Believes a crash costs the attacker the attempt budget
  • Says brute force is infeasible because the address space is 64-bit
  • Cannot name a deployment change short of a rewrite

context