Nullcon HackIM CTF 2025 (Reverse)
flag checker
Description |
---|
All you need to do is to guess the flag! |
Observation
Running in IDA , upon entering the text, it will check on sub_127A
to see the input return value. Below is main function
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
__int64 __fastcall main(int a1, char **a2, char **a3)
{
char s[40]; // [rsp+0h] [rbp-30h] BYREF
unsigned __int64 v5; // [rsp+28h] [rbp-8h]
v5 = __readfsqword(0x28u);
printf("Enter the flag: ");
fgets(s, 35, stdin);
s[strcspn(s, "\n")] = 0;
if ( (unsigned int)sub_127A(s) )
puts("Correct!");
else
puts("Incorrect!");
return 0LL;
}
Below is sub_127A
,first the code will check the length (strlen(a) != 34), and then it go to sub_11E9
. Lastly, the validation start with not equal (!=) condition of bytes_2020[i]
will check if the input was correct each bytes:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
__int64 __fastcall sub_127A(const char *a1)
{
int i; // [rsp+1Ch] [rbp-34h]
char v3[40]; // [rsp+20h] [rbp-30h] BYREF
unsigned __int64 v4; // [rsp+48h] [rbp-8h]
v4 = __readfsqword(0x28u);
if ( strlen(a1) != 34 )
return 0LL;
sub_11E9(a1, v3);
for ( i = 0; i <= 33; ++i )
{
if ( v3[i] != byte_2020[i] )
return 0LL;
}
return 1LL;
}
sub_11E9
, looks like the formula start here, it takes two memory addresses, a1 (input) and a2 (output). It loop for 34 times, processing each byte from a1 by first XORing it with 0x5A. Then, it performs a bitwise transformation on the stored byte by rotating it right by 5 bits and left.:
1
2
3
4
5
6
7
8
9
10
11
12
13
_BYTE *__fastcall sub_11E9(__int64 a1, __int64 a2)
{
_BYTE *result; // rax
int i; // [rsp+1Ch] [rbp-4h]
for ( i = 0; i <= 33; ++i )
{
*(_BYTE *)(i + a2) = (*(_BYTE *)(i + a1) ^ 0x5A) + i;
result = (_BYTE *)(i + a2);
*result = (*result >> 5) | (8 * *result);
}
return result;
}
Looking at the .rodata section of the byte_2020
, and array was stored in hex. we can change it to decimal and start decode:
Before:
.rodata:0000000000002020 ; _BYTE byte_2020[34]
.rodata:0000000000002020 byte_2020 db 0F8h, 0A8h, 0B8h, 21h, 60h, 73h, 90h, 83h, 80h, 0C3h
.rodata:0000000000002020 ; DATA XREF: sub_127A+63↑o
.rodata:0000000000002020 db 9Bh, 80h, 0ABh, 9, 59h, 0D3h, 21h, 0D3h, 0DBh, 0D8h
.rodata:0000000000002020 db 0FBh, 49h, 99h, 0E0h, 79h, 3Ch, 4Ch, 49h, 2Ch, 29h
.rodata:0000000000002020 db 0CCh, 0D4h, 0DCh, 42h
After:
.rodata:0000000000002020 ; _BYTE byte_2020[34]
.rodata:0000000000002020 byte_2020 db 248, 168, 184, 33, 96, 115, 144, 131, 128, 195, 155
.rodata:0000000000002020 ; DATA XREF: sub_127A+63↑o
.rodata:0000000000002020 db 128, 171, 9, 89, 211, 33, 211, 219, 216, 251, 73, 153
.rodata:0000000000002020 db 224, 121, 60, 76, 73, 44, 41, 204, 212, 220, 66
Solution
Original Process: The C function sub_11E9
applies the following steps to each byte:
- XORs the byte with 0x5A
- Adds the current index
- Rotates the resulting byte (right by 5 bits, left by 3 bits)
Reverse Process: To reverse the transformation here how it done:
- Reverses the bitwise rotation
- Subtracts the index value
- XOR with 0x5A to restore the original byte
Code:
1
2
3
4
5
6
def rev(e):
return bytes(((b << 5 & 0xFF | b >> 3) - i & 0xFF) ^ 0x5A for i, b in enumerate(e))
e = [248,168,184,33,96,115,144,131,128,195,155,128,171,9,89,211,33,211,219,216,251,73,153,224,121,60,76,73,44,41,204,212,220,66]
print(rev(e).decode())
Execution
1
2
3
┌──(myenv)(osiris㉿ALICE)-[~/Downloads/CTF/nullcon/rev/flag_check]
└─$ python sol.py
ENO{R3V3R53_3NG1N33R1NG_M45T3R!!!}
scrambled
Description |
---|
I am so close to finding the secret of immortality, however the code has been lost for ages. |
I managed to get all the parts back and even got to know that the key to success is one bite of the forbidden fruit (the scrambled eggs!). |
Can you help me to decipher the rest? |
main.py:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import random
def encode_flag(flag, key):
xor_result = [ord(c) ^ key for c in flag]
chunk_size = 4
chunks = [xor_result[i:i+chunk_size] for i in range(0, len(xor_result), chunk_size)]
seed = random.randint(0, 10)
random.seed(seed)
random.shuffle(chunks)
scrambled_result = [item for chunk in chunks for item in chunk]
return scrambled_result, chunks
def main():
flag = "REDACTED"
key = REDACTED
scrambled_result, _ = encode_flag(flag, key)
print("result:", "".join([format(i, '02x') for i in scrambled_result]))
if __name__ == "__main__":
main()
output.txt:
1
result: 1e78197567121966196e757e1f69781e1e1f7e736d6d1f75196e75191b646e196f6465510b0b0b57
Observation
The main.py
script scrambles the flag by:
- XOR each character with a key.
- Splitting the result into chunks of 4 bytes.
- Shuffling the chunks using a random seed.
Solution
To decode the scrambled flag, the solution script:
- Converts the scrambled hex output back into bytes.
- Reverses the shuffling using brute force on the seed (range 0-10).
- XORs each byte with the key to retrieve the original text.
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
import random
def decode_flag(scrambled_result, key, seed):
chunk_size = 4
chunks = [scrambled_result[i:i+chunk_size] for i in range(0, len(scrambled_result), chunk_size)]
random.seed(seed)
shuffled_indices = list(range(len(chunks)))
random.shuffle(shuffled_indices)
unshuffled_chunks = [None] * len(chunks)
for i, idx in enumerate(shuffled_indices):
unshuffled_chunks[idx] = chunks[i]
unshuffled_result = [item for chunk in unshuffled_chunks for item in chunk]
flag = ''.join([chr(c ^ key) for c in unshuffled_result])
return flag
def main():
scrambled_hex = "1e78197567121966196e757e1f69781e1e1f7e736d6d1f75196e75191b646e196f6465510b0b0b57"
scrambled_result = [int(scrambled_hex[i:i+2], 16) for i in range(0, len(scrambled_hex), 2)]
key = 42
for seed in range(11):
try:
flag = decode_flag(scrambled_result, key, seed)
if flag.startswith('ENO{'):
print(f"Seed: {seed}, Flag: {flag}")
break
except Exception as e:
print(f"Error with seed {seed}: {e}")
continue
if __name__ == "__main__":
main()
Execution
1
2
3
┌──(myenv)(osiris㉿ALICE)-[~/Downloads/CTF/nullcon/rev/scramble]
└─$ python solution.py
Seed: 10, Flag: ENO{5CR4M83L3D_3GG5_4R3_1ND33D_T45TY!!!}