When you type a password into an SSH or sudo prompt, you assume it goes to one place: the program that asked for it. On Linux that assumption is wrong by one word. The password goes to that program's memory, and on a shared kernel, memory is something other processes can be granted the right to read. Matthew Keeley's writeup of Hawk, a credential harvester built for a collegiate red-team competition, is a clean, small demonstration of exactly that gap. It is worth understanding not because the tool is novel, but because the boundary it crosses is one most people draw in the wrong place.

What this is, and isn't

This is a defensive read of a public red-team tool. The technique is post-exploitation: it assumes the attacker is already root on the box. The interesting part is not how to build it, it is what it tells you about where the real trust boundary sits and how to watch the line. Nothing here is a how-to.

What Hawk does

Hawk is a small Go program. It watches a host for anyone in the middle of authenticating, grabs the plaintext credential as it is typed, and ships it to a server. During the Western Regional Collegiate Cyber Defense Competition it fed those credentials straight into a Sliver C2, so a password typed on one machine became a foothold on the next. Three moving parts, and that is the whole loop:

scan /proc, 250ms sshd sudo su / passwd ptrace attach watch write() to the terminal, peek the buffer user + pass plaintext C2 HTTP(S) the credential is read from a live process, not from a file or a hash
Hawk polls /proc for authentication processes, attaches to one with ptrace, reads the typed credential out of its memory as it crosses a write syscall, and exfiltrates the plaintext. The harvest happens in the window between keystroke and hash.

Why reading another process's memory is even possible

The whole thing rests on ptrace, the same kernel facility a debugger uses to step through a program and inspect its registers and memory. A tracer attaches to a target, the kernel stops the target at each syscall boundary, and the tracer can read the target's registers and peek bytes out of its address space. That is not a bug. It is how gdb and strace work.

Hawk uses it the obvious way. When a login process makes a write to its terminal, the syscall arguments are sitting in the CPU registers, and the buffer they point at is sitting in the target's memory. The tracer reads them:

RegisterHoldsWhat Hawk does with it
orig_raxThe syscall numberMatch 1 = write
rdiArg 1: file descriptorMatch the terminal fd
rsiArg 2: buffer pointerPeek the bytes at this address
rdxArg 3: byte countHow much to read

It identifies who is logging in by reading /proc/[pid]/cmdline (an sshd session line carries the username), defaults the rest to root, and filters the captured bytes with crude heuristics on length and printability to throw away the noise and keep the credential. None of this is sophisticated. That is the point. The capability it is using is ordinary and built in.

The boundary you actually have

Most people carry a mental model that looks like the first row below. The real one is the second.

The boundary you imagineThe boundary you have
My password goes to sshd, and nothing else sees itMy password lands in sshd's memory, and anything with the right to trace sshd sees it too

This is the same shape I wrote about in agent memory is an attack surface: the trust boundary you assume is not the trust boundary that exists, and an attacker works in the gap between them. There the gap was a memory store that treated attacker text as a believed fact. Here it is a terminal write that treats the kernel's debugging facility as if it were private.

What actually has to be true for this to work

This is where the defensive read diverges from the red-team writeup, which never says it. Hawk is not a way in. It is a way to stay in and spread. For it to read another user's sshd or a root sudo, the attacker needs the privilege to attach a tracer to a process they do not own, and on Linux that means CAP_SYS_PTRACE, which in practice means root.

The hawk already has the high ground. The tool does not take the box; it harvests once the box is taken. What it buys an attacker is quiet, durable lateral movement: fresh plaintext credentials, not hashes, that survive a password rotation and unlock the next host.

That reframes the defence completely. If you are arguing "but they would need root," you have conceded the premise, not refuted it. The questions that matter are the next three: can a non-root user do this at all, can you see it when it happens, and is the stolen credential worth anything once it leaves.

How to defend the line

Three layers, in the order of how much they actually buy you against this specific behaviour.

LayerWhat it doesWhat it does not do
Restrict ptraceSet kernel.yama.ptrace_scope to 1 (default on many distros), 2, or 3. Stops a non-root process from tracing one it didn't spawn.Does not stop root. CAP_SYS_PTRACE bypasses Yama. Helps against an unprivileged foothold, not a privileged one.
Detect the behaviourAudit the ptrace syscall (auditd or eBPF), and watch for a non-zero TracerPid in /proc/[pid]/status on sshd, sudo, su. Flag any process polling /proc at a tight interval.Does not prevent the read. It tells you it happened, which against a root attacker is often the most you get.
Defang the lootMake a stolen password worthless: SSH keys or hardware-backed certificates plus MFA, short-lived per-host credentials, just-in-time access. A plaintext password that cannot be replayed is a non-event.Does not stop the capture. It removes the reason to bother and breaks the lateral-movement loop the harvest feeds.

The order is deliberate. Restricting ptrace is the cheap hygiene that closes the unprivileged case, and you should do it. Detection is what you actually rely on against a root attacker, because at that level prevention has mostly already failed. And defanging the loot is the only layer that changes the economics: it is the difference between an incident where an attacker walked the network on harvested passwords and one where they captured a string that opened nothing.

The takeaway

Hawk is interesting precisely because it is boring. It chains together a polling loop, a built-in kernel facility, and an HTTP request, and it works because the trust boundary people imagine around a password prompt is one word off from the real one. The defences follow from naming the real boundary out loud: a typed secret is briefly plaintext in a process's memory, the kernel can hand that memory to a sufficiently privileged tracer, and so the secret is only ever as safe as your control over who reaches root and your ability to make the secret useless once it is taken.

If you defend by hoping nobody gets the privilege, you are defending the boundary you imagined. Defend the one you have.

When you type a password into an SSH or sudo prompt, it goes to that program's memory. On a shared Linux kernel, memory is something another process can be granted the right to read. Hawk, a credential harvester built for a collegiate red-team competition, does exactly that: a clean demonstration of a boundary most people draw one word too far out.

What Hawk does

Hawk is a small Go program with three moving parts:

  • poll /proc every 250ms for a login process: sshd, sudo, su, passwd;
  • attach with ptrace and read the typed bytes from the buffer its write syscalls point at;
  • ship the plaintext user and password to a C2 server over HTTP.

It fed those credentials into a Sliver C2, so a password typed on one box became a foothold on the next. It reads the username from /proc/[pid]/cmdline and filters captured bytes by length and printability. None of it is sophisticated: that is the point.

Why it is even possible

ptrace is the facility a debugger uses: it attaches to a target and reads its registers and memory while the kernel stops it at each syscall. That is how gdb and strace work. On a write to the terminal the buffer pointer sits in a register and the bytes in memory, so Hawk reads them between keystroke and hash.

The boundary you imagine is wrong. You assume the password goes to sshd and nothing else sees it; really it lands in memory that any process with the right to trace sshd can read.

The precondition the writeup omits

To trace another user's sshd or a root sudo, the attacker needs CAP_SYS_PTRACE: in practice, root. Hawk is not a way in but a way to stay in and spread, buying quiet lateral movement: fresh plaintext passwords, not hashes, that survive a rotation. "But they would need root" concedes the premise, it does not refute it.

The tool does not take the box. It harvests once the box is taken.

How to defend the line

  • Restrict ptrace. Set kernel.yama.ptrace_scope to 1 or higher. Closes the unprivileged case; does not stop root, which bypasses Yama.
  • Detect the behaviour. Audit the ptrace syscall, watch for a non-zero TracerPid on sshd/sudo/su, and flag tight /proc polling. Against root, usually the most you get.
  • Defang the loot. Hardware-backed certificates plus MFA, short-lived per-host credentials, just-in-time access. A password that cannot be replayed is a non-event, and only this layer breaks the lateral-movement loop.

The kernel hands a briefly plaintext secret to a privileged tracer, so it is only as safe as your grip on root. Defend the boundary you have, not the one you imagined.


Sources