Can you get root with only a cigarette lighter? (2024)

Security researcher David Buchanan demonstrates how a cheap piezo-electric lighter can be used for Electro-Magnetic Fault Injection (EMFI) to achieve local privilege escalation on a laptop.
By David Buchanan, 7 th October 2024
Spoiler alert: Yes.
the elite hacking tool they don't want you to know you already own
Before you can write an exploit, you need a bug. When there are no bugs, we have to get creative—that's where Fault Injection comes in. Fault injection can take many forms, including software-controlled data corruption, power glitching, clock glitching, electromagnetic pulses, lasers, and more.
Hardware fault injection is something that typically requires specialized (and expensive) equipment. The costs stem from requiring a high degree of precision in terms of both when and where the fault is injected. There are many valiant attempts at bringing down the costs, with notable projects ranging from the RP2040-based PicoEMP, all the way to "Laser Fault Injection for The Masses". (The RP2040 crops up a lot due to its low cost combined with the "PIO" peripheral, which can do I/O with tight timings and latency)
A while back I read about using a piezo-electric BBQ Igniter coupled to an inductor as a low-budget tool for electro-magnetic fault injection (EMFI), and I was captivated. I wondered, how far can you take such a primitive tool? At the time, the best thing I could come up with was exploiting a software implementation of AES running on an Arduino, using DFA—it worked!
But I wasn't fully satisfied. I wanted to exploit something more "real," but I was out of ideas for the time being.
Fast forward to a couple of weeks ago, and the announcement of the Nintendo Switch 2 is on the horizon. We anticipate the Switch 2 will run largely the same system software as the Switch 1, and we're all out of software bugs. So, I was inspired to brush up on my hardware exploitation skills, and revisited my thoughts on low-budget EMFI.
Like any self-respecting hacker, I own a pile of junk laptops. I picked out a Samsung S3520, equipped with an Intel i3-2310M CPU and 1GB of DDR3 RAM. Manufactured in 2011, it's new enough that it can comfortably run a lightweight desktop Linux distro (I picked Arch), but crappy enough that I wasn't worried about bricking it.
My goal is to write a local-privilege-escalation exploit that works based on injected hardware faults.
I decided that the most physically vulnerable part of the laptop was the DDR bus that connects the DRAM memory to the rest of the system.
If you've ever looked at a laptop memory module (SODIMM), you'll notice it has a whole lot of pins. Among them are 64 "DQ" pins (numbered DQ0 to DQ63) that transfer data bits in either direction (read or write). I figured that if I could inject faults on one of these pins, I could do something interesting.
After a lot of fiddling around, here's the hardware setup I came up with:
If I counted right, this corresponds to pin 67, aka DQ26
It's just one resistor (15 ohms) and one wire, soldered to DQ26. The wire acts like an antenna, picking up any nearby EM interference and dumping it straight onto the data bus. The resistor (which might be entirely unnecessary) is just there to make sure that the interference isn't so great as to disturb normal operation of the memory—I only want glitches to happen on-demand, not all the time.
Ignore the random electrical tape; this laptop has been through a lot.
I found that clicking a regular piezo-electric lighter (no inductor coils needed) in the vicinity of the antenna wire was enough to reliably induce memory errors, shown here under memtest. Note that the errors shown both correspond to bit 29 being flipped.
Why bit 29, when I soldered to DQ26? Honestly, I'm not entirely sure; either I miscounted the pins or my laptop's motherboard swaps some of the data lines around. As far as I can tell, swapping data lines like that is allowable (it can make signal routing easier).
We don't have much control over when we inject the fault (to the resolution of my finger's reaction speed), but whenever it happens we can be fairly certain it will always flip the same bit of any particular 64-bit read or write.
As a starting point, I wanted to try writing a "sandbox escape" exploit for CPython. This is purely academic since CPython isn't even sandboxed in the first place and you can just do os.system("/bin/sh"), but I needed something easy to get started with and I'm already familiar with CPython's internals. My explanation for this exploit is going to be a bit hand-wavey because the specifics aren't that interesting, it's the overall strategy that I want to convey.
For exploiting CPython, I actually used a wire soldered to DQ7 instead of the DQ26 pictured earlier, for reasons that will become more obvious shortly.
CPython objects live on a garbage-collected heap. An object has a header that contains its refcount, then a pointer to its type object, followed by other type-specific fields. There are two object types of particular interest to us, bytes and bytearray. bytes objects are immutable and bytearray objects are mutable.
A bytes object has a length field, followed by the data itself (as part of the same heap allocation).
A bytearray object has a length field followed by a pointer to the actual data storage buffer.
The core idea of my exploit strategy is to instantiate a bytes object that contains a fake bytearray structure within it. The fake bytearray object is just data, we can't do anything with it, but if we trick CPython into giving us a reference (pointer) to this fake object, then we can construct an arbitrary memory read/write primitive (since we chose the bytearray's length and pointer fields ourselves).
So how do we get a reference to the fake object? Recall that our initial "exploit primitive" is the ability to flip bit 7 of a 64-bit word. This is equivalent to either adding or subtracting 128 from a pointer. If our fake bytearray object was at an offset of +128 bytes within the bytes object, then glitching a pointer to the bytes object will transform it into a pointer to the crafted bytearray object (with 50% probability).
So the big question is, how do we glitch that specific pointer, as opposed to everything else? If we accidentally glitch some important data we'll probably crash the whole OS, which is obviously not good.
Something important to remember is that we're glitching the memory bus, not the memory contents (as in something like Rowhammer). We only glitch the read and write operations, the data "at rest" is mostly safe. The solution here is to spam memory accesses of the pointer we want to glitch. If 99% of bus activity is saturated with exploitable operations, then a randomly timed glitch has (in theory) a 99% chance of landing somewhere we want it to.
If we read the same pointer in a loop, almost nothing would happen. This is because the CPU caches the data to avoid unnecessary DRAM accesses (cache is fast, DRAM is comparatively slow and high-latency).
My solution was to fill up a big array (larger than the 3MiB cache this CPU has) with references to the same object. Then I can access the array items sequentially in a loop, forcing the CPU to fetch them from DRAM each time, and check to see if their value changed. The inner loop looks like this:
# "victim" is the prepared `bytes` object described earlier
spray = (victim,) * 0x100_0000 # I actually use a tuple instead of an array, same idea
for obj in spray:
if obj is not victim:
# under non-glitchy conditions, this is always false
print("Found corrupted ptr!")
assert(type(obj) is bytearray)
Most of the time this won't work, so the whole thing is done from inside another big loop, until it does work (or until the system crashes).
Python's is keyword comes in handy here, it's essentially a pointer comparison operation, allowing us to check if the pointer changed. Visualising the objects in memory, it looks like this:
A "glitched" pointer is shown in red, which is now able to access the fake bytearray object. The glitch itself
Source: Hacker News










