-
Notifications
You must be signed in to change notification settings - Fork 5
Shellcode
While it's well and good to make permanent changes to software, during development it is often much safe and much more convenient to temporarily load code into memory and execute it in place, with no permanent changes to other regions of the chip.
Executing shellcode requires write access to some region in which to place the code, as well as the beginning of SRAM to overwrite the function pointer table. If the pointer table weren't available, it's also possible, though less elegant, to overwrite the call stack.
On the Java side of things, byte[] NfcRF430.exec(int adr)
works by
replacing the function pointer and then triggers that function
pointer.
The function pointer in this implementation is at 0x1C5C
, and it is
called to send back an error message when an illegal block address is
supplied over NFC. If the shellcode sends back the expected two
bytes, they can be returned to the host. The code ends by restoring
the original error message hook.
public byte[] exec(int adr) throws IOException{
/* While we could overwrite the call stack, it is much easier to overwrite the
function call table in early SRAM with a pointer to our function, because we
can only perform writes of 4 or 8 bytes at a time, and the call stack within a
write handler will be quite different from the one in a read handler.
There are plenty of functions to choose from, and an ideal hook would be one that
won't be missed by normal functions. We'd also prefer to have continuation wherever
possible, so that executing the code doesn't crash our target.
The function pointer we'll overwrite is at 0x1C5C, pointing to rom_rf13_senderror() at
0x4FF6. For proper continuation, you can just write two bytes to RF13MTXF and return.
Without proper continuation, an IOException will be thrown in the reply timeout.
To unhook, write 0x4FF6 to 0x1C5C, restoring the original handler.
As a handy side effect, we return the two bytes that need to be transmitted for
continuation, so you can get a bit of data back from your shellcode.
*/
Log.v("GoodV", String.format("Asked to call shellcode at %04x", adr));
// First we replace the read error reply handler.
write(0x1C5C, new byte[] {(byte) (adr&0xFF), (byte) (adr>>8)} );
// Then we read from an illegal address to trigger an error,
// returning the two bytes of its handler.
byte[] shellcodereturn = transceive(new byte[]{
0x02, // Flags
(byte) 0xC0, // MFG Raw Read Command
0x07, // MFG Code
(byte )(0xbe), (byte) (0xba) //16-bit block number, little endian.
});
Log.v("GoodV", "Shellcode returned: "+GoodVUtil.byteArrayToHex(shellcodereturn));
//And finally, we repair the original handler address, like nothing ever happened.
write(0x1C5C, new byte[] {(byte) (0xf6), (byte) (0x4f)} );
return shellcodereturn;
}
For convenience, the TI-TXT loading format implemented in our
Android client has been extended with a new command, x
, that
executes shellcode at the selected address.
For a concrete example, this shellcode loads to FE00
then
executes from hookmain()
at FE18
.
@FE00
31 40 00 2C 3C 40 02 1E 0D 43 3E 40 00 01 B0 12
48 FE 0C 43 B0 12 3C FE 4D 43 3E 40 02 1E 0C 4D
0C 5D 0C 5C 0C 5E BC 43 00 00 BC 43 02 00 1D 53
3D 90 40 00 F4 23 92 43 08 08 30 41 B0 12 18 FE
B0 12 12 50 30 40 44 FE 0F 4C 0E 5C 0F 9E 01 20
30 41 CF 4D 00 00 1F 53 30 40 4C FE
@FE18
x
q
Note that hook_main()
has to send two bytes back to the host for
continuation. If it does not, thent he Android device will timeout
and trigger an IOException, killing the connection.
#include <rf430frl152h.h>
#include <stdint.h>
#include "rom.h"
volatile uint32_t array[64];
/* This replacement function needs to send two bytes for re-entry.
*/
void __attribute__ ((noinline)) hook_main(){
int i;
for(i=0;i<64;i++)
array[i]=0xFFFFFFFF;
//Send two bytes for continuation.
RF13MTXF=1;
return;
}
int main(){
hook_main();
//After we've done our stuff, calling the RESET handler of the ROM
//gives it a chance.
ROM_RESET();
//We save 84 bytes if main() never returns!
while(1);
}