Alchemnesia
Category: rev
Disclaimer: The tl;dr of this writeup is “just hack into the binary lol” (since I’m pretty basic at rev). If you are looking for other “cooler” solutions this is probably not for you XD But personally this was really cool too!
Part I
Simple on the surface. We are given just 1 binary to fiddle around with. Let’s see what we can do!

Firstly, the program itself. The functionality is rather simple; you enter a certain number of bytes, then it applies some function to it and check whether it matches some other byte string. We can surely crack this open up and retrieve all the details!
… Not that simple! Turns out it is a statically-linked and stripped binary. But no worries, we can surely handle this! We can generally have a feel about whether it will be a libc function, but other than that it should not be too big of a deal.
However, upon opening [insert decompiler] (I used Ghdira in my case) we are immediately greeted with some unintelligible code:
undefined8 FUN_004035b6(void)
{
// ...
local_18 = &stack0x00000008;
local_48 = FUN_004031b3;
local_40 = FUN_00402dc1;
local_28 = &local_48;
local_38 = 0xfa1e0ff3;
local_34 = 0xbb49;
local_32 = FUN_00401dd5;
local_2a = 0xba49;
local_20 = 0x90e3ff49;
local_80 = &local_38;
local_58 = &stack0x00000008;
local_88 = FUN_004034b0;
local_68 = &local_88;
local_78 = 0xfa1e0ff3;
local_74 = 0xbb49;
local_72 = FUN_00401dd5;
local_6a = 0xba49;
local_60 = 0x90e3ff49;
(*(code *)&local_78)(FUN_00401e0c);
// ...
return 0;
}
We can clearly see that a bunch of function addresses are referenced, but the only function call is at the end. So what is happening?
If you stare hard enough at the statement
(*(code *)&local_78)(FUN_00401e0c);
things will gradually start to make sense. The address of local_78 is treated as code address, which means that 0xfa1e0ff3 (and whatever follows) should be assembly.
We also see that there are a lot of repeated instructions within the block, so perhaps these are just convoluted function calls! We shall zoom in on one snippet at local_78 by popping into gdb (gef):
gef➤ x/8gx $rax
0x7fffffffddf0: 0x1dd5bb49fa1e0ff3 0xba49000000000040
0x7fffffffde00: 0x00007fffffffdde0 0x00007fff90e3ff49
0x7fffffffde10: 0x00007fffffffde70 0x0000000000000002
0x7fffffffde20: 0x00000000004031b3 0x0000000000402dc1
gef➤ x/6i $rax
0x7fffffffddf0: endbr64
0x7fffffffddf4: movabs r11,0x401dd5
0x7fffffffddfe: movabs r10,0x7fffffffdde0
0x7fffffffde08: rex.WB jmp r11
0x7fffffffde0b: nop
0x7fffffffde0c: (bad)
Notice that 0x401dd5 is referenced for all of these snippets, so maybe it is somehow related. We can take a quick look at what it does:
gef➤ x/20i 0x401dd5
0x401dd5: endbr64
0x401dd9: push rbp
0x401dda: mov rbp,rsp
0x401ddd: push rbx
0x401dde: sub rsp,0x18
0x401de2: mov QWORD PTR [rbp-0x18],rdi
0x401de6: mov rbx,r10
0x401de9: mov QWORD PTR [rbp-0x20],r10
0x401ded: mov rdx,QWORD PTR [rbx+0x8]
0x401df1: mov rax,QWORD PTR [rbp-0x18]
0x401df5: mov rdi,rax
0x401df8: call rdx
0x401dfa: mov rdx,rax
0x401dfd: mov rax,QWORD PTR [rbx]
0x401e00: mov rdi,rdx
0x401e03: call rax
0x401e05: add rsp,0x18
0x401e09: pop rbx
0x401e0a: pop rbp
0x401e0b: ret
The focus lies on these few instructions:
0x401de6: mov rbx,r10
0x401ded: mov rdx,QWORD PTR [rbx+0x8]
0x401df8: call rdx
0x401dfd: mov rax,QWORD PTR [rbx]
0x401e03: call rax
If you trace hard enough (just somewhere above) you will find that, at least for this example,
r10is0x7fffffffdde0- which itself results from the assembly reading whatever is stored at
0x7fffffffde00 - which is
&local_78 + 0x10, i.e.&local_68 local_68itself is&local_88, i.e. “whatever is stored there” is just0x7fffffffdde0- and this address (
0x7fffffffdde0) itself storesFUN_004034b0, a function of interest. - since
rbx=r10, the first function called will be stored at&local_88+0x8, or&local_80.
In this case local_80 contains another such convoluted loop, but this is essentially what happens in these code. Then we can look at how the parameters are passed around, but in summary,
(*(code *)&local_78)(FUN_00401e0c);
can be “replaced” with
local_88(local_80(0x401e0c));
Looking above,
local_88is0x4034b0local_80is&local_38, almost identical to&local_78, hence we apply the same method and see thatlocal_48is0x4031b3local_40is0x402dc1
Thus we end up with something like
0x4034b0(0x4031b3(0x402dc1(0x401e0c)));
Honestly, that’s the main trick of the challenge tackled. Feel free to skip to somewhere near the end as it’s gonna get repetitive.
0x402dc1(param_1):
// ...
local_18 = &stack0x00000008;
local_48 = FUN_00402c89;
local_40 = FUN_00402c37;
local_28 = &local_48;
local_38 = 0xfa1e0ff3;
local_34 = 0xbb49;
local_32 = FUN_00401dd5;
local_2a = 0xba49;
local_20 = 0x90e3ff49;
local_80 = &local_38;
local_58 = &stack0x00000008;
local_88 = FUN_00402d50;
local_68 = &local_88;
local_78 = 0xfa1e0ff3;
local_74 = 0xbb49;
local_72 = FUN_00401dd5;
local_6a = 0xba49;
local_60 = 0x90e3ff49;
(*(code *)&local_78)(param_1);
// ...
becomes
0x402d50(0x402c89(0x402c37(param_1)));
0x402c37 looks like some libc function, so I will not bother with it.
0x402c89 and 0x402d50 are function to print the welcome message (you can take a look at it yourself).
And so we have completed 0x402dc1. Onto the next one, 0x4031b3:
long FUN_004031b3(void)
{
// ...
lVar1 = FUN_0042fef0(0x31);
FUN_00401110(lVar1,0,0x31);
FUN_00412340("Enter Flag: ");
for (local_15c = 0; local_15c < 0x30; local_15c = local_15c + 1) {
FUN_004124d0(&DAT_004b54d5,lVar1 + local_15c);
}
FUN_00422260("\nOh my, so you say you\'ve figured it out?");
FUN_00422260("I hope you\'re right, or we might go boom...");
FUN_00422260("\nAlchemy is pretty simple really...");
FUN_00422260("Mix and shake a couple of times, and it\'s done!");
local_18 = &stack0x00000008;
local_48 = FUN_00402f59;
local_40 = FUN_00402efa;
local_28 = &local_48;
local_38 = 0xfa1e0ff3;
local_34 = 0xbb49;
local_32 = FUN_00401dd5;
local_2a = 0xba49;
local_20 = 0x90e3ff49;
local_80 = &local_38;
local_58 = &stack0x00000008;
local_88 = FUN_00402fc4;
local_68 = &local_88;
local_78 = 0xfa1e0ff3;
local_74 = 0xbb49;
local_72 = FUN_00401dd5;
local_6a = 0xba49;
local_60 = 0x90e3ff49;
local_c0 = &local_78;
local_98 = &stack0x00000008;
local_c8 = FUN_00402f59;
local_a8 = &local_c8;
local_b8 = 0xfa1e0ff3;
local_b4 = 0xbb49;
local_b2 = FUN_00401dd5;
local_aa = 0xba49;
local_a0 = 0x90e3ff49;
local_100 = &local_b8;
local_d8 = &stack0x00000008;
local_108 = FUN_00403154;
local_e8 = &local_108;
local_f8 = 0xfa1e0ff3;
local_f4 = 0xbb49;
local_f2 = FUN_00401dd5;
local_ea = 0xba49;
local_e0 = 0x90e3ff49;
(*(code *)&local_f8)(lVar1);
// ...
return lVar1;
}
Wow, now there is some interesting stuff. We can sort of see that this is where the user inputs the byte string.
0x42fef0 and 0x401110 look like libc functions, and I’m guessing that they are malloc and memset respectively. Then we see some form of read and some form of write, and then it is the familiar code again, which simplifies to:
return 0x403154(0x402f59(0x402fc4(0x402f59(0x402efa(lVar1)))));
Wow, that’s a lot of functions! Maybe we will go all the way to the first such chain and look at the final function first, 0x4034b0:
long FUN_004034b0(long param_1)
{
int local_1c;
int local_18;
int local_14;
local_1c = 0;
while( true ) {
if (0x2f < local_1c) {
FUN_00422260(&DAT_004b5700);
return param_1;
}
if (*(char *)(param_1 + local_1c) != (&DAT_004e2100)[local_1c]) break;
local_1c = local_1c + 1;
}
FUN_00412340("\nExpected Result: ");
for (local_18 = 0; local_18 < 0x30; local_18 = local_18 + 1) {
FUN_00422690((int)(char)(&DAT_004e2100)[local_18]);
}
FUN_00412340("\n\nActual Result: ");
for (local_14 = 0; local_14 < 0x30; local_14 = local_14 + 1) {
FUN_00422690((int)*(char *)(param_1 + local_14));
}
FUN_00422260(&DAT_004b54fd);
FUN_00422260(&DAT_004b5500);
return param_1;
}
This function probably does not require much explanation — it checks if the byte string at address param_1 is equivalent to that at address 0x4e2100, which in hex is
7ffd96d91c8edac03e5bd6f1a4be9ae03a6b6d83219effaa8bfdbcd7a80e221bcf3d4c0adafe84ec6f1dcc32b8ded3c2
Alright, so now we are ready to tackle the main algorithm! For easier reference here is where we left off just now:
return 0x403154(0x402f59(0x402fc4(0x402f59(0x402efa(lVar1)))));
Firstly, 0x402efa:
long FUN_00402efa(long param_1)
{
ulong local_10;
for (local_10 = 0; local_10 < 0x30; local_10 = local_10 + 1) {
FUN_0040203f(param_1 + local_10);
}
return param_1;
}
// ...
byte * FUN_0040203f(byte *param_1)
{
*param_1 = (byte)((int)(uint)*param_1 >> 3) | *param_1 << 5;
return param_1;
}
We can first treat each byte in the input byte string as a string of 8 bits; this function simply rotates right by 3 bits for each byte in the input. For example, ABCDEFGH becomes FGHABCDE.
Next, 0x402f59:
long FUN_00402f59(long param_1)
{
long local_10;
for (local_10 = 0; (ulong)(local_10 << 3) < 0x30; local_10 = local_10 + 1) {
FUN_004021a8(local_10 * 8 + param_1);
}
return param_1;
}
// ...
byte * FUN_004021a8(byte *param_1)
{
byte bVar1;
byte bVar2;
// ...
*param_1 = (byte)((int)(uint)*param_1 >> 1) | *param_1 << 7;
pbVar29 = param_1 + 1;
*pbVar29 = (byte)((int)(uint)*pbVar29 >> 2) | *pbVar29 << 6;
pbVar30 = param_1 + 2;
*pbVar30 = (byte)((int)(uint)*pbVar30 >> 3) | *pbVar30 << 5;
pbVar31 = param_1 + 3;
*pbVar31 = (byte)((int)(uint)*pbVar31 >> 1) | *pbVar31 << 7;
pbVar32 = param_1 + 4;
*pbVar32 = (byte)((int)(uint)*pbVar32 >> 4) | *pbVar32 << 4;
pbVar33 = param_1 + 5;
*pbVar33 = (byte)((int)(uint)*pbVar33 >> 3) | *pbVar33 << 5;
pbVar34 = param_1 + 6;
*pbVar34 = (byte)((int)(uint)*pbVar34 >> 2) | *pbVar34 << 6;
pbVar35 = param_1 + 7;
*pbVar35 = (byte)((int)(uint)*pbVar35 >> 1) | *pbVar35 << 7;
bVar1 = *param_1;
bVar2 = *param_1;
bVar3 = *pbVar29;
bVar4 = *param_1;
bVar5 = *pbVar29;
bVar6 = *pbVar30;
bVar7 = *param_1;
bVar8 = *pbVar29;
bVar9 = *pbVar30;
bVar10 = *pbVar31;
bVar11 = *param_1;
bVar12 = *pbVar29;
bVar13 = *pbVar30;
bVar14 = *pbVar31;
bVar15 = *pbVar32;
bVar16 = *param_1;
bVar17 = *pbVar29;
bVar18 = *pbVar30;
bVar19 = *pbVar31;
bVar20 = *pbVar32;
bVar21 = *pbVar33;
bVar22 = *param_1;
bVar23 = *pbVar29;
bVar24 = *pbVar30;
bVar25 = *pbVar31;
bVar26 = *pbVar32;
bVar27 = *pbVar33;
bVar28 = *pbVar34;
*param_1 = (char)*pbVar35 >> 7 & 1U |
*param_1 & 0x80 | (char)*pbVar29 >> 1 & 0x40U | (char)*pbVar30 >> 2 & 0x20U |
(char)*pbVar31 >> 3 & 0x10U | (char)*pbVar32 >> 4 & 8U | (char)*pbVar33 >> 5 & 4U |
(char)*pbVar34 >> 6 & 2U;
*pbVar29 = (char)*pbVar35 >> 6 & 1U |
bVar1 * '\x02' & 0x80 | *pbVar29 & 0x40 | (char)*pbVar30 >> 1 & 0x20U |
(char)*pbVar31 >> 2 & 0x10U | (char)*pbVar32 >> 3 & 8U | (char)*pbVar33 >> 4 & 4U |
(char)*pbVar34 >> 5 & 2U;
*pbVar30 = (char)*pbVar35 >> 5 & 1U |
(byte)(((int)(char)bVar2 & 0x3fffffe0U) << 2) | bVar3 * '\x02' & 0x40 | *pbVar30 & 0x20
| (char)*pbVar31 >> 1 & 0x10U | (char)*pbVar32 >> 2 & 8U | (char)*pbVar33 >> 3 & 4U |
(char)*pbVar34 >> 4 & 2U;
*pbVar31 = (char)*pbVar35 >> 4 & 1U |
(byte)(((int)(char)bVar4 & 0x1ffffff0U) << 3) | (byte)(((int)(char)bVar5 & 0x10U) << 2)
| bVar6 * '\x02' & 0x20 | *pbVar31 & 0x10 | (char)*pbVar32 >> 1 & 8U |
(char)*pbVar33 >> 2 & 4U | (char)*pbVar34 >> 3 & 2U;
*pbVar32 = (char)*pbVar35 >> 3 & 1U |
(byte)(((int)(char)bVar7 & 0xffffff8U) << 4) | (byte)(((int)(char)bVar8 & 8U) << 3) |
(byte)(((int)(char)bVar9 & 8U) << 2) | bVar10 * '\x02' & 0x10 | *pbVar32 & 8 |
(char)*pbVar33 >> 1 & 4U | (char)*pbVar34 >> 2 & 2U;
*pbVar33 = (char)*pbVar35 >> 2 & 1U |
(byte)(((int)(char)bVar11 & 0x7fffffcU) << 5) | (byte)(((int)(char)bVar12 & 4U) << 4) |
(byte)(((int)(char)bVar13 & 4U) << 3) | (byte)(((int)(char)bVar14 & 4U) << 2) |
bVar15 * '\x02' & 8 | *pbVar33 & 4 | (char)*pbVar34 >> 1 & 2U;
*pbVar34 = (char)*pbVar35 >> 1 & 1U |
(byte)(((int)(char)bVar16 & 0x3fffffeU) << 6) | (byte)(((int)(char)bVar17 & 2U) << 5) |
(byte)(((int)(char)bVar18 & 2U) << 4) | (byte)(((int)(char)bVar19 & 2U) << 3) |
(byte)(((int)(char)bVar20 & 2U) << 2) | bVar21 * '\x02' & 4 | *pbVar34 & 2;
*pbVar35 = *pbVar35 & 1 |
(byte)((int)(char)bVar22 << 7) | (byte)(((int)(char)bVar23 & 1U) << 6) |
(byte)(((int)(char)bVar24 & 1U) << 5) | (byte)(((int)(char)bVar25 & 1U) << 4) |
(byte)(((int)(char)bVar26 & 1U) << 3) | (byte)(((int)(char)bVar27 & 1U) << 2) |
bVar28 * '\x02' & 2;
return param_1;
}
Okay, this function looks crazy!
Let’s stay calm, and break this down step by step:
for (local_10 = 0; (ulong)(local_10 << 3) < 0x30; local_10 = local_10 + 1) {
FUN_004021a8(local_10 * 8 + param_1);
}
We see that the crazy function takes in a sequence of 8 bytes as input.
*param_1 = (byte)((int)(uint)*param_1 >> 1) | *param_1 << 7;
This does not look too bad, in fact it is similar to the previous function we explored! This part simply rotates the bits of each byte by a certain amount (1, 2, 3, 1, 4, 3, 2, 1).
*param_1 = (char)*pbVar35 >> 7 & 1U |
*param_1 & 0x80 | (char)*pbVar29 >> 1 & 0x40U | (char)*pbVar30 >> 2 & 0x20U |
(char)*pbVar31 >> 3 & 0x10U | (char)*pbVar32 >> 4 & 8U | (char)*pbVar33 >> 5 & 4U |
(char)*pbVar34 >> 6 & 2U;
pbVar29 to pbVar35 are used to reference each index of the sequence of 8 bytes. So what this does is it
- takes the first (most significant) bit of the first byte
- and places the first bit of the second byte in its second position
- and so on …
until it forms a new byte with the i-th bit being the first bit of the i-th byte. And it does the same for the other 7 new bytes.
That sounds really complicated because I don’t know how to explain this part. But a much simpler concept to visualise is matrix transposition! If you imagine this sequence of 8 bytes as a matrix of 8 x 8 bits, this section spits out a transpose of that.
0x402fc4:
// ...
local_18 = &stack0x00000008;
local_48 = FUN_0040215c;
local_40 = FUN_00401f6f;
local_28 = &local_48;
local_38 = 0xfa1e0ff3;
local_34 = 0xbb49;
local_32 = FUN_00401dd5;
local_2a = 0xba49;
local_20 = 0x90e3ff49;
local_80 = &local_38;
local_58 = &stack0x00000008;
local_88 = FUN_0040210f;
local_68 = &local_88;
local_78 = 0xfa1e0ff3;
local_74 = 0xbb49;
local_72 = FUN_00401dd5;
local_6a = 0xba49;
local_60 = 0x90e3ff49;
for (local_b0 = 0; local_b0 < 0x30; local_b0 = local_b0 + 1) {
(*(code *)&local_78)(param_1 + local_b0);
}
// ...
which becomes
for (local_b0 = 0; local_b0 < 0x30; local_b0 = local_b0 + 1) {
0x40210f(0x40215c(0x401f6f(param_1 + local_b0)));
}
0x401f6f:
byte * FUN_00401f6f(byte *param_1)
{
*param_1 = (byte)((int)(uint)*param_1 >> 1) | *param_1 << 7;
return param_1;
}
rotating by 1 bit;
0x40215c:
byte * FUN_0040215c(byte *param_1)
{
*param_1 = (char)*param_1 >> 4 & 0xfU | (byte)((int)(char)*param_1 << 4);
return param_1;
}
shifting by 4 bits;
0x40210f:
byte * FUN_0040210f(byte *param_1)
{
*param_1 = (char)*param_1 >> 1 & 0x55U | *param_1 * '\x02' & 0xaa;
return param_1;
}
This one is kind of funny but still easily derivable; essentially ABCDEFGH becomes BADCFEHG.
0x402f59 (repeated; see the crazy function above)
0x403154:
long FUN_00403154(long param_1)
{
ulong local_10;
for (local_10 = 0; local_10 < 0x30; local_10 = local_10 + 1) {
FUN_00401f2c(param_1 + local_10);
}
return param_1;
}
// ...
byte * FUN_00401f2c(byte *param_1)
{
*param_1 = *param_1 ^ 0xff;
return param_1;
}
A simple xor. What a nice way to end this. In the matrix representation this is as simple as taking subtracting each bit from 1.
Part II
In summary, here is a python consolidation of the entire algorithm above:
import numpy as np
inp = 'AAAAAAAABBBBBBBBCCCCCCCCDDDDDDDDEEEEEEEEFFFFFFFF'
inp = np.array([list(map(int, list(bin(ord(x))[2:].zfill(8)))) for x in list(inp)])
abcdef_out = 'fb 79 ed f7 f8 ef e3 fa cb 7d f7 eb d8 ff ff d2 cb 79 e5 e3 d8 ef e3 d2 fb 7d df bf e0 df bf aa fb 79 cd b7 e0 cf a3 aa cb 7d d7 ab c0 df bf 82'
abcdef_out = np.array([list(map(int, list(bin(int(x, 16))[2:].zfill(8)))) for x in abcdef_out.split(' ')])
solve = '7F FD 96 D9 1C 8E DA C0 3E 5B D6 F1 A4 BE 9A E0 3A 6B 6D 83 21 9E FF AA 8B FD BC D7 A8 0E 22 1B CF 3D 4C 0A DA FE 84 EC 6F 1D CC 32 B8 DE D3 C2'
solve = np.array([list(map(int, list(bin(int(x, 16))[2:].zfill(8)))) for x in solve.split(' ')])
# rotates rightwards
def rot(x, n):
return np.concatenate([x[-n:], x[:-n]])
def crazy(p):
return np.array([rot(p[i, :], n) for i, n in enumerate([1, 2, 3, 1, 4, 3, 2, 1])]).T
def mix(x):
return np.array([x[1], x[0], x[3], x[2], x[5], x[4], x[7], x[6]])
def rotate_inp(m):
return np.array([rot(x, 3) for x in m])
def crazy_inp(m):
for i in range(0, 0x30, 8):
m[i:i+8, :] = crazy(m[i:i+8, :])
return m
def more_mix_inp(m):
return np.array([mix(rot(x, 5)) for x in m])
def xor_inp(m):
return 1-m
print(xor_inp(crazy_inp(more_mix_inp(crazy_inp(rotate_inp(inp))))) - abcdef_out)
(abcdef_out was used as a sanity check.)
And that’s it! Luckily all the above functions can easily be reversible, hence we shall do exactly that.
Again, this part was done by my teammate :)
# ...
def f(x):
return np.array([sum([x<<i for i,x in enumerate(p[::-1])]) for p in x])
def rev_rot(x, n):
return rot(x, -n)
def rev_crazy(p):
p = p.T
return np.array([rev_rot(p[i, :], n) for i, n in enumerate([1, 2, 3, 1, 4, 3, 2, 1])])
def rev_mix(x):
return mix(x)
def rev_rotate_inp(m):
return np.array([rev_rot(x, 3) for x in m])
def rev_crazy_inp(m):
for i in range(0x28, -1, -8):
m[i:i+8, :] = rev_crazy(m[i:i+8, :])
return m
def rev_more_mix_inp(m):
return np.array([rev_rot(rev_mix(x), 5) for x in m])
def xor_inp(m):
return np.array([1-x for x in m])
out = solve
out = xor_inp(out)
out = rev_crazy_inp(out)
out = rev_more_mix_inp(out)
out = rev_crazy_inp(out)
out = rev_rotate_inp(out)
print("".join([chr(x) for x in f(out)]))
And there we have it: STF22{L0T5_0F_H0P1UM_4ND_C0P1UM_4ND_SH33R_LUCK}!
Emulator
Category: rev
Disclaimer: Do pardon my lack of knowledge in assembly / rev in general XD
Looks like we are given a .jar file. Running it normally shows us a sequence of 3 screens:

The program starts off with a matrix of random colours which gradually get wiped out.

The splash screen shows a black background with a white logo and some text.

The main screen seems to allow the user to provide some input and then spit out some output. Mashing keys normally does not seem to work. Looking at the provided image we see that the “user” is able to provide some text which outputs a “hash”:

Part I
Onto the decompilation. 5 files were successfully retrieved, all of which use confusing names to make reverse engineering difficult.
Hence to reduce some of the pain, I will first rename all classes from their respective single letters to its corresponding upper case (e.g. class named a is changed to A).
We start off with the smallest file, D.java:
public final class D
{
public static int[] a;
static {
D.a = new int[] { 0, 255, 65280, 65535, 16711680, 16711935, 16776960, 16777215 };
}
}
This class only contains a static integer array. Through a simple search we discover that the array is only used in the main class, hence we can simply migrate the array there (though not necessary).
Next, we hace C.java:
public final class C
{
public short a;
public short[] b;
public boolean c;
public A d;
public C() {
this.a();
}
public final void a() {
this.c = true;
this.a = 2048;
this.b = new short[8];
this.d = new A("
[omitted]
");
this.c = false;
}
}
The omitted portion is a pretty long hex string. We can try to figure out what it is used for by following the constructor to A.java:
import java.util.Random;
public class A
{
private short[] a;
private short[] b;
private short[] c;
private int[] d;
private int e;
public A() {
this("c0f100c07038f00008c49001c000016414007b9ffc");
}
public A(final String s) {
this.a = new short[1024];
this.b = new short[512];
this.c = new short[1];
this.d = new int[2048];
this.a();
this.a(s);
}
public void a() {
final Random random = new Random();
for (int i = 0; i < this.a.length; ++i) {
this.a[i] = (short)random.nextInt();
}
for (int j = 0; j < this.b.length; ++j) {
this.b[j] = (short)random.nextInt();
}
for (int k = 0; k < this.d.length; ++k) {
this.d[k] = random.nextInt();
}
}
public void a(final String s) {
this.e = Math.floorDiv(s.length(), 6);
for (int i = 0; i < this.e; ++i) {
final int beginIndex = i * 6;
this.d[i] = Integer.parseInt(s.substring(beginIndex, beginIndex + 6), 16);
}
}
// ...
}
As a note, a lot of the other functions in this class are also named a; we can check which function is called in the constructor by looking at the type of the argument passsed.
The first method named a seems to initialise arrays a, b and d with random values. The second one though is what seems to parse the hex string. Looking at the function, A (class) expects the string s to be a series of 6 characters, each of which is then directly turned into an integer and stored in d. This means that the integers in d will have a maximum of 12 bytes.
But so far, the program is only being fed “useless” information; where are these values actually used in the program? For that, we shall return to the main class, starting from, of course, the entry point:
// ...
public class EmuMain extends Canvas implements Runnable
{
// ...
public static void main(final String[] array) {
final EmuMain comp;
(comp = new EmuMain()).setMinimumSize(new Dimension(960, 720));
comp.setMaximumSize(new Dimension(960, 720));
comp.setPreferredSize(new Dimension(960, 720));
(EmuMain.h = new JFrame("Emulator")).setDefaultCloseOperation(3);
EmuMain.h.setLayout(new BorderLayout());
EmuMain.h.add(comp, "Center");
new JPanel().setLayout(new GridLayout(1, 1));
EmuMain.h.pack();
EmuMain.h.setResizable(true);
EmuMain.h.setLocationRelativeTo(null);
EmuMain.h.setVisible(true);
comp.setFocusable(true);
final EmuMain target;
(target = comp).a = true;
new Thread(target).run();
}
}
This portion does 2 things:
- Initialise fields;
- Run the bulk of the program.
Looking at the first point:
// ...
public class EmuMain extends Canvas implements Runnable
{
private boolean a;
private int b;
private BufferedImage c;
private int[] d;
private static double e;
private static double f;
private C g;
private static JFrame h;
static {
new Random();
EmuMain.e = 400.0;
EmuMain.f = 1.0E9 / EmuMain.e;
}
public EmuMain() {
this.a = false;
this.b = 0;
this.c = new BufferedImage(160, 120, 1);
this.d = ((DataBufferInt)this.c.getRaster().getDataBuffer()).getData();
this.g = new C();
new B(this, this.g);
}
// ...
}
Right now these don’t make much sense yet, but the notable variables for now are d and g. We can pretty easily tell that d controls the individual “pixels” of whatever will be drawn in the program, while g (if you recall what the class C did) seemed to load some “code” into the “system”.
This is later further confirmed by the repeated access of g.d which, perhaps confusingly, references an A object, where all the interesting arrays are.
Finally, we see EmuMain create a B object (keycodes annotated):
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
public final class B implements KeyListener
{
private boolean[] a;
private C b;
public B(final EmuMain emuMain, final C b) {
this.a = new boolean[128];
emuMain.addKeyListener(this);
this.b = b;
}
private void a(int n, final boolean b) {
if (n < this.a.length) { this.a[n] = b; }
if (this.a[82]) { this.b.a(); } // R
n = 0;
if (this.a[38]) { n = 1; } // up
if (this.a[40]) { n = (short)(n | 0x2); } // down
if (this.a[37]) { n = (short)(n | 0x4); } // left
if (this.a[39]) { n = (short)(n | 0x8); } // right
if (this.a[49]) { n = (short)(n | 0x10); } // 1
if (this.a[50]) { n = (short)(n | 0x20); } // 2
if (this.a[51]) { n = (short)(n | 0x40); } // 3
if (this.a[52]) { n = (short)(n | 0x80); } // 4
this.b.d.a((short)n);
}
@Override
public final void keyPressed(final KeyEvent keyEvent) {
this.a(keyEvent.getKeyCode(), true);
}
@Override
public final void keyReleased(final KeyEvent keyEvent) {
this.a(keyEvent.getKeyCode(), false);
}
@Override
public final void keyTyped(final KeyEvent keyEvent) {
}
}
When B is created, the program’s C object is loaded. We can easily tell that this class mainly handles the inputs, where the keys being pressed down are encoded as bits of a short
b.d.a is then called; Which exact function is called is kind of confusing, but by patiently tracing the objects we arrive at this specific function within A.java:
// ...
public void a(final short n) {
this.c[0] = n;
}
// ...
How?
B.bis aCobjectC.dis anAobjectA.arefers to the above function when a short is passed.
Note: Most of the painful parts were related to this, but if confused a quick renaming always helps a ton. For example even renaming B.b to B.c saves quite a bit of brain power trying to understand where each thing comes from. Of course ultimately to intuitively understand the program we can also rename them to something useful.
Back to the code; We can now figure out that A.c is an array that only contains one (1) short, which is a sequence of bits corresponding to keypresses.
Now we shall attempt to understand the bulk of EmuMain.
// ...
@Override
public void run() {
long nanoTime = System.nanoTime();
double n = 0.0;
int i = 0;
int j = 0;
long currentTimeMillis = System.currentTimeMillis();
while (this.a) {
final long nanoTime2 = System.nanoTime();
n += (nanoTime2 - nanoTime) / EmuMain.f;
nanoTime = nanoTime2;
while (n >= 1.0) {
++j;
final C g;
if (!(g = this.g).c) {
final int b;
final int n2 = (b = g.d.b(g.a)) >> 18 & 0x3F;
final int n3 = b >> 15 & 0x7;
final int n4 = b >> 12 & 0x7;
final int n5 = b >> 9 & 0x7;
final int n6 = (b & 0x7FF) - (b & 0x800);
final C c = g;
++c.a;
switch (n2) {
case 0: { g.c = true; break; }
case 24: { g.b[n3] = g.d.a(g.b[n4] + n6); break; }
// ...
}
}
++this.b;
--n;
}
try {
Thread.sleep(8L);
}
catch (InterruptedException ex) {
ex.printStackTrace();
}
++i;
final BufferStrategy bufferStrategy;
if ((bufferStrategy = this.getBufferStrategy()) == null) {
this.createBufferStrategy(3);
}
else {
for (int k = 0; k < 120; ++k) {
for (int l = 0; l < 160; ++l) {
final int n33 = k * 160 + l;
final short c5;
final int n34 = (c5 = this.g.d.c((l >> 3) + (k >> 3) * 20)) & 0xFF;
final int n35 = c5 >> 8 & 0x7;
final int n36 = c5 >> 11 & 0x7;
final int n37 = n34;
final int n38 = k & 0x7;
final int n39 = l & 0x7;
final int n40 = n38;
long n41 = 0L;
switch (n37 & 0xFF) {
default: { n41 = 0L; break; }
case 17: { n41 = -8700919226847423759L; break; }
// ...
}
this.d[n33] = (((n41 >> (n40 << 3) + n39 & 0x1L) == 0x1L) ? D.a[n36] : D.a[n35]);
}
}
final Graphics drawGraphics;
(drawGraphics = bufferStrategy.getDrawGraphics()).drawImage(this.c, 0, 0, this.getWidth(), this.getHeight(), null);
drawGraphics.dispose();
bufferStrategy.show();
}
if (System.currentTimeMillis() - currentTimeMillis > 1000L) {
currentTimeMillis += 1000L;
EmuMain.h.setTitle("Emulator " + j + " tps " + i + " fps\n");
i = 0;
j = 0;
}
}
}
// ...
Looks crazy! But if we first look at the outer shells we quickly notice that the while loop contains 2 main components:
- Fiddling with arrays in
CandA - Handling graphics (i.e. drawing stuff)
Starting with the second bullet as it is relatively easier to handle. At a glance we can tell that k and l represent the “height” and “width” of the image grid, and n33 represents the “index” of each specific cell.
g.d.c(idx) is again a function that simply grabs the value at A.b[idx]. We can see that A.b is sort of the “graphics” section of the “memory”. The >> 3 part tells is that each 8x8 square corresponds to a single value in memory, which means that the image is actually 15 (high) by 20 (wide) cells.
Looking at how the stored value is subsequently split up tells us about how the bits of the short is utilised to contain information about the entire drawn cell. Each short is coded as 00fffbbbcccccccc, where f is the foreground “colour”, b is the background “colour”, and c is the character in ASCII (the daunting switch portion simply spits out the corresponding graphic for each ASCII used). We can also now see that the values in D.a (the static array from the very beginning) actually stores colour values (e.g. 0x0, 0xFF, 0xFF00, etc.).
That is essentially it for this portion — we learnt that A.b stores the characters to be printed on the image. Now onto the first bullet:
// ...
final C g;
if (!(g = this.g).c) {
final int b;
final int n2 = (b = g.d.b(g.a)) >> 18 & 0x3F;
final int n3 = b >> 15 & 0x7;
final int n4 = b >> 12 & 0x7;
final int n5 = b >> 9 & 0x7;
final int n6 = (b & 0x7FF) - (b & 0x800);
final C c = g;
++c.a;
switch (n2) {
case 0: { g.c = true; break; }
case 24: { g.b[n3] = g.d.a(g.b[n4] + n6); break; }
// ...
}
}
// ...
We can see a similar pattern in that the value in b contains quite some information encoded as bits. As a side node, looking inside the switch portion immediately gives away the fact that program is reading pseudo-assembly.
Returning to g.d.b (which is in a sense A.b) we see:
// ...
public int b(final int n) {
if (n >= 2048) {
return this.d[n - 2048];
}
return 0;
}
// ...
Judging by how the value is subsequently broken up and interpreted, we can safely assume that A.d represents the “machine code” of the embedded program. Interestingly, n is offset by 2048, but that does not impact the program too much.
For completeness sake, here are the remaining functions in A:
// ...
public short a(final int n) {
if (n == 1535) {
return this.c[0];
}
if (n >= 1024) {
return this.b[n - 1024];
}
return this.a[n];
}
public void a(final int n, final int n2) {
if (n >= 1024) {
this.b[n - 1024] = (short)n2;
return;
}
this.a[n] = (short)n2;
}
// ...
This should complete the picture. The above functions provide a way to get / set the memory, while the arrays of A are as follows: a is the “regular” memory (e.g. stack), b holds the graphics portion, c contains the keypresses and d the instructions. (The registers themselves are found in C).
Part II
Together with the cases inside switch, the previously read long hex string can now be understood and parsed into some sort of assembly with the help of some generic script (e.g. python).
For my solution I did not translate it into something with some sort of assembly syntax, but actually more of a python syntax (but it should not matter). By default the numbers shown below are in hex. Also the capital letters are the registers (8 total).
Here’s the starting point of the program (0x800, or 2048 in decimal):
800 | G = H + 100
801 | B = H + 0
802 | [H + 0] = B
803 | (H) jz 899; F = 804; clear H
I did not know at that time how to explain something like 0x803, but essentially the program does 3 things, in this order:
- Set some register to some value (
F = 0x804) - Set register
Hto0 - Jump if a certain register (in brackets) is
0(or sometimes if not0).
It is important to note that register H is almost always set to 0, as sort of a “reference point” to reset to values of other registers (such that e.g. A = H + 0x400 “sets” A to 0x400).
We can also sort of see that F generally stores the “return address” for functions, which is kind of interesting (some familiarity in the unknown :))
Anyways, looking at the above snippet we notice that G is set to 0x100, and the program jumps to 0x899.
899 | G = G + 1
89a | [G - 1] = F
89b | A = H + 38
89c | A = A << 8
89d | (H) jz 904; F = 89e; clear H
# ...
Here we can more clearly see the role of G, and that is of course the stack pointer. It does not do much however, as it only serves to save values of certain registers. Interesting nonetheless.
Do take note of the value of A, being an interesting 0x3800.
904 | G = G + 1
905 | [G - 1] = F
906 | G = G + 1
907 | [G - 1] = B
908 | B = H + 12c
909 | B = B - 1
90a | [B + 400] = A
90b | (B) jnz 909; H = 90c; clear H
90c | B = [G - 1]
90d | G = G - 1
90e | F = [G - 1]
90f | G = G - 1
910 | jmp F; H = 911; clear H
Ignoring the stack manipulation this function is actually only 4 instructions long. We also see an implementation of a loop, where B starts from 0x12c (which in decimal is 300, or 15 by 20 — seems familiar?).
The memory at B+400 is then populated with A, being 0x3800. If you can recall what this does, you will notice that this exactly corresponds to the portion where the random colours of the first screen (of the program) is replaced with a black splash screen! 0x3800, or 0b00 111 000 00000000, tells the program to draw null characters with a white foreground and black background.
Once completed, we jump back to the saved return address, 0x89e:
89e | B = H + 400
89f | C = A + 11
8a0 | [B + 6d] = C
8a1 | C = A + 12
8a2 | [B + 6e] = C
# ...
900 | F = [G - 1]
901 | G = G - 1
902 | jmp F; H = 903; clear H
903 | end
I will not bore you with the details, but essentially the program here prints out the logo / text on the splash screen, followed by the entirety of the input screen, done in a fashion similar to 0x89f and 0x8a0.
We then return to our initial saved address:
804 | (H) jz 806; F = 805; clear H
805 | end
806 | (H) jz 809; F = 807; clear H
807 | (H) jz 806; H = 808; clear H
808 | end
After some confusing manoeuvres the program will end up at 0x809:
809 | G = G + 1
80a | [G - 1] = F
80b | G = G + 1
80c | [G - 1] = A
80d | G = G + 1
80e | [G - 1] = B
80f | G = G + 1
810 | [G - 1] = C
811 | A = [H + 5ff] # keypresses
812 | B = A & 1 # up
813 | (B) jz 81f; H = 814; clear H
We can see that the program starts to look at the keypresses. The following is run if the program detects “up” being pressed:
814 | B = [H + 0]
815 | B = [B + 450] # user input location
816 | C = B - 20
817 | C = C + 1
818 | C = C & 3f
819 | C = C + 20
81a | B = B >>> 8
81b | B = B << 8
81c | B = B | C
81d | C = [H + 0]
81e | [C + 450] = B
Honestly this part was quite confusing to me, but essentially it does what it does in the actual program, which is to cycle through the 64 valid characters. We can also see that the 0-th index of the memory stores the “caret” position.
To not bore you of the details, the program then runs similar checks for down, left and right, which have uninteresting functionalities (and can be found out pretty easily by running the actual program).
Here comes the interesting part though:
847 | B = A & 10 # 1
848 | (B) jz 84a; H = 849; clear H
849 | (H) jz 853; F = 84a; clear H
84a | C = [G - 1]
84b | G = G - 1
84c | B = [G - 1]
84d | G = G - 1
84e | A = [G - 1]
84f | G = G - 1
850 | F = [G - 1]
851 | G = G - 1
852 | jmp F; H = 853; clear H
Firstly, the program detects if 1 is pressed; if not, the program finishes its checks and exit the function call.
However, if 1 is pressed, the program jumps to an extremely important section at 0x853: The “hash”.
Part III
Note: This part was solved by my teammate
As the algorithm instructions are written in a way similar to python, we can very easily translate it into a much more readable python form.
# A-Z[\]^_ !"#$%&'()*+,-./0123456789:;<=>?@
def short(key):
return key % (2**15) - (2**15)*((key >> 15) % 2)
def func(key):
temp = key
temp = short(temp ^ (temp << 7))
temp = short(temp ^ (temp >> 9))
temp = short(temp ^ (temp << 8))
return temp
inp = [0 for _ in range(20)] # input
out = [0 for _ in range(20)]
key = 0x2a
for x in inp[::-1]:
key = (key^x) << 1
for _ in range(0x100):
key = func(key)
for j, x in enumerate(inp[::-1]):
i = len(inp)-j-1
key = func(key) ^ x
out[i] = key%64
print(bytes([i+32 for i in out]).decode())
The trick for this actually lies in the size of the key. Because a short has a maximum size of 65535, a brute-force attack is actually rather feasible. As such, we can easily try all possible keys (right before the last for loop in the above script) to derive the input.
out = [63, 45, 31, 27, 38, 23, 51, 20, 0, 14, 15, 54, 34, 52, 41, 38, 26, 34, 7, 0]
for K in range(65536):
key = K
for j, x in enumerate(out[::-1]):
i = len(inp)-j-1
key = func(key)
inp[i] = (x^key)%64
key ^= inp[i]
X = bytes([i+32 for i in inp])
if (b"STF" in X):
print(X)
And there we have it! STF22[CU5T0M1C5BCPU]
Thanks for sticking with me till the end :) I’m still relatively inexperienced at writeups so hopefully this was an okay read. Accidentally made this quite long as I wanted to make my half-day solving process as detailed as possible (within limits), regardless of whether this is the intended solve.