Skip to content

Shellcode

Travis Goodspeed edited this page Nov 8, 2019 · 1 revision

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.

Executing Shellcode

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;
    }

TI-TXT Example

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);
}
Clone this wiki locally