DUCTF 2023 – Wrong Signal
You straight to oops()
. Right away.
Description
Medium. 27/1424 solves.
I am getting all the wrong signals from this binary.
Author: hashkitten
Writeup
Analysis
On decompilation, the binary appears to be an innocent program which adds and subtracts numbers. The code itself is relatively simple.
Observations:
- A
sigaction
handler takes care of any SIGSEGV faults, outputsWrong!
and exits. SIGSEGVs occur when there are invalid memory accesses (e.g. reads, writes). - The for-loop iterates through crumbs (2-bit groups1), xors it with static data (
mangle_buf
), and modifieslocal_c0
depending on the value of each crumb. Yeah, that eyesore in the switch condition computes the current crumb. - Since 16-bytes are read, this means there are 64 crumbs, therefore 64 operations.
- Our goal is to modify
local_c0
so that it goes from0x13386000
to0x13398000
—a difference of0x12000
.
The last point is interesting, since there's no way we can get a unique solution. There are multiple ways to reach an offset of 0x12000
.
For example, if our crumbs are 3, 1, 1, 1, then we've already arrived at our target address, right? Then we can just fill the rest with 2s and 1s to do nothing to local_c0
, right? Right?
wRoNg! Ay c-rumba.
Using a Z3 script spun by reversing the program, we can output some test payloads. Now obviously this isn't the flag, but I'm interested in testing out some cases. Using I
as the first letter, we trigger case 3 (+0x15000
) as our first operation.2 Turns out we can't do that as our first move, because it catapults us into oops()
.
If instead our first case was 2, the program continues, and we're not thrown straight into jail oops()
. So there must be something we're missing.
Where are the segfaults coming from?
While all the above observations are fine and dandy, the decompilation leaves out something crucial. Isn't it weird how local_c0
seems to be working with addresses and jumping around without actually doing anything? Turns out, there's a sneaky little dereference after the switch-case, at 0x401305
.
Don't underestimate these few lines. Even though the dereferenced value is unused, a read is performed nonetheless!
So why are some reads causing segfaults? To answer this, we can use the vmmap
command that comes with GDB GEF. This shows various segments of a binary, their address ranges, and whether they're readable/writable.3
Yikes! That's a lot of segments. Notice how some of them disallow all permissions? Our pointer was trying to read those regions. All that's left is to filter out the regions in our code.
Concluding Remarks
Peeking at the official solve script... it turns out the challenge was a... maze?!? Wut? Didn't expect that. But overall it was a fun little challenge with some nice surprises, and a good reminder to not overlook (or completely ignore) the small details such as that hidden byte read.
Solve Script
I didn't do a step-by-step walkthrough of my solve script this time, but I've littered it with comments, so hopefully it's understandable—even for those new to the Z3 library.
Flag
Footnotes
Comments are back! Privacy-focused, without ads, bloatware, and trackers. Be one of the first to contribute to the discussion — I'd love to hear your thoughts.