May 21, 2024
How to use dynamic reverse engineering for embedded devices
The proliferation of IoT has been accompanied by a proliferation of security vulnerabilities. Left unchecked, malicious attackers can use these weaknesses to infiltrate organizations' systems. Regular
The proliferation of IoT has been accompanied by a proliferation of security vulnerabilities. Left unchecked, malicious attackers can use these weaknesses to infiltrate organizations' systems.
Regular penetration testing, long recognized as a security best practice, help security teams identify and mitigate vulnerabilities and weaknesses in embedded devices. Many organizations, however, limit pen testing to investigating networks and infrastructure -- IoT devices are often overlooked.
To get security teams up to speed on embedded device pen testing, Jean-Georges Valle, senior vice president at Kroll, a cyber risk and financial services consultancy, wrote Practical Hardware Pentesting: Learn attack and defense techniques for embedded systems in IoT and other devices.
In the following excerpt from Chapter 10, Valle details how pen testers can use dynamic reverse engineering to see how code behaves during execution on embedded devices. Valle provides an example of dynamic reverse engineering to show pen testers the challenges that may arise while observing how code behaves.
Read an interview with Valle about embedded penetration testing, including common testing steps he uses, the difficulties of embedded pen testing and his opinion on how well organizations today secure embedded devices.
Editor's note: The following excerpt is from an early access version of Practical Hardware Pentesting, Second Edition and is subject to change.
I've prepared a variant of the previous example that will pose us some challenges. I will show you how to overcome these challenges both statically and dynamically in order for you to be able to compare the amount of effort needed in both cases.
The rule of thumb when comparing dynamic and static approaches is that 99% of the time, dynamic approaches are just easier and should be given priority if possible (don't forget that you may not be able to get access to JTAG/SWD or other on-chip debugging protocols).
In this section, we will also learn how to break where we want, inspect memory with GDB, and all this good stuff!
The target program is located here in the folder you cloned, in the ch12 folder.
First, let's start by loading it into Ghidra and inspect it superficially. Pay attention to setting the correct architecture and base address in Ghidra's loading window (refer to the previous chapter if you don't remember how to do that or the base address value).
At first glance, the main function looks very similar to the main function in the previous chapter. We can find the reference to the main function by searching a PASSWORD string just like in the previous chapter and look into analyzing its structure.
I will let you work on the skills you acquired in the previous chapter to find the different functions. In this executable, you will find the following again:
The similarity of the structure is intentional as this is your first time. If I were to repeat the exact same steps as in the previous chapter, it wouldn't give you anything new to learn, right?
Now, let's go through multiple methods of bypassing this password validation through dynamic interaction with the system. We will go from the most complex to the simplest in order to keep you focused and acquiring know-how (if you are anything like me, if there is an easy way to bypass something, why go for the hard way?).
The first thing we're going to do is try to see how the password is validated to understand how to generate a password that passes the tests.
Let's have a look at the validation function equivalent C code that is output by Ghidra:
Humm... this is doing nothing directly with the parameters. This is copying the content of a 0x47 (71) long static array of bytes to RAM (and NOTs it) and then calls it as a function.
This is strange.
Or is it?
This is a very common technique to camouflage code (of course, a very simple version of it). If a clear version of the opcode is not present in the .bin file (and hence not in the flash of the MCU), a reverse engineering tool like Ghidra cannot detect that it is code! Here, we have two possible approaches:
I will leave the first solution for you to implement as an exercise. It should take more or less 10 lines of Python or C code for such a simple task! You want to be a hacker? Hack away!
Me? I'm a lazy guy. If a computer can work for me, well... So be it! I'll go for the second solution.
First, let's fire up a screen session in a terminal so we can enter passwords and see how it reacts:
Let's fire up OpenOCD and GDB in a second terminal, as we did at the beginning of the chapter, and let's poke around:
And... and damn! It doesn't give me control back! No problem if that happens to you -- a little Ctrl + C will give you control back straight away:
After our Ctrl + C (^c), gdb tells us that the execution is stopped at address 0x080003aa in an unknown function (??).
Depending on your specific state, you may break at another address.
Do not panic -- put your thinking hat on and take your towel with you (always).
This is not a problem. The chances are that you will be breaking very near this address since it is in the waiting loop that blinks the LED, waiting for a password to be received on the serial interface.
First things first, let's have a look at our registers:
We see that pc is indeed where it is supposed to be, everything looks fine and dandy. So, now let's try to enter a password.
And... nothing works on the serial interface window! Thinking hat on... GDB is actually blocking the execution of the code; the serial interface will not react to your inputs. This is normal.
So, let's allow it to continue (continue or c in the gdb window) and see if the serial works now. Yes, it does. Let's break it again and put a breakpoint on the address of the password validation function, shall we?
In Ghidra, we can see that the address of the first instruction of the function is 0x080002b0:
Let's put a breakpoint there, let gdb resume execution, and enter a dummy password:
Let's dissect that:
Okay, now what can we do with that?
First things first, if you remember the code of the validation function, its arguments were passed directly to the decoded code. Let's have a look at what they can be (remember the calling convention for functions: arguments are in r0-3):
The first argument is something in RAM, and the second is some kind of value. (This is the transformed UUID value for your chip, which you noted down, right?)
Now, what is stored at this first address? Let's examine it:
Ah! Ah! Ah! (See what I did there?) This is our password. Please note the usage of the format modifier for the x command.
So, this is expected.
Now let's look into the deciphered code.
Ghidra tells us that the instruction that follows the decoding loops is at 0x080002f0. Let's break there:
So, the address of the deciphered code is in r3. We saw the buffer was 0x47 (71) long. We are in thumb mode (so size 2 instructions). This should be 47/2 : about 35 instructions. The last bit of the address is for the mode; we can get rid of that:
That's more like it! We see a normal function prelude (saving intra-function registers to the stack), some processing, and a function return. But GDB warns us about illegal instruction parameters (0x2000016c).
When looking at the listing, we see that GDB indicates the usage of a PC relative piece of data:
This is very often used to store data in an assembly program. adr is a pseudo instruction that tells the assembler, please add the offset to a label (a named position) in the code.
Let's look at what is stored there:
This is indeed a string that is used in the process somehow.
Let's step through the first instructions, as an example of how to follow an execution flow. We will first set up gdb so it shows us the interesting registers, content on each step:
Now we are ready to use stepi (step instruction) to see what is going on:
This zeros r4, r3, and r5 (x^x = 0):
This loads the first character of the password string in r5 (r1 is the address and r4 is zeroed at this point) and copies it to r8 and r6:
This shifts r6 4 bits to the right, r5 4 bits to the left, and puts their ORed value in r4. It then masks out the ORed result with 0xff, basically exchanging the 4 lower and 4 higher bits of the password character and cleaning out the excess bits!
This moves 15 in r6, copies r4 in r8 and r7, and masks r7 with 15. But why? At this point, r4 is 0! This may be used later -- since we saw that r4 was used as an offset on the loading of the password character, r4 is probably a counter! If that is the case, this masking can be used as a kind of modulo... (it's very common to use masking for modulo a power of two -1):
This loads the first character of the string that was hidden in r6 and uses r7 and an offset! r4 is definitely a counter here and r7 a modulo'ed version of it. This is a very typical programming way to approach this:
This is XORing the value of the bit swapped password character with the current ranks of the strange string, adding this to r0 and incrementing the r4 counter:
This loads a new password character with the new offsetting r5. r3 is 0 so the cmp checks r5-r3 and ... Wait … bgt.n? What is that? Do you remember what to do when you have doubts? Go read the documentation here: https://community.arm.com/developer/ip-products/processors/b/processors-ip-blog/posts/condition-codes-1-condition-flags-and-codes.
So, it jumps if r5 > r3. And r3 is 0, so? This is testing for a 0 terminated string!
This is the main validation logic loop!
Once this is done, it does this:
It XORs this sum with the UUID depending on the value it calculated, restores the caller register values, and returns this value. The C code then checks whether this value is null to actually display the winning string. We then just need to arrange it so that our sum is equal to the UUID dependent value for the XOR to be null!
We have the whole logic!