Removing Randomness with LLDB
Let’s say you’re debugging a third-party app that has some code like this:
let data1 = randomData(length: 8)
let data2 = randomData(length: 8)
let data3 = randomData(length: 16)
doSomething(data1, data2, data3)
Sure, the code doesn’t really look like that — those calls are spread miles apart in the binary, with at least eight layers of abstraction in between them — but the end result is exactly the same. The app generates random data in a few different places, and that random data is all fed into some larger system.
You could be trying to figure out how an animation works, or examining an edge case in a video game, or trying to break some client-side cryptography (🙋).
But you’re debugging, and the randomness is getting in the way. Debugging benefits from consistency, and randomness is your enemy.
Let’s get rid of the randomness accordingly — all from lldb
.
Our Test Program
First, let’s write a quick program that we can use for testing. It creates some random data values and prints them out.
#import <Foundation/Foundation.h>
// Creates an NSData instance of a given
// length filled with random data
NSData* randomData(NSUInteger length) {
// Create a mutable data instance
NSMutableData* data
= [NSMutableData dataWithLength:length];
// Fill the data with random `int` values
uint32_t* bytes = data.mutableBytes;
for (NSUInteger i = 0; i < length / sizeof(int); i++) {
bytes[i] = arc4random();
}
return data;
}
// Print out an array of NSData values
void printDatas(NSArray<NSData *>* datas) {
for (NSData* data in datas) {
NSLog(@"%@", [data base64EncodedStringWithOptions:0]);
}
}
// Our main function:
// Create some values and print them out!
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSData* data1 = randomData(8);
NSData* data2 = randomData(8);
NSData* data3 = randomData(16);
printDatas(@[data1, data2, data3]);
}
return 0;
}
Running this locally, I get a result like:
B9aD3b0xxoM=
V+DfMmixVEw=
zq8LseN/VR4GBAjSMlEtIQ==
Though of course, the output will change every time!
Now that we have some example randomness, let’s work on getting rid of it through the debugger.
Fixed Size, Fixed Data
I’ve talked about lldb
’s thread return
command a bit in a previous post
— it lets you exit a method early and return any value you’d like. Combined with auto-continuing breakpoints,
it’s a super quick & easy way to provide a mock return value to a given method.
We can use thread return
to effectively make it such that every time someone calls our randomData
function,
they automatically get the same value back. We’ll do this by setting a breakpoint on randomData
, instructing
the debugger to immediately return a fixed value when that breakpoint is triggered, and then auto-continue execution.
// Set a breakpoint...
breakpoint set
// On the function named `randomData`
-n "randomData"
// Command to run when the breakpoint is hit:
// Create an `NSMutableData` instance
// of length `8` filled with zeroed bytes.
--command "thread return
[NSMutableData dataWithLength:8]"
// Automatically continue once the
// breakpoint has been hit, rather
// than waiting for us to continue
// execution manually
--auto-continue true
lldb
doesn’t love multiline commands (let alone comments), so here’s a pastable version of the above:
breakpoint set -n "randomData" --command "thread return [NSMutableData dataWithLength:8]" --auto-continue true
To use this command, we need to launch our program, pause execution in the debugger
(if you’re running from Xcode, just setting a breakpoint on main
is easiest), and then
simply enter the command into the debugger.
Note: If you’re running this in Xcode, you may need to disable backtrace recording in order to get debugger commands working — though this is not an issue you would run into when debugging a released app.
Now we can resume execution, and see our new output!
AAAAAAAAAAA=
AAAAAAAAAAA=
AAAAAAAAAAA=
Perfect! No randomness there at all!
Depending on your use case, something like this might actually be fine; but it does lead to two potential issues:
- We’re now disregarding the requested
length
of data — remember that our test code is requesting data of length8
, then8
, then16
. Returning a fixed length might be OK if that length is long enough, but this seems likely to cause issues. - Our data doesn’t look random — we don’t want all zeroes (or
A
’s, in the base64 case) — what we want is random-looking but consistent data.
Let’s fix both of those!
Dynamic Size, Fixed Data
We hardcoded the above example to return data of length 8
— let’s instead derive that length
from the data passed into the method.
If we set a breakpoint on our randomData
function — a normal breakpoint,
not our fancy command one from above — and re-run the program,
we can access the given length value by inspecting $arg1
,
which represents the first (and in our case, only!) argument passed into the method:
NSData* data1 = randomData(8);
// breakpoint on `randomData`
(lldb) p $arg1
(unsigned long) $0 = 8
Note: Finding Argument Values
In our test case, we access the length value with $arg1
, but how you’d find an equivalent value
in other cases will depend on how the function was defined (and in some cases, how it was implemented!).
If this were a method on an Objective-C object (like a method called randomDataWithLength:
in some RandomHelper
class) we would need to access $arg3
instead — since for Obj-C methods, $arg1
will be self
, $arg2
will be _cmd
, and then $arg3
will be our first “real” argument.
Swift’s calling conventions get much more complicated,
and lldb
will likely prevent you from using the $arg
variables altogether.
For a basic Swift function, you may just be able to inspect a register directly
(like $X0
for the first argument on an ARM-based machine), but that can change
quickly based on a number of factors — including whether the method implementation
references self
! Finding this value will be a larger battle in Swift, but should always be possible with enough digging.
Updating Our Breakpoint Command
Currently in our breakpoint command, we’re hard-coding our data length to be 8
:
thread return [NSMutableData dataWithLength:8]
Since we know the actual length we want is accessible via $arg1
, in a perfect world, we could simply
swap out that length argument and be done:
thread return [NSMutableData dataWithLength:$arg1]
This command is evaluated in the context of our randomData
function, so $arg1
should have the correct value.
However, if you try this, you’ll likely run into an error:
error: Aborting reading of commands after command #0:
'thread return [NSMutableData dataWithLength:$arg1]'
failed with error: Error evaluating result expression:
error: Couldn't apply expression side effects :
couldn't dematerialize register x0 without a stack frame
First, let’s take a moment to acknowledge that the string couldn't dematerialize register
doesn’t
return any search engine results outside of one link to lldb
’s source code
itself
— which means we must be doing something fun here! Hooray for us.
Without knowing the full context here, some quick speculation on my part —
$arg0
is (in my case, on an ARM-based machine) effectively a helper to access
register X0
, where the first argument is stored while calling (most) functions.
You’ll see the same error if you try to access the value through $X0
directly.
lldb
knows that we want to read the value of this register, but
in order to make the method call we’re requesting
(+[NSMutableData dataWithLength:]
),
lldb
also has to change the value of that register; since that method
has its own set of arguments, the first of which has to be stored in X0
.
These two goals are slightly at odds; ideally, lldb
could store the existing
value of X0
on the stack, but as the error message notes, it does not have
a stack frame available with which to do so. It ends up erroring out as a result.
If that is indeed the issue, we can work around it by breaking our command into two; one to
read the value of $arg0
, and another to use it:
// Create a `$length` variable
// that we will set later
e int $length;
// Set a breakpoint...
breakpoint set
// On the function named `randomData`
-n "randomData"
// First command — update `$length`
// to the value in $arg1
--command "e $length = $arg1"
// Second command — create an
// `NSMutableData` instance with
// `$length` bytes
--command "thread return
[NSMutableData dataWithLength:$length]"
// Automatically continue once the
// breakpoint has been hit, rather
// than waiting for us to continue
// execution manually
--auto-continue true
We now have two top-level debugger commands: one to create our length variable, and one to set our breakpoint. And the breakpoint itself has two commands attached to it: one to set our length variable, and then one to perform the actual data creation.
It’s a slightly more annoying setup due to a couple lldb
design choices (breakpoint commands
not sharing a variable scope, new persistent variable declarations not overwriting old ones
— both reasonable choices, just not helpful for us here).
As before, here’s a pastable version:
e int $length;
breakpoint set -n "randomData" --command "e $length=$arg1" --command "thread return [NSMutableData dataWithLength:$length]" --auto-continue true
By pasting in the above commands at the start of the program, then continuing execution so that our random data is created & printed out…
AAAAAAAAAAA=
AAAAAAAAAAA=
AAAAAAAAAAAAAAAAAAAAAA==
We are now properly recognizing the length
values of our different calls — 8
, 8
, and 16
. Perfect!
Dynamic Size, Dynamic Data
Now that we have access to the correct length
value for each call, it’s
not a huge jump to return whatever data we want.
We’ll just need to update our current data creation code:
[NSMutableData dataWithLength:$length]
to something more advanced.
One of the easiest ways to do this is to simply
append a bunch of randomly-generated int
’s to our data
until we have reached the desired number of bytes
— similar to what our test implementation already does.
Better yet — if we use a random number generator that
we control the seed of (like rand
via srand
),
we can ensure that we get the same exact result each time;
giving us the random-looking but consistent data
that we hoped for.
// Seed `rand` with any
// fixed value we want
srand(0);
// For each byte...
for(int i = 0; i < $length / sizeof(int); i++) {
// Get a random value
int randomInt = (int)rand();
// Add it to the data
[data appendBytes:(void*)&randomInt
length:sizeof(int)];
}
This is also quite nice in that we can change the seed value if we want to investigate a different result — you can find a seed value that works for what you need to debug, and then keep using it across launches.
Let’s update our lldb
commands to include the above setup.
Some of our breakpoint commands are getting quite long, but
they still work. I’ve added some additional spacing here to keep
things somewhat readable.
// Create a `$length` variable
// that we will set later
e int $length;
// Create a `$mockData` variable
// that we will set later
e NSMutableData *$mockData;
// Seed the `rand` function with
// a set seed. We can change this
// to get different results on
// different runs
e srand(0);
// Set a breakpoint...
breakpoint set
// On the function named `randomData`
-n "randomData"
// First command — update `$length`
// to the value in $arg1
--command "e $length=$arg1"
// Second command - update `$mockData`
// to a new instance of random data
--command "e
// Create a new NSMutableData instance
$mockData = (NSMutableData *)
[NSMutableData dataWithCapacity:$length];
// For each byte...
for(int _i = 0; _i < $length / sizeof(int); _i++) {
// Get a random value
int _randomInt = (int)rand();
// Add it to the data
(void)[$mockData
appendBytes:(void*)&_randomInt
length:sizeof(int)
];
}"
// Third command — return our
// new data instance
--command "thread return $mockData"
// Automatically continue once the
// breakpoint has been hit, rather
// than waiting for us to continue
// execution manually
--auto-continue true
And again, a pastable version:
e int $length;
e NSMutableData *$mockData;
e srand(0);
breakpoint set -n "randomData" --command "e $length=$arg1" --command "e $mockData = (NSMutableData *)[NSMutableData dataWithCapacity:$length]; for(int _i = 0; _i < $length / sizeof(int); _i++) { int _randomInt = (int)rand(); (void)[$mockData appendBytes:(void*)&_randomInt length:sizeof(int)];}" --command "thread return $mockData" --auto-continue true
If I run these lldb
commands at the start of our test app, and then continue execution,
I now get the same output every time:
Qs4MH/teuQE=
n7EKMeBWEzU=
WaesCIHIHn9azv09gZb2YQ==
and, if you’re following along on your machine (and your rand
implementation is equivalent to mine)
— you probably do too!
Whatever values you get, you’ll see them printed every time you run the app with
these debugger commands; and if you need different random outcomes, you can simply change
the seed passed to srand
.
Even more fun, you can create a breakpoint on main
that executes these four debugger actions,
then automatically continues, and now every execution of the program will have the same
un-randomized behavior without you having to lift a finger.
Wrapping Up
This technique takes a bit of work to set up, but it can be super useful when debugging anything that has an inherently random aspect to it — and the example above usually only requires minor tweaks to be adapted to all sorts of situations.
Anyways — back to trying to break some encryption!
Say Hello!