Post

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!!!!}

This post is licensed under CC BY 4.0 by the author.