How to Hack Embedded Firmware: Function Calls
We spend a lot of time at ReFirm Labs finding ways to make the embedded firmware function in connected devices more secure. We do that, of course, because we’re a business and that’s what we do. But we also do it because it’s an important thing to do, and it’s the right thing to do.
And we also do it because we strongly believe that cyber security is everyone’s business, especially given the rapidly-increasing interconnection and interdependence of the world brought on both by widespread internet access and the continuing avalanche of devices connecting all aspects of our daily lives via that internet.
With that in mind, it’s our hope that this rather sensationally-titled piece will actually provide some down-to-earth developer basics for helping to make our connected world at least a little safer — because it’s with the developers that this story begins.
Firmware Function – What’s Old is New Again
The demand for developers to write code for the flood of new connected devices grows along with the number of devices on the market — and the demand, almost by necessity, has to be met by new and junior developers who are also entering the market along with the devices. These junior developers lack experience, of course, but in the rush to market of a hot device they can also lack oversight as manufacturers prioritize speed and profitability over security in regards to firmware function.
None of which is to suggest that every connected device manufacturer out there is a bad actor, or that they don’t want their devices to be secure because of firmware function. Nor do developers set out to write intentionally insecure code. We can’t know the mind of every single developer, of course — but what seems most likely in all of this is simply that junior developers, lacking detailed oversight from more senior personnel, simply don’t know that their code may have serious vulnerabilities built into it, and that this in turn makes the devices themselves vulnerable.
Firmware Function is Vital
The embedded firmware function on these devices is most often written in relatively low-level languages like C and C++ because of the power and flexibility that those languages afford. In and of itself, that’s not a problem — indeed, the need for firmware to communicate with and control device hardware at the most fundamental of levels makes that kind of power a near-necessity. The trouble is that, having been around for a long while indeed, there are several known vulnerabilities which, if they are found in code, hackers can use to gain control of a device with that code in its firmware.
These vulnerabilities typically occur through functions originally built into the language for which exploits were later identified. Although these functions have been replaced with newer versions that remove the vulnerabilities, for backwards-compatibility the original, vulnerable functions remain in the language. Newer developers may lack the experience or the training to call the new functions, and instead make function calls to the vulnerable functions simply because they don’t know any better.
Hackers, on the other hand, are well aware of the vulnerabilities in the older functions — and actively look for them in embedded firmware code. This is because many of the vulnerabilities present via the older function calls allow for buffer overflow attacks, which is a valuable tool in the hacker toolbox. We’ll first look at buffer overflow, and then at some specific function calls which a hacker can exploit to trigger a buffer overflow — along with more secure versions of those functions which developers should use instead.
Your (Memory) Cup Runneth Over: Buffer Overflows
If you’re not familiar with how memory works in a computer you can think of it as a vast series of mailboxes of a fixed size, each with their own unique address. One of the reasons that C is such a powerful programming language (and also so dangerous!) is that it addresses those memory addresses practically directly, and often with very little regard for safeguarding them.
Thus, when a developer creates a variable of type string (for example) in the code the program will reserve an address for that variable, guaranteeing it a place in memory. But because protections are few, and a C program is very literal in following its instructions, if you tell it to store something at a memory address it will do so — even if what you told it to store is too large to fit into that address. What then? It will continue to write what you told it to write by spilling over into an adjacent address or addresses. Whatever had been stored in those addresses is now gone — and if what the computer had been told to store was some malicious code from a hacker, that’s now in memory and ready for use.
That’s a rather simplistic overview, but it’s exactly the kind of vulnerability that exists in the following function calls, which do not check the size of what’s being placed into memory and thus facilitate overflow attacks. Thus, if a hacker sees these function calls in code, they know they can send more information than the address can hold — and the “overflow” information is going to be code to suit whatever purposes they have in mind.
With that in mind, here are some of the older function calls to avoid — and some updated calls that will better enforce protections against overflow.
- Avoid gets(). Use fgets() or gets_s().
We can’t say it any better than this, direct from the U.S. government’s Computer Emergency Readiness Team: If there was ever a hard and fast rule in secure programming in C and C++ it is this: Never use gets().” The gets_s is the better choice here, if it’s available to you. If not, go with fgets. Remember: Never use gets().
- Avoid strcat() and strcopy(). Use strncat() and strncopy().
These two functions, strncopy() and strncat(), were attempts to update the even-more-dangerous strcat() and strcopy() — so definitely don’t use those! But DO be aware that, even though strncopy and strncat are better than the original functions, they still contain serious buffer overflow vulnerabilities. Support for their newer and more secure descendants, strncopy_s and strncat_s, is spotty, at best — but if you can use those two, at least consider it.
- Avoid sprintf(). Use snprintf().
Like the other avoid/use functions in this list, these two functions are essentially identical, except that when a function call to snprintf is made an argument is passed that tells the compiler the maximum number of characters that will be written to memory, thus preventing any overflow attacks via this vector from succeeding.
Is Cybersecurity Really That Simple?
No. And yes.
Best-practice firmware coding is a start on the road to cybersecurity — but there are plenty of other places within firmware that can leave devices open to attack. Furthermore, given the frequent rush to market that many devices see, firmware code errors that lead to vulnerabilities are not always caught in code review or during testing. Once vulnerabilities are found, manufacturers will often release updated firmware for their affected devices.
However, once devices are in places as part of an IoT implementation, it can be difficult to keep track of what firmware updates are available. That’s why we’ve built Centrifuge to automatically scan firmware for other vulnerabilities, as well as to help business make sure that, once patches are released, they are always running the most up-to-date firmware across their networks.
Backdoored firmware found in the supply chain of video surveillance chips from HiSilicon (a subsidiary of Huawei) allows remote access via Telnet.
A few days ago I decided to reverse engineer my router’s firmware image with binwalk. I’ve bought the TP-Link Archer C7 home router. Not one of the best, but good enough for my needs.
On February 4th, 2020 we deployed a new analyzer to the Centrifuge Platform, our automated firmware analysis platform which detects the presence of the Cable Haunt vulnerability in eCos-based firmware images.