There are several anit-reversing logic, so I just patched with \x90 (nop instruction) to avoid them. After this process, it was able to figure out the logic of the program.
- Use
/bin/catto something to get a string - XOR the prologue of a function by the first 5 bytes of the given input.
- XOR the given input and the string from 1., then check the result is right.
The part 2. is easy to patch, because the first 5 bytes of the given input is always n1ctf.
Then, I found out that 1. give us the string starts with Linux version with gdb. so it was able to get flag like this:
>>> arr
[53, 45, 17, 26, 73, 125, 17, 20, 43, 59, 62, 61, 60, 95]
>>> arr2
[76, 105, 110, 117, 120, 32, 118, 101, 114, 115, 105, 111, 110, 32]
>>> s = ''
>>> for i in range(len(arr)):
... s += chr(arr[i] ^ (arr2[i] + 2))
...
>>> s
'{Fam3_is_NULL}'
The given Unity binary uses il2cpp + mono. It is easily reversible with Il2CppDumper.
With the dumper, it was able to find out that there's a class named cam and it has the main logic.
The structure of the member of the class was like:
00000000 cam_Fields struc ; (sizeof=0x48, align=0x8, copyof_17367)
00000000 ; XREF: cam_o/r
00000000 baseclass_0 UnityEngine_MonoBehaviour_Fields ?
00000008 text dq ? ; offset
00000010 encrypt_flag dq ? ; offset
00000018 speed dd ?
0000001C distance_v dd ?
00000020 distance_h dd ?
00000024 rotation_H_speed dd ?
00000028 rotation_V_speed dd ?
0000002C max_up_angle dd ?
00000030 max_down_angle dd ?
00000034 max_left_angle dd ?
00000038 max_right_angle dd ?
0000003C current_rotation_V dd ?
00000040 angleY dq ? ; offset
00000048 cam_Fields ends
The important field is angleY, because we cannot move over -9 degree or +9 degree because of the code. At this time, I decided to use cheat engine to change angleY value.
It was able to find angleY from the memory because of other member values.
void __stdcall cam___ctor(cam_o *this, const MethodInfo *method)
{
__int64 v3; // rdx
UnityEngine_MonoBehaviour_o *v4; // rbx
if ( !byte_180872E55 )
{
sub_18011A030(10994i64, (__int64)method);
byte_180872E55 = 1;
}
v3 = StringLiteral_3877;
this->fields.encrypt_flag = (struct System_String_o *)StringLiteral_3877;
sub_180119BC0((__int64)&this->fields.encrypt_flag, v3);
this->fields.speed = 20.0;
this->fields.rotation_H_speed = 1.0;
this->fields.rotation_V_speed = 1.0;
this->fields.max_up_angle = 80.0;
this->fields.max_down_angle = -60.0;
this->fields.max_left_angle = -30.0;
this->fields.max_right_angle = 30.0;
v4 = (UnityEngine_MonoBehaviour_o *)sub_18011A130(EncryptValue_TypeInfo);
UnityEngine_MonoBehaviour___ctor(v4, 0i64);
this->fields.angleY = (struct EncryptValue_o *)v4;
sub_180119BC0((__int64)&this->fields.angleY, (__int64)v4);
UnityEngine_MonoBehaviour___ctor((UnityEngine_MonoBehaviour_o *)this, 0i64);
}Member values like speed, max_up_angle, max_down_angle are never changed after the given constructor fix its value. Therefore, it was able to find the pointer angleY by finding values [20.0, 1.0, 1.0, 80.0, -60.0, -30.0, 30.0].
After this, I changed angleY to -120 and moved slowly to right. When angleY was -60, the flag was out, it was n1ctf{encrypt_value}.
About n1egg flag, it was able to get n1egg flag by just searching n1egg on the memory.
The flag was n1egg{you_found_the_eggs}.
There was a secret logic that triggered when SIGFPE is sent to the program. The main goal of the challenge is to trigger this secret logic.
There's only one point that SIGFPE signal can be triggered, with divide-by-zero exception.
if ( !memcmp(&s1, &s2, 0x20uLL)
&& !memcmp(&v14, &v50, 0x20uLL)
&& 0x422048F8DF49762ELL / (v5 | v4) == 1
&& v4 == 0x9E48562A
&& v5 == 0x422048F8DF41242ELL )If both v5 and v4 is zero, then it will be triggered.
After reversing binary, it was able to know that:
- Every part of the input data is digested in to SHA256 and compared with the fixed value (
memcmpabove), exceptdata[0x10B0::2]. This means we cannot change exceptdata[0x10B0::2]to pass the SHA256 checking logic. v5is the CRC64 value of the data andv4is the CRC32 value of the data.
So this challenge is about changing data[0x10B0::2] (total 12 bytes) to make both CRC32 and CRC64 to zero.
Usually, CRC has an interesting property, that (1 << k) - 1, so this is a bit different:
From this, for each unknown bit
For
For each bit of CRC64, we can get 64 linear equations over mod 2 on
The solver code is here:
#!/usr/bin/env sage
import struct
with open('credential.png', 'rb') as f:
data = f.read(0x10C8)
crc32_table = []
crc64_table = []
with open('n1vault', 'rb') as f:
f.seek(0x1960)
for i in range(256):
crc32_table.append(struct.unpack('<I', f.read(4))[0])
f.seek(0x1d60)
for i in range(256):
crc64_table.append(struct.unpack('<Q', f.read(8))[0])
def crc64(arr):
crc = (1 << 64) - 1
for c in arr:
crc = crc ^^ c
for i in range(8):
if crc & 1:
crc = (crc >> 1) ^^ crc64_table[0x80]
else:
crc = crc >> 1
return crc ^^ ((1 << 64) - 1)
def crc32(arr):
crc = (1 << 32) - 1
for c in arr:
crc = crc ^^ c
for i in range(8):
if crc & 1:
crc = (crc >> 1) ^^ crc32_table[0x80]
else:
crc = crc >> 1
return crc ^^ ((1 << 32) - 1)
mat = [ [0 for j in range(96)] for i in range(96)]
for i in range(12):
tmp = bytearray([0 for _ in range(0x10C8)])
for j in range(8):
tmp[0x10B0 + 2 * i] = 1 << j
v32 = crc32(tmp)
v64 = crc64(tmp)
for k in range(32):
bit = (v32 >> k) & 1
mat[k][i * 8 + j] = bit
for k in range(64):
bit = (v64 >> k) & 1
mat[k + 32][i * 8 + j] = bit
target = [ 0 for i in range(96) ]
v32 = crc32(data) ^^ crc32([0 for _ in range(0x10C8)])
v64 = crc64(data) ^^ crc64([0 for _ in range(0x10C8)])
for i in range(32):
bit = (v32 >> i) & 1
target[i] = bit
for i in range(64):
bit = (v64 >> i) & 1
target[i + 32] = bit
mat = Matrix(GF(2), mat)
ans = mat.solve_right(Matrix(GF(2), target).transpose())
tmp = bytearray(data)
for i in range(12):
for j in range(8):
val = int(ans[8 * i + j][0])
tmp[0x10B0 + 2 * i] = tmp[0x10B0 + 2 * i] ^^ (val << j)
with open('credential_false.png', 'wb') as f:
f.write(tmp)The flag was n1ctf{fa4bdf1d540831c88ca40794fc128f10}.
Sorry for latex tag (
$$), plz download and watch.