Added writeups
This commit is contained in:
275
the_clockwork.md
Normal file
275
the_clockwork.md
Normal file
@@ -0,0 +1,275 @@
|
||||
# The Clockwork
|
||||
|
||||
`the_clockwork` is a reverse engineering challenge involving a system of interdependent equations. We are provided with a binary `challenge` and need to find the correct input to satisfy its internal logic.
|
||||
|
||||
## Information Gathering
|
||||
|
||||
```bash
|
||||
$ file challenge
|
||||
challenge: ELF 64-bit LSB executable, x86-64, ... not stripped
|
||||
```
|
||||
|
||||
The binary is not stripped, revealing function names. We analyze it using Ghidra.
|
||||
|
||||
## Reverse Engineering
|
||||
|
||||
### Main Function
|
||||
|
||||
We locate the `main` function (`0x402057`). The decompilation reveals the initialization of a target array and a loop verifying the calculated "gears".
|
||||
|
||||
```c
|
||||
undefined8 main(void)
|
||||
|
||||
{
|
||||
bool bVar1;
|
||||
int iVar2;
|
||||
char *pcVar3;
|
||||
size_t sVar4;
|
||||
long in_FS_OFFSET;
|
||||
int local_164;
|
||||
int local_158 [64];
|
||||
char local_58 [72];
|
||||
long local_10;
|
||||
|
||||
local_10 = *(long *)(in_FS_OFFSET + 0x28);
|
||||
local_158[0] = 0x174;
|
||||
local_158[1] = 0x2fe;
|
||||
local_158[2] = 0x3dc;
|
||||
local_158[3] = 0x30c;
|
||||
local_158[4] = 0xfffffe57;
|
||||
local_158[5] = 0xffffffc6;
|
||||
local_158[6] = 0x28a;
|
||||
local_158[7] = 0x23d;
|
||||
local_158[8] = 0x24d;
|
||||
local_158[9] = 0xee;
|
||||
local_158[10] = 0x183;
|
||||
local_158[0xb] = 0x124;
|
||||
local_158[0xc] = 0x1e0;
|
||||
local_158[0xd] = 0x19c;
|
||||
local_158[0xe] = 0x1ab;
|
||||
local_158[0xf] = 0x444;
|
||||
// ... (initialization continues for 32 values) ...
|
||||
local_158[0x1f] = 0x209;
|
||||
|
||||
// ... (input reading logic) ...
|
||||
|
||||
if (sVar4 == 0x20) {
|
||||
// Calculate gears, storing result in the second half of local_158
|
||||
calculate_gears(local_58,local_158 + 0x20);
|
||||
bVar1 = true;
|
||||
local_164 = 0;
|
||||
goto LAB_00402348;
|
||||
}
|
||||
|
||||
// ...
|
||||
|
||||
LAB_00402348:
|
||||
if (0x1f < local_164) goto LAB_00402351;
|
||||
|
||||
// Constraint Check:
|
||||
// gears[next] * 2 + gears[current] == target[current]
|
||||
// where next = (current + 1) % 32
|
||||
if (local_158[(long)((local_164 + 1) % 0x20) + 0x20] * 2 + local_158[(long)local_164 + 0x20] !=
|
||||
local_158[local_164]) {
|
||||
bVar1 = false;
|
||||
goto LAB_00402351;
|
||||
}
|
||||
local_164 = local_164 + 1;
|
||||
goto LAB_00402348;
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
The loop at `LAB_00402348` verifies that for every gear `i`:
|
||||
`gears[i] + 2 * gears[(i+1)%32] == target[i]`
|
||||
|
||||
### Calculate Gears
|
||||
|
||||
The function `calculate_gears` computes the `gears` array from the input string.
|
||||
|
||||
```c
|
||||
void calculate_gears(char *param_1,undefined4 *param_2)
|
||||
|
||||
{
|
||||
undefined4 uVar1;
|
||||
|
||||
uVar1 = f0((int)*param_1);
|
||||
*param_2 = uVar1;
|
||||
uVar1 = f1((int)param_1[1],*param_2);
|
||||
param_2[1] = uVar1;
|
||||
uVar1 = f2((int)param_1[2]);
|
||||
param_2[2] = uVar1;
|
||||
uVar1 = f3((int)param_1[3],param_2[2]);
|
||||
param_2[3] = uVar1;
|
||||
|
||||
// ... Pattern continues ...
|
||||
|
||||
uVar1 = f30((int)param_1[0x1e]);
|
||||
param_2[0x1e] = uVar1;
|
||||
uVar1 = f31((int)param_1[0x1f],param_2[0x1e]);
|
||||
param_2[0x1f] = uVar1;
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
It uses 32 helper functions (`f0` through `f31`).
|
||||
- Even indices depend only on the input character: `gears[i] = f_i(input[i])`
|
||||
- Odd indices depend on the input and the previous gear: `gears[i] = f_i(input[i], gears[i-1])`
|
||||
|
||||
## Solution
|
||||
|
||||
Solving this challenge manually would be difficult because the equations are cyclic: `gears[0]` affects `gears[1]`, which affects `gears[2]`... and the verification loop wraps around so that `gears[31]` affects `gears[0]`.
|
||||
|
||||
Instead of calculating it by hand, we can use **Z3**, a powerful theorem prover from Microsoft. Z3 allows us to describe the problem as a set of logic constraints (e.g., "x is an integer," "y = x + 5," "y must equal 10") and then asks the engine to find values for `x` and `y` that satisfy all statements.
|
||||
|
||||
### Solver Construction
|
||||
|
||||
We build the solver step-by-step.
|
||||
|
||||
**1. Define Inputs**
|
||||
We start by defining our unknown inputs. We know the flag is 32 characters long, so we create 32 32-bit BitVectors. We also constrain them to be printable ASCII (32-126) because we know the flag is a string.
|
||||
|
||||
**2. Define Targets**
|
||||
We extract the target values directly from the `main` function's stack initialization code. These are the values our gears must align with.
|
||||
|
||||
**3. Replicate Helper Functions**
|
||||
We need to tell Z3 how to calculate the gears. We take the logic from `f0`, `f1`, etc., and rewrite it in Python.
|
||||
For example, `f0` in C is `return (char)(param_1 ^ 0x55) + 10;`.
|
||||
In Python for Z3, we write `return c_char(p1 ^ 0x55) + 10`.
|
||||
Note that `f1` uses modulo 200. In C, `%` on negative numbers can be tricky, but `SRem` (Signed Remainder) in Z3 matches the C behavior.
|
||||
|
||||
**4. Build the Gears Array**
|
||||
We programmatically construct the list of gear values.
|
||||
`gears[0]` is the result of `f0(flag[0])`.
|
||||
`gears[1]` is the result of `f1(flag[1], gears[0])`.
|
||||
We do this for all 32 gears, following the pattern found in `calculate_gears`.
|
||||
|
||||
**5. Add Constraints**
|
||||
Finally, we add the condition found in the `main` loop: `gears[i] + 2 * gears[(i+1)%32] == targets[i]`. This links everything together into a solvable system.
|
||||
|
||||
**6. Solve**
|
||||
We ask Z3 to check if there is a solution (`s.check()`). If it finds one, we extract the values of our flag variables and print them as characters.
|
||||
|
||||
### Final Solver Script
|
||||
|
||||
```python
|
||||
import z3
|
||||
|
||||
s = z3.Solver()
|
||||
|
||||
# 1. Define inputs (32 chars)
|
||||
flag = [z3.BitVec(f'flag_{i}', 32) for i in range(32)]
|
||||
|
||||
# 2. Constrain to Printable ASCII (The only hint we need)
|
||||
for i in range(32):
|
||||
s.add(flag[i] >= 32)
|
||||
s.add(flag[i] <= 126)
|
||||
|
||||
# 3. The Target Values (Extracted from your decompilation)
|
||||
# These correspond to local_158[0] through local_158[31]
|
||||
targets = [
|
||||
0x174, 0x2fe, 0x3dc, 0x30c, 0xfffffe57, 0xffffffc6, 0x28a, 0x23d,
|
||||
0x24d, 0xee, 0x183, 0x124, 0x1e0, 0x19c, 0x1ab, 0x444,
|
||||
0xffffffc8, 0xffffff4c, 0x13c, 0x25e, 0x1fe, 0x18a, 200, 0x82,
|
||||
0x233, 0x2da, 0x36e, 0x3c3, 0x47d, 0x2a4, 0x3b5, 0x209
|
||||
]
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# HELPER FUNCTIONS (The Gears)
|
||||
# We use the Unsigned Logic (0-255) that worked for you before.
|
||||
# ---------------------------------------------------------
|
||||
|
||||
def c_char(x): return x & 0xFF # Treat char as unsigned (0-255)
|
||||
def c_rem(a, b): return z3.SRem(a, b) # Signed Remainder
|
||||
|
||||
def f0(p1): return c_char(p1 ^ 0x55) + 10
|
||||
def f1(p1, p2): return c_rem((p1 + p2), 200)
|
||||
def f2(p1): return p1 * 3 - 20
|
||||
def f3(p1, p2): return (p1 ^ p2) + 5
|
||||
def f4(p1): return (p1 + 10) ^ 0xaa
|
||||
def f5(p1, p2): return (p1 - p2) * 2
|
||||
def f6(p1): return p1 + 100
|
||||
def f7(p1, p2): return (p1 ^ p2) + 12
|
||||
def f8(p1): return (p1 * 2) ^ 0xff
|
||||
def f9(p1, p2): return p2 + p1 - 50
|
||||
def f10(p1): return c_char(p1 ^ 123)
|
||||
def f11(p1, p2): return c_rem((p1 * p2), 500)
|
||||
def f12(p1): return p1 + 1
|
||||
def f13(p1, p2): return (p1 ^ p2) * 2
|
||||
def f14(p1): return p1 - 10
|
||||
def f15(p1, p2): return (p2 + p1) ^ 0x33
|
||||
def f16(p1): return p1 * 4
|
||||
def f17(p1, p2): return (p1 - p2) + 100
|
||||
def f18(p1): return c_char(p1 ^ 0x77)
|
||||
def f19(p1, p2): return c_rem((p1 + p2), 150)
|
||||
def f20(p1): return p1 * 2
|
||||
def f21(p1, p2): return (p1 ^ p2) - 20
|
||||
def f22(p1): return p1 + 33
|
||||
def f23(p1, p2): return (p2 + p1) ^ 0xcc
|
||||
def f24(p1): return p1 - 5
|
||||
def f25(p1, p2): return c_rem((p1 * p2), 300)
|
||||
def f26(p1): return p1 ^ 0x88
|
||||
def f27(p1, p2): return p2 + p1 - 10
|
||||
def f28(p1): return p1 * 3
|
||||
def f29(p1, p2): return (p1 ^ p2) + 44
|
||||
def f30(p1): return p1 + 10
|
||||
def f31(p1, p2): return (p2 + p1) ^ 0x99
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# CALCULATE GEARS
|
||||
# ---------------------------------------------------------
|
||||
gears = [0] * 32
|
||||
gears[0] = f0(flag[0])
|
||||
gears[1] = f1(flag[1], gears[0])
|
||||
gears[2] = f2(flag[2])
|
||||
gears[3] = f3(flag[3], gears[2])
|
||||
gears[4] = f4(flag[4])
|
||||
gears[5] = f5(flag[5], gears[4])
|
||||
gears[6] = f6(flag[6])
|
||||
gears[7] = f7(flag[7], gears[6])
|
||||
gears[8] = f8(flag[8])
|
||||
gears[9] = f9(flag[9], gears[8])
|
||||
gears[10] = f10(flag[10])
|
||||
gears[11] = f11(flag[11], gears[10])
|
||||
gears[12] = f12(flag[12])
|
||||
gears[13] = f13(flag[13], gears[12])
|
||||
gears[14] = f14(flag[14])
|
||||
gears[15] = f15(flag[15], gears[14])
|
||||
gears[16] = f16(flag[16])
|
||||
gears[17] = f17(flag[17], gears[16])
|
||||
gears[18] = f18(flag[18])
|
||||
gears[19] = f19(flag[19], gears[18])
|
||||
gears[20] = f20(flag[20])
|
||||
gears[21] = f21(flag[21], gears[20])
|
||||
gears[22] = f22(flag[22])
|
||||
gears[23] = f23(flag[23], gears[22])
|
||||
gears[24] = f24(flag[24])
|
||||
gears[25] = f25(flag[25], gears[24])
|
||||
gears[26] = f26(flag[26])
|
||||
gears[27] = f27(flag[27], gears[26])
|
||||
gears[28] = f28(flag[28])
|
||||
gears[29] = f29(flag[29], gears[28])
|
||||
gears[30] = f30(flag[30])
|
||||
gears[31] = f31(flag[31], gears[30])
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# ADD CONSTRAINTS (The Chain Link)
|
||||
# Logic: gears[i] + 2 * gears[next] == target[i]
|
||||
# ---------------------------------------------------------
|
||||
for i in range(32):
|
||||
next_idx = (i + 1) % 32
|
||||
s.add((gears[i] + gears[next_idx] * 2) == targets[i])
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# SOLVE
|
||||
# ---------------------------------------------------------
|
||||
print("Solving new unique constraints...")
|
||||
if s.check() == z3.sat:
|
||||
m = s.model()
|
||||
result = ""
|
||||
for i in range(32):
|
||||
result += chr(m[flag[i]].as_long())
|
||||
print("\n[+] FOUND FLAG:", result)
|
||||
else:
|
||||
print("unsat - No solution found.")
|
||||
```
|
||||
Reference in New Issue
Block a user