Skip to content
{{ message }}

Instantly share code, notes, and snippets.

# jhs7jhs/n1ctf.md

Created Oct 19, 2020
simple write-up (oflo, fixed camera, n1vault)

## oflo

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.

1. Use /bin/cat to something to get a string
2. XOR the prologue of a function by the first 5 bytes of the given input.
3. 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}'


## fixed camera

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

### n1egg

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

## n1vault

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:

1. Every part of the input data is digested in to SHA256 and compared with the fixed value (memcmp above), except data[0x10B0::2]. This means we cannot change except data[0x10B0::2] to pass the SHA256 checking logic.
2. v5 is the CRC64 value of the data and v4 is 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 $CRC(x \oplus y) = CRC(x) \oplus CRC(y)$. In this challenge, the starting value of CRCk is (1 << k) - 1, so this is a bit different: $CRC(x \oplus y) = CRC(x) \oplus CRC(y) \oplus CRC(0)$.

From this, for each unknown bit $x_0, x_1, ..., x_{95}$, this challenge asks: $CRC(x_0 \oplus x_1 \oplus \ldots \oplus x_{95}) = 0$. By using the property above, we can make this into $CRC(x_0) \oplus CRC(x_1) \oplus \ldots \oplus CRC(x_{95}) \oplus CRC(0) = 0$.

For $k$-th bit of CRC, we can think like: $CRC(x_0)_k \oplus CRC(x_1)k \oplus \ldots \oplus CRC(x{95})_k \oplus CRC(0)_k = 0$. Moreover, we can think like this way: $CRC(x_0 | x_0=1)k \cdot x_0 \oplus CRC(x_1 | x_1=1)k \cdot x_1 \oplus \ldots \oplus CRC(x{95} | x{95} = 1)k \cdot x{95} \oplus CRC(0)k = 0$. We know that XOR operator is same as addition on mod 2. Therefore, this is a linear equation over mod 2 on $x_0$ to $x{95}$.

For each bit of CRC64, we can get 64 linear equations over mod 2 on $x_0$ to $x_{95}$. For each bit of CRC32, we can get 32 linear equations over mod 2 on $x_0$ to $x_{95}$. These are total 96 equations, and unknown bits are total 96 bits. We can solve this by gauss elimination.

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

### jhs7jhs commented Oct 19, 2020

 Sorry for latex tag (), plz download and watch.
to join this conversation on GitHub. Already have an account? Sign in to comment