{{ course.notes }} · Lecture 4 · Non-Control-Flow
Instructors: {{ course.instructors }}

Non-Control-Flow Attacks

Winning without ever hijacking control flow, by corrupting, or merely reading, ordinary data. Data-oriented attacks today; more on the way.

Builds on  Spatial Memory Errors Prereq  C, some C++ Time  ~35 min
Part · A different threat model

Non-Control-Flow Attacks

Everything so far (overflows, ROP, bad casts) ultimately hijacked control flow. But an attacker can win without ever changing where the program jumps, by corrupting (or merely reading) ordinary data.

Non-control-flow attack

Data-oriented attacks

Ask a sharper question: do you actually need to divert control flow to win? Often the answer is no. A data-oriented attack corrupts ordinary non-control data and leaves the control flow completely intact.

Control data
Return addresses, function pointers, vtables: they decide where execution goes. What control-flow hijacking targets.
Non-control data
A permission flag, a user ID, a length, a filename: never executed, but corrupting it can be just as devastating.

The state of the art is to corrupt security-critical data, leave control flow unchanged, and still do significant damage. A classic example is the Internet Explorer SafeMode bypass {{ cite.yu_blackhat }}. In IE, JScript runs in a locked-down mode called SafeMode, the guard that forbids web-page scripts from reaching dangerous capabilities such as the WScript.Shell object. Any web page you visit runs untrusted JavaScript in your browser; WScript.Shell would let that page's script run operating-system commands on your machine (e.g. launch calc.exe, or download and run malware), so SafeMode normally blocks it. Whether that guard is up is stored as a single flag, safemode, at a fixed offset inside the JScript object:

// jsobj + 0x188 = the SafeMode flag (JScript 5.8)
safemode = *(DWORD *)(jsobj + 0x188);   // read the guard flag
if ( (safemode & 0xB) == 0 ) {          // guard DOWN when the flag is 0 ← the check
    Turn_on_God_Mode();                 // dangerous COM objects now allowed
}

The guard is that if on line 3: normally safemode is non-zero, the test fails, and the privileged path stays closed. The attack never touches this code. Instead, using an arbitrary-write primitive it simply zeroes the safemode field in memory. The next time IE's own, unmodified code runs this check the test now passes, the guard is down, and WScript.Shell becomes available to run arbitrary commands: no control-flow hijack, no ROP. Another canonical case: the wu-ftpd seteuid bug, where corrupting a stored user-ID gives root without touching any code pointer.

You may not need to corrupt anything at all

Push the idea further. Some attacks just read memory they shouldn't. Heartbleed (2014) is the poster child {{ cite.openssl_advisory }} {{ cite.heartbleed_site }}.

The setting. OpenSSL is the open-source library that most web servers use to speak TLS/HTTPS, the encryption behind the padlock in your browser. When a client (your browser) and a server hold a TLS connection open, either side can send a heartbeat: a small "are you still there?" message. You send a short piece of data plus a number saying how long that data is; the other end simply echoes the same data back. The bug: The OpenSSL code when parsing the HTTP data trusted the claimed length received over the Internet, instead of measuring the data it actually received. The result is that an unauthorised client can leak secrets from the server, such the SSL cryptographic keys.

The cartoon below is the intuition. Imagine the client sends a 4-letter word but claims it's 500 letters, and the server dutifully copies 500 bytes into its reply: your 4 letters plus 496 bytes of whatever sat next to them in server memory (session cookies, passwords, even the private key). No corruption, no control-flow change; just a missing bounds check on a read.

Cartoon contrasting a normal Heartbeat request with a malicious one that lies about its length
A cartoon of Heartbleed: the “give me 500 letters” analogy, not the real code. The actual bug is the few lines of OpenSSL shown below. Source: Heartbleed, Wikipedia.

The real bug: a missing bounds check

Stripped of the cartoon, here is the actual vulnerable function (tls1_process_heartbeat / dtls1_process_heartbeat, OpenSSL 1.0.1). A heartbeat request carries a type, a 16-bit length, and a payload, and the length is taken straight from the attacker:

/* OpenSSL 1.0.1 · ssl/t1_lib.c → tls1_process_heartbeat() */
unsigned char *p = &s->s3->rrec.data[0], *pl;
unsigned short hbtype;
unsigned int   payload;                /* length the request claims */

hbtype = *p++;                         /* 1. read message type      */
n2s(p, payload);   /* 2. payload = length the CLIENT claims  ← attacker-controlled */
pl = p;                                /* 3. pl → real payload bytes */

buffer = OPENSSL_malloc(1 + 2 + payload + padding);
bp = buffer;
*bp++ = TLS1_HB_RESPONSE;
s2n(payload, bp);
memcpy(bp, pl, payload);   /* BUG: copies 'payload' bytes, but never checks
                             the request actually held that many */

Nothing compares payload against rrec.length, the number of bytes actually received. So memcpy reads up to 65,535 bytes starting at pl, walking off the end of the real request into adjacent heap memory (private keys, session cookies, passwords) and ships it back to the attacker in the response. A buffer over-read, no write required.

The fix (OpenSSL 1.0.1g) is a single bounds check: reject any request whose claimed length exceeds the bytes actually received.

hbtype = *p++;
n2s(p, payload);
if (1 + 2 + payload + 16 > s->s3->rrec.length)
    return 0;              /* claimed length > bytes received → discard */
pl = p;
More non-control-flow attacks

Coming soon

Two deeper topics will join this part once the slides are ready:

Coming soon
Data-Oriented Programming (DOP)

Chaining non-control-data corruptions into a Turing-complete computation {{ cite.dop }}, the data-only analogue of ROP.

Coming soon
Bad-Casting, in depth

A deeper look at bad-casting beyond the basics: real-world exploits and how tools detect them.

End of section

Non-control-flow attacks, in brief

  • You can win without hijacking control flow: corrupt non-control data (a flag, an ID, a length) and the program's own code does the damage {{ cite.chen_noncontrol }}.
  • Sometimes you needn't corrupt anything at all: a missing bounds check on a read (Heartbleed) leaks secrets outright.
  • Because control flow never changes, control-flow defences never fire.
  • Still to come: Data-Oriented Programming (DOP) and a deeper look at bad-casting.

{{ course.notes }} · Instructors: {{ course.instructors }}

05 · Sources

References

[{{ r.n }}] {{ r.cite }} ↗ {{ r.linkLabel }}

Links last checked July 2026.