pacman
Initial analysis
In Ghidra, there are only a few available functions, but one is suspiciously called after it is passed as a function parameter. This indicates self-modifying code, especially since the target function has no legible decompilation.
Debugging
After stepping through, I found a quick way to debug immediately.
1
2
3
sstart
b *0x4012c8
b *0x40136e
sstart
is a great pwndbg
command that stops once it gets to __libc_start_main
. Even though we can’t find it in the binary, sstart
identifies it.
0x4012c8
is main
, I think, and 0x40136e
begins the input check (after the 32-byte length input check).
The function at 0x401234 receives user input doubled:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
=> 0x401234: endbr64
0x401238: push rbp
0x401239: mov rbp,rsp
0x40123c: push rbx
0x40123d: sub rsp,0x28
0x401241: mov QWORD PTR [rbp-0x30],rdi
0x401245: mov rax,QWORD PTR [rbp-0x30]
0x401249: mov QWORD PTR [rbp-0x20],rax
0x40124d: mov rax,QWORD PTR [rbp-0x30]
0x401251: add rax,0x8
0x401255: mov QWORD PTR [rbp-0x18],rax
0x401259: mov DWORD PTR [rbp-0x24],0x0
0x401260: jmp 0x4012ba
0x401262: mov rax,QWORD PTR [rbp-0x18]
0x401266: mov rax,QWORD PTR [rax]
0x401269: mov QWORD PTR [rbp-0x10],rax
0x40126d: mov rax,QWORD PTR [rbp-0x20]
0x401271: mov rbx,QWORD PTR [rax]
0x401274: mov eax,DWORD PTR [rbp-0x24]
0x401277: cdqe
0x401279: lea rdx,[rax*8+0x0]
0x401281: lea rax,[rip+0xd98] # 0x402020
0x401288: mov rdx,QWORD PTR [rdx+rax*1]
0x40128c: mov rax,QWORD PTR [rbp-0x18]
0x401290: mov rax,QWORD PTR [rax]
0x401293: mov rsi,rdx
0x401296: mov rdi,rax
0x401299: call 0x4011f6
0x40129e: xor rbx,rax
0x4012a1: mov rdx,rbx
0x4012a4: mov rax,QWORD PTR [rbp-0x18]
0x4012a8: mov QWORD PTR [rax],rdx
0x4012ab: mov rax,QWORD PTR [rbp-0x20]
0x4012af: mov rdx,QWORD PTR [rbp-0x10]
0x4012b3: mov QWORD PTR [rax],rdx
0x4012b6: add DWORD PTR [rbp-0x24],0x1
0x4012ba: cmp DWORD PTR [rbp-0x24],0x3
0x4012be: jle 0x401262
0x4012c0: nop
0x4012c1: nop
0x4012c2: mov rbx,QWORD PTR [rbp-0x8]
0x4012c6: leave
0x4012c7: ret
We can also see that it uses these static values:
0x402020 = 0x1337deadbeef
0x402028 = 0xc0de12345678
0x402030 = =0xabcdef012345
0x402038 = 0x9876543210ab
Another function 0x4011f6 does the some obfuscation:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
0x4011f6: endbr64
0x4011fa: push rbp
0x4011fb: mov rbp,rsp
0x4011fe: mov QWORD PTR [rbp-0x8],rdi
0x401202: mov QWORD PTR [rbp-0x10],rsi
0x401206: mov rax,QWORD PTR [rbp-0x10]
0x40120a: xor QWORD PTR [rbp-0x8],rax
0x40120e: mov rax,QWORD PTR [rbp-0x8]
0x401212: rol rax,0xd
0x401216: mov rcx,rax
0x401219: mov rdx,QWORD PTR [rbp-0x8]
0x40121d: mov rax,rdx
0x401220: shl rax,0x5
0x401224: sub rax,rdx
0x401227: xor rax,rcx
0x40122a: mov QWORD PTR [rbp-0x8],rax
0x40122e: mov rax,QWORD PTR [rbp-0x8]
0x401232: pop rbp
0x401233: ret
Converting it to C, we can see what it does:
1
2
3
4
5
6
uint64_t obfuscate(uint64_t a, uint64_t b) {
uint64_t x = a ^ b;
uint64_t rotated = (x << 13) | (x >> (64 - 13)); // rotate left by 13
uint64_t multiplied = x * 31;
return rotated ^ multiplied;
}
At 0x4013c0, the binary makes a call to memcmp on 32 bytes. If the return is 0, prints a checkmark. Otherwise, prints an x.
1
2
3
4
5
6
7
8
9
10
pwndbg> x/8gx $rdi
0x7fffffffe0e0: 0xdf30eab81b7a53c4 0xe34d873146e92fdc
0x7fffffffe0f0: 0x14afed086ba1f200 0x4d59d1e3fc90662b
0x7fffffffe100: 0x3837363534333231 0x6867666564636261
0x7fffffffe110: 0x3837363534333231 0x4847464544434241
pwndbg> x/8gx $rsi
0x402040: 0xfd83487a8f04bc91 0x1ea9b29316416331
0x402050: 0x2fbea4546b08944f 0x922e9e7e9854dcaf
0x402060: 0x6170207265746e45 0x6573617268707373
0x402070: 0x6168632032332820 0x000a00203a297372
Script
After enough reversing, I was able to replicate the binary in Python:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#!/usr/bin/env python3
target = [
0xfd83487a8f04bc91,
0x1ea9b29316416331,
0x2fbea4546b08944f,
0x922e9e7e9854dcaf,
]
keys = [
0x1337deadbeef,
0xc0de12345678,
0xabcdef012345,
0x9876543210ab,
]
mask64 = (1 << 64) - 1
def obfuscate(quarter, key):
x = quarter ^ key
sh13 = (x << 13) & mask64
rotated = sh13 | (x >> (64 - 13))
x31 = (x * 31) & mask64
return rotated ^ x31
def cycle
def transform(quarter1, quarter2):
for key in keys:
temp = quarter2
quarter2 = quarter1 ^ obfuscate(quarter2, key)
quarter1 = temp
return quarter1, quarter2
from Crypto.Util.number import bytes_to_long
quarters = [
bytes_to_long(b"87654321"),
bytes_to_long(b"87654321"),
bytes_to_long(b"87654321"),
bytes_to_long(b"87654321"),
]
quarters[0], quarters[1] = transform(quarters[0], quarters[1])
quarters[2], quarters[3] = transform(quarters[2], quarters[3])
for quarter in quarters:
print(f"0x{quarter:016x}")
1
2
3
4
5
6
pwndbg> x/4gx $rdi
0x7fffffffe0e0: 0x0dcdb523a4771273 0x5946edf14ec0ce14
0x7fffffffe0f0: 0x0dcdb523a4771273 0x5946edf14ec0ce14
pwndbg> x/4gx $rsi
0x402040: 0xfd83487a8f04bc91 0x1ea9b29316416331
0x402050: 0x2fbea4546b08944f 0x922e9e7e9854dcaf
Crypto problem!
My team helped me with this part, and they used AI to reverse the Feistel:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#!/usr/bin/env python3
from Crypto.Util.number import long_to_bytes
# comparison target
target = [
0xfd83487a8f04bc91,
0x1ea9b29316416331,
0x2fbea4546b08944f,
0x922e9e7e9854dcaf,
]
keys = [
0x1337deadbeef,
0xc0de12345678,
0xabcdef012345,
0x9876543210ab,
]
mask64 = (1 << 64) - 1
def obfuscate(quarter, key):
x = quarter ^ key
sh13 = (x << 13) & mask64
rotated = sh13 | (x >> (64 - 13))
x31 = (x * 31) & mask64
return rotated ^ x31
def inverse_transform(l, r):
# undo 4 Feistel rounds in reverse key order
for key in reversed(keys):
prev_r = l
prev_l = r ^ obfuscate(prev_r, key)
l, r = prev_l, prev_r
return l, r
# apply inverse to both pairs of targets
q0, q1 = inverse_transform(target[0], target[1])
q2, q3 = inverse_transform(target[2], target[3])
# convert back to bytes and concatenate
flag_bytes = long_to_bytes(q0) \
+ long_to_bytes(q1) \
+ long_to_bytes(q2) \
+ long_to_bytes(q3)
print(flag_bytes.decode())
The flag: flag: L3AK{feistel_netWork_Is_fun!!!!}