CrossCTF Finals 2018 : Slowmo (Pwn)
First Blood by : OSI Layer 8
What is in this mysterious package?
nc ctf.pwn.sg 4005
Creator - amon (@nn_amon)
Static Analysis
Run file slowmo
:
slowmo: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, for GNU/Linux 3.2.0, BuildID[sha1]=0c6352b62e9c246c5530b20dc13a5d82c60619cc, stripped
Running IDA Pro on the binary, I got code that I originally thought was some sort of language-specific setup code, like Go's runtime initialization. I had no idea what to do, so I went to solve other challenges.
Much later into the competition, the source code was released:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
int happy = 0;
void (*indirection)();
char tape[256];
void dis() {
system("/bin/date");
}
void dos() {
system("/bin/sh");
}
int main() {
indirection = dis;
char * ptr = tape + 128;
alarm(30);
char inst;
while (1) {
if (happy) {
for (int i = 0; i < 256; i++) {
printf("/%02x", tape[i]);
}
printf("/\n");
}
read(0, &inst, 1);
switch (inst) {
case '<':
++ptr;
break;
case '>':
--ptr;
break;
case '^':
*ptr += 1;
break;
case 'v':
*ptr -= 1;
break;
case '!':
indirection();
return 0;
case '`':
happy = 1;
}
}
}
I actually had this function in IDA's analysis, but I thought it was some junk:
int __fastcall sub_105EC(int a1, int a2)
{
unsigned int v2; // r0
int result; // r0
unsigned __int8 v4; // [sp+3h] [bp-11h]
_BYTE *v5; // [sp+4h] [bp-10h]
int v6; // [sp+8h] [bp-Ch]
int v7; // [sp+Ch] [bp-8h]
v7 = dword_97F8C;
dword_99EBC = (int (__fastcall *)(_DWORD))sub_105B4;
v5 = &unk_99E3C;
sub_279F8(30, a2, (int)sub_105B4, &unk_99E3C);
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
if ( dword_98EEC )
{
v6 = 0;
if ( v6 <= 255 )
printf("/%02x", byte_99DBC[v6]);
sub_17E30();
}
v2 = sub_28620(0, &v4, (void *)1, &v4);
if ( v4 != '>' )
break;
--v5;
}
if ( (signed int)v4 <= 62 )
break;
switch ( v4 )
{
case 0x60u:
dword_98EEC = 1;
break;
case 0x76u:
--*v5;
break;
case 0x5Eu:
++*v5;
break;
}
}
if ( v4 == 33 )
break;
if ( v4 == 60 )
++v5;
}
dword_99EBC(v2);
result = 0;
if ( v7 != dword_97F8C )
sub_2AB9C(0);
return result;
}
~~This was where I realised that I hate stripping~~. Oh well, time to solve.
We can see that we can manipulate where ptr
points to and decrement/increment the byte pointed by it. The solution is rather obvious.
The ptr
originally points to tape+128
Solution
Right after the tape[256]
array is the indirection
function pointer (in terms of program memory).
So we move the pointer by 128 again (send '<' 128 times).
Then increment/decrement the bytes of the function pointer indirection
till it points to dos
instead of dis
(send 'v' or '^' abs(address of dos
- address of dis
) number of times, use '^' if dos
is after dis
or 'v' otherwise)
Call indirection()
(send '!') and we get shell.
How do find where
dos()
anddis()
are located? The binary is stripped after all.
Well, function names are stripped, but strings are not obfuscated. But we can search for '/bin/date' and '/bin/sh', then find references to these strings.
.rodata:00071E9C aBinDate DCB "/bin/date",0 ; DATA XREF: sub_105B4+8↑o
.rodata:00071E9C ; .text:off_105CC↑o
.rodata:00071EA6 ALIGN 4
.rodata:00071EA8 aBinSh_0 DCB "/bin/sh",0 ; DATA XREF: .text:000105D8↑o
.rodata:00071EA8 ; .text:off_105E8↑o
Next, we see what are the starting addresses of these functions:
.text:000105B4 sub_105B4 ; DATA XREF: sub_105EC+1C↓o
.text:000105B4 ; .text:off_10778↓o
.text:000105B4 STMFD SP!, {R11,LR}
.text:000105B8 ADD R11, SP, #4
.text:000105BC LDR R0, =aBinDate ; "/bin/date"
.text:000105C0 BL sub_17428
.text:000105C4 NOP
.text:000105C8 LDMFD SP!, {R11,PC}
.text:000105C8 ; End of function sub_105B4
.text:000105C8
.text:000105C8 ; ---------------------------------------------------------------------------
.text:000105CC off_105CC DCD aBinDate ; DATA XREF: sub_105B4+8↑r
.text:000105CC ; "/bin/date"
.text:000105D0 ; ---------------------------------------------------------------------------
.text:000105D0 STMFD SP!, {R11,LR}
.text:000105D4 ADD R11, SP, #4
.text:000105D8 LDR R0, =aBinSh_0 ; "/bin/sh"
.text:000105DC BL sub_17428
.text:000105E0 NOP
.text:000105E4 LDMFD SP!, {R11,PC}
.text:000105E4 ; ---------------------------------------------------------------------------
.text:000105E8 off_105E8 DCD aBinSh_0 ; DATA XREF: .text:000105D8↑r
.text:000105E8 ; "/bin/sh"
.text:000105EC
ARM functions (usually) begin with STMFD instructions, so we can ascertain that dis()
is at 0xB4
while dos()
is at 0xD0
.
Our solution is now: 1. Send < a total of 128 times 2. Send ^ a total of (0xD0-0xB4) times 3. Send ! 4. Profit
The solution script is here