Event: RIFFHACK - Black Market Break-In ·
Category: Reverse Engineering ·
Difficulty: Easy
Description
The docking console hides its handshake phrase in a runtime-built buffer behind one more computed alignment value. Use the lightest reversing path and recover both values.
Binary
orbital_docking_handshake (Mach-O arm64 — renamed to .txt for hosting)
Solution
This problem is easier to do on an M-series Mac.
Let’s do the basic stuff first on Kali:
1
2
3
4
5
6
7
| ┌──(s1nister㉿kali)-[~]
└─$ file orbital_docking_handshake
orbital_docking_handshake: Mach-O 64-bit arm64 executable, flags:<NOUNDEFS|DYLDLINK|TWOLEVEL|PIE>
┌──(s1nister㉿kali)-[~]
└─$ checksec orbital_docking_handshake
orbital_docking_handshake: Magic number does not match
|
After seeing this I switched over to my Mac to do the rest.
Let’s get our executable to execute first!
1
2
3
4
5
| sudo chmod +x orbital_docking_handshake
Password:
./orbital_docking_handshake
zsh: killed ./orbital_docking_handshake
|

Oops! Classic Mac. Let’s fix this real quick.
Go to Settings > Privacy & Security > Security section and press Allow Anyway:

Relaunch the program and press Open Anyway again:

1
2
3
4
5
6
7
| ❯ ./orbital_docking_handshake
Orbital Docking Handshake
Trace the handshake routine, recover the correct phrase, and align the approach window.
Hint for analysts: the shortest path is still the cleanest one.
Docking phrase: 12345
Alignment window: 696969
Handshake rejected. Docking denied.
|
Alright, that’s enough fiddling with the binary. Let’s switch over to Ghidra.
The problem description says “Use the lightest reversing path and recover both values”. So I’ll use a very dumb solution here. Spoiler alert: we’re patching the binary lmao.
Here’s what Ghidra’s decompiler shows:
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
| undefined4 entry(void)
{
uint uVar1;
int iVar2;
int iVar3;
int iVar4;
char *pcVar5;
size_t sVar6;
ulong uVar7;
undefined4 local_8c;
char acStack_85 [32];
undefined1 auStack_65 [13];
char acStack_58 [64];
long local_18;
local_18 = *(long *)PTR____stack_chk_guard_100004000;
_puts("Orbital Docking Handshake");
_puts("Trace the handshake routine, recover the correct phrase, and align the approach window.");
_puts("Hint for analysts: the shortest path is still the cleanest one.");
_printf("Docking phrase: ");
_fflush(*(FILE **)PTR____stdoutp_100004018);
pcVar5 = _fgets(acStack_58,0x40,*(FILE **)PTR____stdinp_100004028);
if (pcVar5 == (char *)0x0) {
uVar1 = _puts("No phrase supplied.");
uVar7 = (ulong)uVar1;
local_8c = 1;
}
else {
sVar6 = _strcspn(acStack_58,"\n");
acStack_58[sVar6] = '\0';
_printf("Alignment window: ");
_fflush(*(FILE **)PTR____stdoutp_100004018);
pcVar5 = _fgets(acStack_85,0x20,*(FILE **)PTR____stdinp_100004028);
if (pcVar5 == (char *)0x0) {
uVar1 = _puts("No alignment window supplied.");
uVar7 = (ulong)uVar1;
local_8c = 1;
}
else {
_build_expected_phrase();
iVar2 = _compute_grid_offset(auStack_65);
iVar3 = _atoi(acStack_85);
iVar4 = _compare_identity(acStack_58,auStack_65);
if (iVar4 == 0) {
uVar1 = _puts("Handshake rejected. Docking denied.");
uVar7 = (ulong)uVar1;
local_8c = 1;
}
else if (iVar3 == iVar2) {
uVar7 = _print_flag(0,auStack_65,iVar2);
local_8c = 0;
}
else {
uVar1 = _puts("Alignment window rejected.");
uVar7 = (ulong)uVar1;
local_8c = 1;
}
}
}
if (*(long *)PTR____stack_chk_guard_100004000 - local_18 != 0) {
/* WARNING: Subroutine does not return */
___stack_chk_fail(*(long *)PTR____stack_chk_guard_100004000 - local_18,uVar7);
}
return local_8c;
}
|
Now we could spend some time looking into what _compute_grid_offset or _compare_identity does. For example here’s what they look like:
1
2
3
4
5
6
7
8
9
10
11
12
| int _compute_grid_offset(long param_1)
{
undefined8 local_18;
undefined4 local_c;
local_c = 0;
for (local_18 = 0; *(char *)(param_1 + local_18) != '\0'; local_18 = local_18 + 1) {
local_c = local_c + (uint)*(byte *)(param_1 + local_18) * ((int)local_18 + 3);
}
return local_c % 1000 + 200;
}
|
1
2
3
4
5
6
7
8
| bool _compare_identity(char *param_1,char *param_2)
{
int iVar1;
iVar1 = _strcmp(param_1,param_2);
return iVar1 == 0;
}
|
But do we really need to? Let’s do this the dumb way.
Look at this bit of code:
1
2
3
4
| else if (iVar3 == iVar2) {
uVar7 = _print_flag(0,auStack_65,iVar2);
local_8c = 0;
}
|
That’s it! If we can somehow make the program reach here, it might just print out the flag. So let’s try to make the program reach here. Forcefully.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| else {
_build_expected_phrase();
iVar2 = _compute_grid_offset(auStack_65);
iVar3 = _atoi(acStack_85);
iVar4 = _compare_identity(acStack_58,auStack_65);
if (iVar4 == 0) {
uVar1 = _puts("Handshake rejected. Docking denied.");
uVar7 = (ulong)uVar1;
local_8c = 1;
}
else if (iVar3 == iVar2) {
uVar7 = _print_flag(0,auStack_65,iVar2);
local_8c = 0;
}
else {
uVar1 = _puts("Alignment window rejected.");
uVar7 = (ulong)uVar1;
local_8c = 1;
}
}
|
The outer if statement just checks whether we have some inputs or not. So control will naturally fall into this else block once we type something in.
So let’s invert the condition for if (iVar4 == 0). If we run our program and type something, it fails with Handshake rejected. Docking denied., so it’s in our best interest to take control flow to the else if where the actual print_flag is called, instead of just sitting in the if block.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| LAB_1000005bc XREF[1]: 10000059c(j)
1000005bc e0 2f 01 91 add x0,sp,#0x4b
1000005c0 e0 07 00 f9 str x0,[sp, #local_a8]
1000005c4 30 00 00 94 bl _build_expected_phrase undefined _build_expected_phrase()
1000005c8 e0 07 40 f9 ldr x0,[sp, #local_a8]
1000005cc 4e 00 00 94 bl _compute_grid_offset undefined _compute_grid_offset()
1000005d0 e0 1f 00 b9 str w0,[sp, #local_94]
1000005d4 e0 af 00 91 add x0,sp,#0x2b
1000005d8 ce 00 00 94 bl _atoi int _atoi(char * param_1)
1000005dc e1 07 40 f9 ldr x1,[sp, #local_a8]
1000005e0 e0 23 00 b9 str w0,[sp, #local_90]
1000005e4 a0 23 01 d1 sub x0,x29,#0x48
1000005e8 67 00 00 94 bl _compare_identity undefined _compare_identity()
1000005ec 00 01 00 35 cbnz w0,LAB_10000060c
1000005f0 01 00 00 14 b LAB_1000005f4
|
1000005ec 00 01 00 35 cbnz w0,LAB_10000060c is our point of interest. This is where the if statement compiles down to an actual conditional jump.
This is ARM assembly btw, not x86_64. And this is what cbnz / cbz mean.
CBZ and CBNZ — Compare and Branch on Zero / Compare and Branch on Non-Zero.
Syntax
1
2
| CBZ Rn, label
CBNZ Rn, label
|
where Rn is the register holding the operand and label is the branch destination.
So right-click on that instruction and click on Patch Instruction:

And change the cbnz to a cbz:

Now check where the print_flag function went:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| else {
_build_expected_phrase();
iVar2 = _compute_grid_offset(auStack_65);
iVar3 = _atoi(acStack_85);
iVar4 = _compare_identity(acStack_58,auStack_65);
if (iVar4 == 0) {
if (iVar3 == iVar2) {
uVar7 = _print_flag(0,auStack_65,iVar2);
local_8c = 0;
}
else {
uVar1 = _puts("Alignment window rejected.");
uVar7 = (ulong)uVar1;
local_8c = 1;
}
}
else {
uVar1 = _puts("Handshake rejected. Docking denied.");
uVar7 = (ulong)uVar1;
local_8c = 1;
}
}
|
Alright. Now time to focus on the if (iVar3 == iVar2). Let’s just invert that check so that it almost always executes, instead of executing only when both are equal. (a.k.a. we’re reverting the behavior of the program such that when all checks fail, it prints out the flag, instead of printing the flag only when all checks succeed.)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| LAB_10000060c XREF[1]: 1000005ec(j)
10000060c e8 23 40 b9 ldr w8,[sp, #local_90]
100000610 e9 1f 40 b9 ldr w9,[sp, #local_94]
100000614 08 01 09 6b subs w8,w8,w9
100000618 00 01 00 54 b.eq LAB_100000638
10000061c 01 00 00 14 b LAB_100000620
LAB_100000620 XREF[1]: 10000061c(j)
100000620 00 00 00 90 adrp x0,0x100000000
100000624 00 88 29 91 add x0=>s_Alignment_window_rejected._100000a62,x0, = "Alignment window rejected."
100000628 ab 00 00 94 bl _puts int _puts(char * param_1)
10000062c 28 00 80 52 mov w8,#0x1
100000630 e8 27 00 b9 str w8,[sp, #local_8c]
100000634 06 00 00 14 b LAB_10000064c
LAB_100000638 XREF[1]: 100000618(j)
100000638 e1 1f 40 b9 ldr w1,[sp, #local_94]
10000063c e0 2f 01 91 add x0,sp,#0x4b
100000640 5e 00 00 94 bl _print_flag undefined _print_flag()
100000644 ff 27 00 b9 str wzr,[sp, #local_8c]
100000648 01 00 00 14 b LAB_10000064c
|
100000618 00 01 00 54 b.eq LAB_100000638 is our point of interest.
In ARM assembly, b.eq just means branch if equal. So what’s the opposite of that? b.ne — branch if not equal. Patch it. After that, this is what our decompiled code looks like:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| else {
_build_expected_phrase();
iVar2 = _compute_grid_offset(auStack_65);
iVar3 = _atoi(acStack_85);
iVar4 = _compare_identity(acStack_58,auStack_65);
if (iVar4 == 0) {
if (iVar3 - iVar2 == 0) {
uVar1 = _puts("Alignment window rejected.");
uVar7 = (ulong)uVar1;
local_8c = 1;
}
else {
uVar7 = _print_flag(iVar3 - iVar2,auStack_65,iVar2);
local_8c = 0;
}
}
else {
uVar1 = _puts("Handshake rejected. Docking denied.");
uVar7 = (ulong)uVar1;
local_8c = 1;
}
}
|
So now, if the checks fail the program is going to print out the flag.
Go to Ghidra, File > Export Program, Format: Original File, and make sure under Options, Export User Byte Modifications is checked.
You should have a new binary at the location that you specified during export. Let’s run it!
1
2
3
4
5
6
7
| ~
❯ sudo chmod +x orbital_docking_handshake
Password:
~ 3s
❯ ./orbital_docking_handshake
zsh: killed ./orbital_docking_handshake
|
Oops. Well, it’s just Apple being Apple. Let’s fix that.
1
2
3
4
5
6
| ~
❯ xattr -c ./orbital_docking_handshake
~
❯ codesign --force --deep --sign - ./orbital_docking_handshake
./orbital_docking_handshake: replacing existing signature
|
Re-run the executable. Type anything you want and make it fail.
1
2
3
4
5
6
7
8
9
| ~
❯ ./orbital_docking_handshake
Orbital Docking Handshake
Trace the handshake routine, recover the correct phrase, and align the approach window.
Hint for analysts: the shortest path is still the cleanest one.
Docking phrase: 67676767
Alignment window: 69696969
Docking accepted. Flag:
bitctf{{0rb1t4l_d0ck1ng_r0ut1n3}}
|
And you shall succeed.
There are other dumb ways to solve this. Maybe NOP out the other checks. Or make _compute_grid_offset return a fixed number. The main idea is that we need to reach the _print_flag function somehow. Doesn’t matter how you do it.
Thanks for reading!
— s1nisteR