Rating:

TechSupport
-----------
When connecting to the remote server, we get something like:


Thank you for contacting Chemisoft technical support.
foo
Is your keyboard properly connected to the computer? yes
Did you experience similar problems with other software as well? no
Does the program persists when you are not looking at it? yes
I heard sometimes bugs are caused by the presence of floppy drives. Do you have one? no

Alright then - it looks like we ruled out the most common problems.
So, let me now look at the program for you.
I am going to use port 3456. Everything ready on your side? yes


Then we get an incoming GDB connection on 3456. We set up a GDB server that runs the provided mcalc binary (a simple molecular weight calculator). It complains about the invalid license, and the challenge server performs integrity checks via GDB to protect from patching. Those can be bypassed either by writing a fake GDB server, or by patching the binary in a way that is not detected (most of the time):


004016fe mov dword [rbp-0x30 {var_38}], 0xabc8eef
00401705 mov dword [rbp-0x2c {var_34}], 0xb096bff4
0040170c mov dword [rbp-0x28 {var_30}], 0xe0c54799
00401713 mov dword [rbp-0x24 {var_2c}], 0x68cbc732
0040171a nop
(...)
00401721 nop


Now the helpdesk says the program worked fine. We patch in a int 3 at 0x401abb to make it crash, it says it'll try to reproduce the bug. So we need to make the original binary crash through the input formula only.

It is possible to force a division by zero. main calculates the total weight as sum of every count by its atom's weight, and if greater than 1000 it calls the sub at 0x40190b to print stats about the main element. In the sub, the total weight (re-calculated) is used as denominator. Because of how the element-count mapping array in main is populated, if the same element is repeated multiple times, main will sum all occurrences, while the sub's total will only use the rightmost occurrence. So if we find a chemical formula that overflows the 32-bit sum to zero, then prepend an atom (with weight > 1000) that is already in the formula, we get a weight greater than 1000 in main but equal to zero in the sub, causing a division by zero. Such a formula can be found as a solution to an LP problem (minimizing used atoms, as there is a length limit).

Once the remote reproduction crashes, too, the helpdesk prints out the differences between the two states (our crash and reproduced crash). If a register is a valid memory address, it shows a dereferenced qword. So plan is: control a remote register at the crash to dereference the valid license buffer (which is reasonably the flag).

Before calling the sub, ecx contains the total weight as calculated in main, and it is not touched by the sub before the division. We build a formula so that the right portion overflows to zero (to trigger the crash), and the left portion uses only atoms already present in the right portion and sums to the address we want to read (to set ecx). License is 16 bytes at 0x6033d0, so two crafted formulas later, we have the flag.

Script to generate the formulas:

python
#!/usr/bin/python3

import struct
import pulp

with open('mcalc', 'rb') as f:
f.seek(0x30a0)

atoms = []
for i in range(0, len(raw_atoms), 8):
raw_atom = raw_atoms[i:i+8]
atom = (raw_atom[:4].strip(b'\x00').decode('ascii'), struct.unpack('<I', raw_atom[4:])[0])
atoms.append(atom)

def formula(goal):
prob = pulp.LpProblem('Formula Left', pulp.LpMinimize)
cnt = pulp.LpVariable.dicts('cnt', range(len(atoms)), lowBound=0, upBound=999, cat='Integer')
used = pulp.LpVariable.dicts('used', range(len(atoms)), cat='Binary')
prob += sum(used)
prob += sum(atoms[i][1] * cnt[i] for i in range(len(atoms))) == GOAL
prob += sum(cnt) > 0
for i in range(len(atoms)):
prob += used[i] <= cnt[i], 'C_{}_upper'.format(i)
prob += cnt[i] <= 10000*used[i], 'C_{}_lower'.format(i)

prob.solve()

formula = ''
first_part_atoms = []
for i in range(len(atoms)):
value = int(pulp.value(cnt[i]))
if value > 0:
formula += '{}{}'.format(atoms[i][0], value if value > 1 else '')
first_part_atoms.append(i)

prob = pulp.LpProblem('Formula Right', pulp.LpMinimize)
cnt = pulp.LpVariable.dicts('cnt', range(len(atoms)), lowBound=0, upBound=999, cat='Integer')
used = pulp.LpVariable.dicts('used', range(len(atoms)), cat='Binary')
prob += sum(used)
prob += sum(atoms[i][1] * cnt[i] for i in range(len(atoms))) == 2**32
prob += sum(cnt) > 0
for i in range(len(atoms)):
prob += used[i] <= cnt[i], 'C_{}_upper'.format(i)
prob += cnt[i] <= 10000*used[i], 'C_{}_lower'.format(i)
for i in first_part_atoms:
prob += used[i] == True

prob.solve()

for i in range(len(atoms)):
value = int(pulp.value(cnt[i]))
if value > 0:
formula += '{}{}'.format(atoms[i][0], value if value > 1 else '')
return formula