Tags: gdb md5 rev python

Rating:

You may find this writeup complementing [that one](https://ctftime.org/writeup/18566), so read that one first.

The binary was packed with UPX and section headers were stripped so the upx couldn't unpack it. We can run the binary in gdb, break it when it asks for a key, and dump the whole thing on the disk. When we try to open in in IDA, it complains a bit but otherwise there are no errors.

Since the binary is written in Go, we can use IDAGolanHelper script to recover all function names. After that we need to go to main.main function. There something obscure happens, which can be described like this: the binary wants you to enter the 16 characters key, after what each characters MD5 hash should be equal to one of the 10 predefined hashes. The order of those hashes is set in the map.

The only catch here is that MD5 function is not actually the default one — if you look at it, you may notice different initial state. In MD5 algorithm it is 4 numbers, (0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476), but in binary those are changed to (0x68442402, 0xEECCAA88, 0x97B9DBFD, 0x0F315375). If we take an MD5 implementation, insert these numbers there and compare hashes with the binary in debugger, it will become clear that nothing else is changed.

So no we have 10 hashes, the order of them and modified MD5 algorithm. We can decode the secret key, after what we patch the binary in runtime so it calls the ExampleNewCBCDecrypter with the right key.

My Python decoding script (which also output some patching commands for gdb):
 python
#!/usr/bin/env python3

import struct
from enum import Enum
from math import (
floor,
sin,
)

from bitarray import bitarray

class MD5Buffer(Enum):
A = 0x68442402
B = 0xEECCAA88
C = 0x97B9DBFD
D = 0x0F315375

class MD5():
_string = None
_buffers = {
MD5Buffer.A: None,
MD5Buffer.B: None,
MD5Buffer.C: None,
MD5Buffer.D: None,
}

@classmethod
def hash(cls, string):
cls._string = string

preprocessed_bit_array = cls._step_2(cls._step_1())
cls._step_3()
cls._step_4(preprocessed_bit_array)
return cls._step_5()

@classmethod
def _step_1(cls):
# Convert the string to a bit array.
bit_array = bitarray(endian="big")
bit_array.frombytes(cls._string.encode("utf-8"))

# Pad the string with a 1 bit and as many 0 bits required such that
# the length of the bit array becomes congruent to 448 modulo 512.
# Note that padding is always performed, even if the string's bit
# length is already conguent to 448 modulo 512, which leads to a
# new 512-bit message block.
bit_array.append(1)
while bit_array.length() % 512 != 448:
bit_array.append(0)

# For the remainder of the MD5 algorithm, all values are in
# little endian, so transform the bit array to little endian.
return bitarray(bit_array, endian="little")

@classmethod
def _step_2(cls, step_1_result):
# Extend the result from step 1 with a 64-bit little endian
# representation of the original message length (modulo 2^64).
length = (len(cls._string) * 8) % pow(2, 64)
length_bit_array = bitarray(endian="little")
length_bit_array.frombytes(struct.pack("<Q", length))

result = step_1_result.copy()
result.extend(length_bit_array)
return result

@classmethod
def _step_3(cls):
# Initialize the buffers to their default values.
for buffer_type in cls._buffers.keys():
cls._buffers[buffer_type] = buffer_type.value

@classmethod
def _step_4(cls, step_2_result):
# Define the four auxiliary functions that produce one 32-bit word.
F = lambda x, y, z: (x & y) | (~x & z)
G = lambda x, y, z: (x & z) | (y & ~z)
H = lambda x, y, z: x ^ y ^ z
I = lambda x, y, z: y ^ (x | ~z)

# Define the left rotation function, which rotates x left n bits.
rotate_left = lambda x, n: (x << n) | (x >> (32 - n))

# Define a function for modular addition.
modular_add = lambda a, b: (a + b) % pow(2, 32)

# Compute the T table from the sine function. Note that the
# RFC starts at index 1, but we start at index 0.
T = [floor(pow(2, 32) * abs(sin(i + 1))) for i in range(64)]

# The total number of 32-bit words to process, N, is always a
# multiple of 16.
N = len(step_2_result) // 32

# Process chunks of 512 bits.
for chunk_index in range(N // 16):
# Break the chunk into 16 words of 32 bits in list X.
start = chunk_index * 512
X = [step_2_result[start + (x * 32) : start + (x * 32) + 32] for x in range(16)]

# Convert the bitarray objects to integers.
X = [int.from_bytes(word.tobytes(), byteorder="little") for word in X]

# Make shorthands for the buffers A, B, C and D.
A = cls._buffers[MD5Buffer.A]
B = cls._buffers[MD5Buffer.B]
C = cls._buffers[MD5Buffer.C]
D = cls._buffers[MD5Buffer.D]

# Execute the four rounds with 16 operations each.
for i in range(4 * 16):
if 0 <= i <= 15:
k = i
s = [7, 12, 17, 22]
temp = F(B, C, D)
elif 16 <= i <= 31:
k = ((5 * i) + 1) % 16
s = [5, 9, 14, 20]
temp = G(B, C, D)
elif 32 <= i <= 47:
k = ((3 * i) + 5) % 16
s = [4, 11, 16, 23]
temp = H(B, C, D)
elif 48 <= i <= 63:
k = (7 * i) % 16
s = [6, 10, 15, 21]
temp = I(B, C, D)

# The MD5 algorithm uses modular addition. Note that we need a
# temporary variable here. If we would put the result in A, then
# the expression A = D below would overwrite it. We also cannot
# move A = D lower because the original D would already have
# been overwritten by the D = C expression.
temp = rotate_left(temp, s[i % 4])

# Swap the registers for the next operation.
A = D
D = C
C = B
B = temp

# Update the buffers with the results from this chunk.

@classmethod
def _step_5(cls):
# Convert the buffers to little-endian.
A = struct.unpack("<I", struct.pack(">I", cls._buffers[MD5Buffer.A]))[0]
B = struct.unpack("<I", struct.pack(">I", cls._buffers[MD5Buffer.B]))[0]
C = struct.unpack("<I", struct.pack(">I", cls._buffers[MD5Buffer.C]))[0]
D = struct.unpack("<I", struct.pack(">I", cls._buffers[MD5Buffer.D]))[0]

# Output the buffers in lower-case hexadecimal format.
return f"{format(A, '08x')}{format(B, '08x')}{format(C, '08x')}{format(D, '08x')}"

a = [
'B1 60 46 DE 42 18 89 78 66 25 5F 71 55 92 66 40',
'1A B4 0E F4 C2 AF F6 66 C5 76 CC 11 30 4A 62 47',
'A7 1B 1D 3A 54 58 12 49 40 3B B9 ED 4E BE 3E 7E',
'B2 EC 46 34 56 BD 0A FC C4 76 C9 92 41 5F 66 89',
'80 64 FB 6F 3A ED DC CF 1E 65 0D 59 64 61 45 C9',
'05 DF E3 7B A7 A7 19 94 C2 18 D7 6B 68 28 19 9D',
'A6 D7 50 19 6F 7B BB 83 48 CA 6D EC 0F 44 22 A6',
'73 C5 E9 EA D9 81 AE 05 15 AA 91 4F D3 01 B3 B5',
'90 71 BF 3E 8F 7F 90 65 3C 1F CD A9 C9 0B F4 A4',
'55 E2 96 30 7C 2D 80 C3 AB 90 ED DF 2E E3 31 1C'
]
a = [s.split() for s in a]
a = [''.join(s) for s in a]
m = [0, 1, 2, 3, 1, 4, 5, 1, 6, 5, 1, 5, 7, 8, 7, 9]

q = {}
for i in range(0x20, 0x7E):
q[MD5.hash(chr(i)).lower()] = i

import codecs
res = bytes([q[a[x].lower()] for x in m])
print(res.decode())
res = codecs.encode(res, 'hex').decode()
for i, c in enumerate(res):
print(f"set *(char*)0x{0x4C2196 + i:02X} = '{c}'")