This crackme-style reversing task consisted of an x86_64 binary whose main operations are:
data
of 128-bit constants.flag\{[0-9a-f]{32}\}
.x
.v
with the value -1 (all bits set).c
in "flag{" + x + "}"
:
v := data[(v&0xff) ^ c] ^ (v >> 8)
.v
in little-endian representation equals x
.There seems to be little hope to solve this problem generally for a “random” data
array, hence we first focused on finding useful structure in the algorithm that generates the constants:
bit = 8
off = 128
val = 1
data[0] = 0
do {
lsb = val & 1
val >>= 1
if (lsb) {
val ^= 0xb595cf9c8d708e2166d545cf7cfdd4f9
}
ctr = 0
do {
data[off+ctr] = data[ctr] ^ val;
} while ((ctr += 2*off) < 0x100)
off >>= 1
} while (--bit)
The first part of the outer loop (shift and XOR) looks like division by
poly := 0xb595cf9c8d708e2166d545cf7cfdd4f9 << 1 | 1
Hence the value of val
ranges through data[i]
with the i
set. In other words:
(Here,
sage
quickly confirms this:
poly = 0xb595cf9c8d708e2166d545cf7cfdd4f9 << 1 | 1
F.<t> = GF(2**128, modulus = sum(int(c)*x**i for i,c in enumerate(bin(poly)[2:][::-1])))
es = []
for l in open('data.txt').read().strip().split('\n'):
lo,hi = map(int, l.strip().replace(' ','').rstrip(',').split(','))
es.append(F.fetch_int(hi<<64 | lo))
for i in range(256):
assert es[i] == F.fetch_int(i) / t**8
Revisiting the flag checking algorithm with this new information, we realize that
v := data[(v&0xff) ^ c] ^ (v >> 8)
is just an addition of the flag byte followed by a division by v
in the beginning (hence making it bigger than 128 bits).
This point of view implies that the output of the flag processing loop is nothing more than a computation of the form
where flag{...}
around the input and the initialization of v
as -1.
The main consequence of this is: the flag checking algorithm is an affine function over the vector space
Hence, we can recover
With that description, it is easy to find a fixed point: If sage
script does (it needs the values initialized in the first sage
snippet above):
v2n = lambda v: int(''.join(map(str, v)), 2)
n2v = lambda n: vector(GF(2), '{:0128b}'.format(n))
n2s = lambda n: struct.pack('<QQ', n&2**64-1, n>>64).encode('hex')
flagify = lambda v: 'flag{' + n2s(v2n(v)) + '}'
def compute(vecflag):
assert vecflag in GF(2)**128
hexflag = flagify(vecflag)
decflag = hexflag[:5] + hexflag[5:-1].decode('hex') + hexflag[-1:]
v = -1 & 2**128-1
assert len(decflag) == 22
for c in map(ord, decflag):
v = (v >> 8) ^^ (es[(c ^^ v) & 0xff]).integer_representation()
return n2v(v)
origin = compute(vector(GF(2),128))
A = []
for i in range(128):
v = vector(GF(2), (j == i for j in range(128)))
A.append(compute(v) - origin)
sol = (matrix(A) - 1).solve_left(origin)
flag = flagify(sol)
print('input: {:#x}'.format(v2n(sol)))
print('output: {:#x}'.format(v2n(compute(sol))))
print('--> {}'.format(compute(sol) == sol))
print(flag)
Running this code yields the flag:
$ ./pwn.sage
input: 0xa4efbaba0fa55545faf7b779c3440367
output: 0xa4efbaba0fa55545faf7b779c3440367
--> True
flag{670344c379b7f7fa4555a50fbabaefa4}
$
One can also compute
prfx = b'flag{'
tpl = prfx + b'\0'*16 + b'}'
R.<x> = F[]
f = sum(t**i for i in range(128))
f += sum(F.fetch_int(ord(c))*t**(8*i) for i,c in enumerate(tpl))
f += t**(8*len(prfx))*x
f /= t**(8*22)
for sol,_ in (f - x).roots():
print('flag{' + n2s(sol.integer_representation()) + '}')