You are on page 1of 80

Unwinding The Stack

For Fun and Profit


Victor M. Duta Fabian Freyer
Fabio Pagani Marius Muench Cristiano Giuffrida
Binary Exploitation
Control Flow Hijacking
Stack
int main() {
callme();
// more things
}
main + 5

void callme() {
// do things
}
Stack
int main() {
callme();
// more things
}
0x41414141

void callme() {
// overflow
}
Stack
int main() {
callme();
// more things
}
main + 5

void callme() {
// overflow

}
Stack
int main() {
callme();
// more things
}
main + 5

void callme() { random cookie


// overflow
// check canary
}
Stack
int main() {
callme();
// more things
}
main + 5

void callme() { 0xdeadbeef


// overflow
// check canary
}
return A return A
return B
return C
return B

Shadow Stack
return C
Backward edge Forward edge
Control-flow Control-flow
Hijack Hijack
handler_1

SEH (Windows)
return
Single Linked List
handler_2 of pointers to handlers

return Called when Exception is thrown

handler_3
handler_1

SEH (Windows)
return
Single Linked List
handler_2 of pointers to handlers

return Called when Exception is thrown

evil
handler_1

SafeSEH
return
Allowed List of Handlers
handler_2
● handler_1
● handler_2
return ● handler_3
evil not in list!
evil
Stack Buffer
Overflows
are a solved Any questions?

problem!
Thank you for listening!
Are there any exceptions
to these mitigations?
Yes!
CHOP
Control Flow Hijacking
by confusing the unwinder
In a nutshell
unwinder
😕 compilers

CH
OP
defenses

handler evil
NDSS 2023

RG O
M B A 02 0 23

E J a n 1
Roadmap
Exploitation
Techniques

General Stats
Idea

Real-World
Exploits
Unwinding
void foo() { Stack
try {
bar();
}
catch (...) {
// handle errors
} foo + 5
} unwinding

void bar() {
throw new Exception();
}
Exception Tables

Landing
Call Site Exception Metadata
Pad

0x1000..0x1004 * 0x1042

0x2000..0x2040 runtime_error 0x2080


0x1002

0x2030

Stack with 3 stack frames

0x3042

throw
bad_alloc;
0x1002 Return address

0x2030 Return address

0x3042 Return address

throw
bad_alloc;
0x1002

0x2030

0x3042

throw
bad_alloc;
0x1002 Call Site
Exception Landing
Metadata Pad

0x1000..0x1004 * 0x1042

0x2030 0x2000..0x2040 runtime_error 0x2080

0x3042

throw
bad_alloc;
0x1002 Call Site
Exception Landing
Metadata Pad

0x1000..0x1004 * 0x1042

0x2030 0x2000..0x2040 runtime_error 0x2080

No Handler in Table

0x3042

throw
bad_alloc;
0x1002 Call Site
Exception Landing
Metadata Pad

0x1000..0x1004 * 0x1042

0x2030 0x2000..0x2040 runtime_error 0x2080

Not a runtime_error!

0x3042

throw
bad_alloc;
0x1002 Call Site
Exception Landing
Metadata Pad

0x1000..0x1004 * 0x1042

0x2030 0x2000..0x2040 runtime_error 0x2080

We found a handler!

0x3042 Jump to 0x1042

throw
bad_alloc;
Roadmap
Exploitation
Techniques

General Stats
Idea

Real-World
Exploits
Unwinding
void foo() { Stack
try {
bar();
}
catch (...) {
// handle errors
} foo + 5
}

void bar() {

throw new Exception();


}
void foo() { Stack
try {
bar();
}
catch (...) {
// handle errors
} 0x41414141
}

void bar() {
// overflow!
throw new Exception();
}
void foo() {
try {
vuln();
Catch Handler
} Confusion
catch (...) { lose(); }
}

void vuln() {
// overflow
throw new Exception();
}

void bar() { Overwrite return_address


try { /* ... */ } to call site range of bar()
catch (...) { win(); }
}
What do we control?
void foo() {
int x = 23;
try { vuln(); } Prints 23
catch (...) {
cout << x << endl;
}
}

void baz() { What does it print now?


int x = 42;
try { /* ... */ }
catch (...) { … still 23
cout << x << endl;
}
}
void foo() {
int x = 23;
try { vuln(); }
catch (...) {
cout << x << endl; x = 23
}
} baz + 17
void baz() {
int x = 42; vuln()
try { /* ... */ }
catch (...) {
cout << x << endl;
} x gets restored
} from the stack!
void foo() {
int x = 23;
try { vuln(); }
catch (...) {
cout << x << endl; x = 1337
}
} baz + 17
void baz() {
int x = 42; vuln()
try { /* ... */ }
catch (...) {
cout << x << endl;
} Attackers control the target
} handler’s local variables!
void foo() {
int x = 23;
try { vuln(); }
catch (...) {
cout << x << endl;
}
} baz + 17
void baz() { x = 1337
int x = 42;
try { /* ... */ }
vuln()
catch (...) {
cout << x << endl;
} Sometimes they are even
} stored in callee-saved regs!
Roadmap
Exploitation
Techniques

General Stats
Idea

Real-World
Exploits
Unwinding
Stack Buffer
Overflow

The Attack throw

Viable Handler
Are there interesting
handlers we can reach?
throw

catch
Golden Gadget in libstdc++
void __cxa_call_unexpected (void *exc_obj_in) {
xh_terminate_handler = xh->terminateHandler;
try { /* ... */ }
catch (...) {
Restored
__terminate(xh_terminate_handler); from stack
}
}

void __terminate (void (*handler)()) throw () {


/* ... */
handler();
std::abort(); And then
} called!
bool StackProtector::runOnFunction(Function &Fn) {
// ...

if (!RequiresStackProtector())
return false;

// ...

return InsertStackProtectors();
}

llvm::StackProtector
/// Check whether or not this function needs a stack protector based
/// upon the stack protector level.
///
/// We use two heuristics: a standard (ssp) and strong (sspstrong).

L ; R
/// The standard heuristic which will add a guard variable to functions that

D
/// call alloca with a either a variable size or a size >= SSPBufferSize,
/// functions with character buffers larger than SSPBufferSize, and functions

T
/// with aggregates containing character buffers larger than SSPBufferSize.
The
/// strong heuristic will add a guard variables to functions that call alloca
/// regardless of size, functions with any buffer regardless of type and size,
/// functions with aggregates that contain any buffer regardless of type and
/// size, and functions that contain stack-based variables that have had their
/// address taken.
bool StackProtector::RequiresStackProtector() {

llvm::StackProtector
➔ Functions that call alloca()

➔ Functions with any buffer

➔ Functions that contain stack-based variables


that have had their address taken.

Other functions don’t have stack cookies!


Let’s look at an Example
cmp cookie Function Epilog

return abort
throw Handler

cmp cookie Function Epilog

return abort
➔ No alloca
➔ No buffer
➔ No addr taken
➔ No cookie!

throw Handler Handler

cmp cookie Function Epilog return

return abort
➔ No alloca
➔ No buffer
➔ No addr taken
over ➔ No cookie!
flow
throw Handler Handler

cmp cookie Function Epilog return

return abort Pivot2ROP


3. baz() - throws
2. bar() {

baz();
// unreachable
}
1. foo() {
try { bar(); }
catch(...) {...}
}
3. baz() - throws class Thing {
2. bar() { Thing() {}
Thing it();
~Thing() {
baz();
// unreachable // cleanup
} } Unwind_Resume
1. foo() { }
try { bar(); }
catch(...) {...}
}
Cleanup Handlers

➔ Often call
free()

➔ Can be chained
(similar to ROP)
libstdc++
💀
kill -SIGUSR1 $pid
⚙⏸ ???

Signal
Handler
return
sigreturn
frame

All the registers!

mov rax, 0xf


syscall
return
return
sigreturn
frame

All the registers!

48 c7 c0 0f
00 00 00 0f
return
05
return
fake sigreturn
frame

All the registers!

48 c7 c0 0f
00 00 00 0f
return
05
return return
fake sigreturn
frame
Handler
RSP
RIP
48 c7 c0 0f
00 00 00 0f
return
05
Recap: Techniques

➔ Divert control flow by confusing Catch Handlers

➔ Evade Stack cookies and Pivot to ROP

➔ Groom the heap by chaining Cleanup Handlers

➔ Craft signal frames to pivot the stack


Does this even happen?
Roadmap
Exploitation
Techniques

General Stats
Idea

Real-World
Exploits
Unwinding
Stack Buffer
Overflow

Preconditions
throw
Let’s recap

Viable Handler
Select top 1000 popular
packages (~3.3k binaries)

~10% of Binaries
Debian repo use exception handling

Half contain
Debian Binary at least 40%
DB throwing functions
PopCon Analysis
Interesting gadgets Cleanup Catch
➔ Arbitrary Free 79 % 58 %
➔ Control-flow Hijack 64 % 51 %
➔ Write-What-Where 44 % 46 %
➔ Write-Where 66 % 49 %
➔ Write-What 2 % 17 %
% of binaries containing
at least one gadget
Roadmap
Exploitation
Techniques

General Stats
Idea

Real-World
Exploits
Unwinding
● PowerDNS
Snap Back CVE-2009-4009
to Reality ● SmartCardServices
CVE-2018-4300
Exploiting
Known Bugs ● LibRaw
CVE-2018-5809
● PowerDNS
Snap Back CVE-2009-4009
to Reality ● SmartCardServices
CVE-2018-4300
Exploiting
Known Bugs ● LibRaw
CVE-2018-5809
parse_exif (int base) {
// ... CVE-2018-5809
while (entries--) {
tiff_get (base, &tag, &type, &len, &save);
// ...
switch (tag) {
// ...
case 37500: // tag 0x927c
if ((!strncmp(make, "RaspberryPi",11)) &&
(!strncmp(model, "RP_imx219",9)))) // and others
char mn_text[512];
// ...
fgets(mn_text, len, ifp);
// no throw :(
}
parse_exif (int base) {
// ... CVE-2018-5809
while (entries--) {
tiff_get (base, &tag, &type, &len, &save);
// ...
switch (tag) {
// ...
case 37500: // tag 0x927c
if ((!strncmp(make, "RaspberryPi",11)) &&
(!strncmp(model, "RP_imx219",9)))) // and others
char mn_text[512];
// ...
fgets(mn_text, len, ifp);
// no throw :(
}
else
parse_makernote (base, 0);
RaspberryPi

return

return

mn_text parse_exif
PwnpberryPi Now we can trigger throw!

return

one_gadget
/bin/sh
Pivot2ROP

mn_text parse_exif
INT64 LibRaw::x3f_thumb_size()
{
try
{
// address to here
// in the return address
}
catch (...)
{
return -1;
}
}
Demo Time
parse_exif (int base) {
// ... CVE-2018-5809
while (entries--) {
tiff_get (base, &tag, &type, &len, &save);
// ...
switch (tag) {
// ...
case 37500: // tag 0x927c
if ((!strncmp(make, "RaspberryPi",11)) &&
(!strncmp(model, "RP_imx219",9)))) // and others
char mn_text[512];
// ...
fgets(mn_text, len, ifp);
// no throw :(
}
else
parse_makernote (base, 0);
If we corrupt the stack,
we can modify data
to reach throw
via unexpected paths!
Recap
Exploitation
Techniques

General Stats
Idea

Real-World
Recap Exploits
Unwinding
● The entire research team

● Tasteless

Thanks! ● Gregor Kopf

● Jiska Classen

● The VUSec group


Black Hat ➔ Exception Handling is an
overlooked attack surface
Sound Bytes
➔ Mitigations are often
Key Takeaways
incomplete

➔ Even when preconditions


github.com/ seem unlikely, exploitation
chop-project/ is often possible
chop

You might also like