Home

R2Con2020 CTF - Cyberlock

☕️☕️☕️☕️ 22 min read

Introduction

R2con is a yearly conference about radare2, a multi-platform, open-source, reverse engineering framework. As part of the conference they host a CTF. This year consisting of fourteen challenges across three levels: easy, medium, and hard. More details can be found here. This post goes into detail on solving a medium level challenge that I found particularly interesting, Cyberlock. The challenge boldly claims to be:

“The world’s only quantum, military-grade, agile, blockchain backed, hardened, bullet-proof 12-pin digital cryptographic cyber lock”.

As the description suggests, it is a 12-pin digital lock that requires twelve separate keys to unlock the flag. The challenge is an unobfuscated stripped x86 binary that requires some creative problem solving. This post documents the journey of solving this challenge, mistakes and all.

Background

I have been using Radare2 sporadically for the past two years in combintation with other disassemblers / decompilers as part of my reversing toolkit. I particularly enjoy scripting with r2 using r2pipe as it provides a quick and convenient interface. Unfortunately, I rarely get the opportunity to use r2 as my primary reversing tool because of time constraints. Last r2con, in 2019, I wrote a blog post on solving a CTF challenge called “Land of Ecodelia” available here. This was my first time using r2 as my primary tool from end to end. Since then I’ve been trying to use r2 more when I can.

Walk-through

Instead of breaking this down into logical sections, such as static / dynamic analysis, I’ve decided to walk-through my step by step approach to solving the challenge. The rationale behind this is to provide an accurate representation of how I approached the problem. Reversing in my experience is a very iterative process and understanding why a problem was solved in a certain way is really interesting.

Initial Analysis

After downloading the challenge, the first step is to run the binary, however, this didn’t get me very far. It appears the Cyberlock binary only accepts pins once per hour, at three minutes past the hour. Of course the main focus immediately became bypassing this feature to speed up the process of solving the challenge. The full error message can be seen below.

➜ ./Cyberlock 
[i] R2conCTF 2020 - CyberLock 1.3.3.7-alpha

     .--------.
    / .------. \
   / /        \ \
   | |        | |
  _| |________| |_
.' |_|        |_| '.
'._____ ____ _____.'
|     .'____'.     |
'.__.'.'    '.'.__.'
'.__  |!R2Con|  __.'
|   '.'.____.'.'   |
'.____'.____.'____.'
'.________________.'

[i] The r2con Cyberlock is the world's only quantum, military-grade, agile, blockchain backed, hardened, bullet-proof 12-pin digital cryptographic cyber lock.
[i] Hackers will never make it past the first pin!!!!

[!] Oh... we forgot to tell you that this secure lock can only be unlocked once per hour - ha ha ha! Come back at 3 minutes past the hour.
Exiting...

I started by loading the challenge into r2 and searching for any interesting strings using the iz command. I found a few unusual 128 character length strings but nothing obvious. In addition, I didn’t see anything resembling a flag so I needed to dig deeper. The next step is to look at the high level control flow of binary, I started by listing the functions in the binary by using the afl command.

r2 CyberLock 
 -- Default scripting languages are NodeJS and Python.
[0x004011c0]> aaa
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze function calls (aac)
...
[0x004011c0]> afl
0x004011c0    1 42           entry0
0x00401030    1 6            sym.imp.g_free
0x00401040    1 6            sym.imp.printf
0x00401050    1 6            sym.imp.memset
0x00401060    1 6            sym.imp.strncmp
0x00401070    1 6            sym.imp.malloc
0x00401080    1 6            sym.imp.fopen
0x00401090    1 6            sym.imp.g_string_ascii_up
0x004010a0    1 6            sym.imp.EVP_CIPHER_CTX_ctrl
0x004010b0    1 6            sym.imp.EVP_DecryptInit_ex
0x004010c0    1 6            sym.imp.g_string_new
0x004010d0    1 6            sym.imp.strlen
0x004010e0    1 6            sym.imp.__ctype_b_loc
0x004010f0    1 6            sym.imp.sprintf
0x00401100    1 6            sym.imp.EVP_DecryptFinal_ex
0x00401110    1 6            sym.imp.g_compute_checksum_for_string
0x00401120    1 6            sym.imp.strcat
0x00401130    1 6            sym.imp.EVP_CIPHER_CTX_new
0x00401140    1 6            sym.imp.EVP_aes_256_gcm
0x00401150    1 6            sym.imp.strcpy
0x00401160    1 6            sym.imp.EVP_CIPHER_CTX_free
0x00401170    1 6            sym.imp.g_file_get_contents
0x00401180    1 6            sym.imp.localtime
0x00401190    1 6            sym.imp.EVP_DecryptUpdate
0x004011a0    1 6            sym.imp.EVP_EncryptInit_ex
0x004011b0    1 6            sym.imp.time
0x004018b0    9 214          main
0x004012a0    5 114  -> 51   entry.init0
0x00401270    3 33   -> 28   entry.fini0
0x00401200    4 33   -> 31   fcn.00401200
0x004012b0    1 327          fcn.004012b0
0x00401400    1 201          fcn.00401400
0x00401990    1 374          fcn.00401990
0x004014d0   19 986          fcn.004014d0
0x00401000    3 23           fcn.00401000

The challenge contains symbols relating to crypto functionality, however, this isn’t really important at this stage. I decided to start at the main function and work my way down.

[0x004018b0]> s main
[0x004018b0]> pdf
            ; DATA XREF from entry0 @ 0x4011dd214: int main (int argc, char **argv, char **envp);; var int64_t var_34h @ rbp-0x34; var int64_t var_30h @ rbp-0x30; var uint32_t var_2ch @ rbp-0x2c; var int64_t var_28h @ rbp-0x28; var file*var_20h @ rbp-0x20; var char *filename @ rbp-0x18; var int64_t var_10h @ rbp-0x10; var uint32_t var_8h @ rbp-0x8; var int64_t var_4h @ rbp-0x40x004018b0      55             push rbp0x004018b1      4889e5         mov rbp, rsp0x004018b4      4883ec40       sub rsp, 0x400x004018b8      c745fc000000.  mov dword [var_4h], 00x004018bf      e8cc000000     call fcn.004019900x004018c4      e847020000     call fcn.00401b10
│           0x004018c9      8945f8         mov dword [var_8h], eax0x004018cc      837df803       cmp dword [var_8h], 3
│       ┌─< 0x004018d0      0f8594000000   jne 0x40196a
│       │   0x004018d6      48c745f00000.  mov qword [var_10h], 0
│       │   0x004018de      48b8fd264000.  movabs rax, str.tmp_masterkey.in ; 0x4026fd ; "/tmp/masterkey.in"
│       │   0x004018e8      488945e8       mov qword [filename], rax
│       │   0x004018ec      488b7de8       mov rdi, qword [filename]   ; const char *filename
│       │   0x004018f0      48be0f274000.  movabs rsi, 0x40270f        ; "r" ; const char *mode
│       │   0x004018fa      e881f7ffff     call sym.imp.fopen          ; file*fopen(const char *filename, const char *mode)
│       │   0x004018ff      488945e0       mov qword [var_20h], rax
│       │   0x00401903      48837de000     cmp qword [var_20h], 0
│      ┌──< 0x00401908      0f8447000000   je 0x401955
│      ││   0x0040190e      31c0           xor eax, eax
│      ││   0x00401910      89c2           mov edx, eax
│      ││   0x00401912      488b7de8       mov rdi, qword [filename]
│      ││   0x00401916      488d75d8       lea rsi, [var_28h]
│      ││   0x0040191a      488d4df0       lea rcx, [var_10h]
│      ││   0x0040191e      e84df8ffff     call sym.imp.g_file_get_contents
│      ││   0x00401923      8945d4         mov dword [var_2ch], eax
│      ││   0x00401926      837dd400       cmp dword [var_2ch], 0
│     ┌───< 0x0040192a      0f8420000000   je 0x401950
│     │││   0x00401930      48bf11274000.  movabs rdi, str.i__Procesing_input_code... ; 0x402711 ; "[i] Procesing input code...\n" ; const char *format
│     │││   0x0040193a      b000           mov al, 0
│     │││   0x0040193c      e8fff6ffff     call sym.imp.printf         ; int printf(const char *format)
│     │││   0x00401941      488b7dd8       mov rdi, qword [var_28h]    ; int64_t arg1
│     │││   0x00401945      8b75f8         mov esi, dword [var_8h]     ; int64_t arg2
│     │││   0x00401948      8945d0         mov dword [var_30h], eax
│     │││   0x0040194b      e880fbffff     call fcn.004014d0
│     │││   ; CODE XREF from main @ 0x40192a
│    ┌└───> 0x00401950      e910000000     jmp 0x401965
│    │ ││   ; CODE XREF from main @ 0x401908
│    │ └──> 0x00401955      488b3c25f040.  mov rdi, qword [0x4040f0]   ; [0x4040f0:8]=0x402004 str.2E7D2C03A9507AE265ECF5B5356885A53393A2029D241394997265A1A25AEFC6454349E422F05297191EAD13E21D3DB520E5ABEF52055E4964B82FB213F593A1 ; int64_t arg1
│    │  │   0x0040195d      8b75f8         mov esi, dword [var_8h]     ; int64_t arg2
│    │  │   0x00401960      e86bfbffff     call fcn.004014d0
│    │  │   ; CODE XREF from main @ 0x401950
│    └─┌──< 0x00401965      e914000000     jmp 0x40197e
│      ││   ; CODE XREF from main @ 0x4018d0
│      │└─> 0x0040196a      48bf2e274000.  movabs rdi, str.Oh..._we_forgot_to_tell_you_that_this_secure_lock_can_only_be_unlocked_once_per_hour___ha_ha_ha__Come_back_at_3_minutes_past_the_hour.__Exiting... ; 0x40272e ; "[!] Oh... we forgot to tell you that this secure
 lock can only be unlocked once per hour - ha ha ha! Come back at 3 minutes past the hour.\nExiting...\n" ; const char *format
│      │    0x00401974      b000           mov al, 0
│      │    0x00401976      e8c5f6ffff     call sym.imp.printf         ; int printf(const char *format)
│      │    0x0040197b      8945cc         mov dword [var_34h], eax
│      │    ; CODE XREF from main @ 0x401965
│      └──> 0x0040197e      31c0           xor eax, eax0x00401980      4883c440       add rsp, 0x400x00401984      5d             pop rbp0x00401985      c3             ret

At a glance it looks like it is running a function, verifying the return value is 3 at offset 0x004018cc. If not it throws an error message saying the lock only accepts input at three minutes past the hour.

If it is three minutes past the hour, it opens and reads from the file /tmp/masterkey.in at offset 0x004018fa using fopen. The result of this operation is then stored in the rax register and null checked at offset 0x00401908, docs for fopen can be found here.

If the contents are null, a static value is sent to fcn.004014d0 at offset 0x00401960, along with the value stored in dword [var_8h] as the second parameter. Otherwise, the contents of the file are then extracted at offset 0x0040191e and null checked at offset 0x0040192a. The contents of the file are then passed as a parameter to fcn.004014d0 at offset 0x0040194b, the same dword [var_8h] value is used as the second parameter.

Bypassing the Rate Limit Control

With this information I began trying to progress through the challenge. I re-opened the binary in write mode by using the -w flag with r2 and attempted to patch the jne to je at offset 0x004018d0.

[0x004011c0]> s main
[0x004018b0]> pdf
...
│           0x004018b0      55             push rbp0x004018b1      4889e5         mov rbp, rsp0x004018b4      4883ec40       sub rsp, 0x400x004018b8      c745fc000000.  mov dword [var_4h], 00x004018bf      e8cc000000     call fcn.004019900x004018c4      e847020000     call fcn.00401b10
│           0x004018c9      8945f8         mov dword [var_8h], eax0x004018cc      837df803       cmp dword [var_8h], 3
│       ┌─< 0x004018d0      0f8594000000   jne 0x40196a
│       │   0x004018d6      48c745f00000.  mov qword [var_10h], 0
...
[0x004018b0]> s 0x004018d0
[0x004018b0]> wa je 0x40196a

Unforunately, this caused a segmentation fault. Clearly the application is using this value in some way, shape, or form. I went back to figuring out what was actually going on in more detail.

➜ ./Cyberlock
[i] R2conCTF 2020 - CyberLock 1.3.3.7-alpha

     .--------.
    / .------. \
   / /        \ \
   | |        | |
  _| |________| |_
.' |_|        |_| '.
'._____ ____ _____.'
|     .'____'.     |
'.__.'.'    '.'.__.' 
'.__  |!R2Con|  __.'
|   '.'.____.'.'   |
'.____'.____.'____.'
'.________________.'

[i] The r2con Cyberlock is the world's only quantum, military-grade, agile, blockchain backed, hardened, bullet-proof 12-pin digital cryptographic cyber lock.
[i] Hackers will never make it past the first pin!!!!

[1]    1371 segmentation fault (core dumped)  ./Cyberlock

Before the conditional jump the eax register, containing the result of the fcn.00401b10 function, is compared against the value 3.

...
│           0x004018bf      e8cc000000     call fcn.004019900x004018c4      e847020000     call fcn.00401b10
│           0x004018c9      8945f8         mov dword [var_8h], eax0x004018cc      837df803       cmp dword [var_8h], 3
...

The fcn.00401b10 function is getting the current system time and parsing the minute value from the return struct found here.

[0x004018b0]> pdf @ fcn.00401b10
            ; CALL XREF from main @ 0x4018c453: fcn.00401b10 ();; var time_t var_20h @ rbp-0x20; var int64_t var_14h @ rbp-0x14; var tm*var_10h @ rbp-0x10; var time_t *timer @ rbp-0x80x00401b10      55             push rbp0x00401b11      4889e5         mov rbp, rsp0x00401b14      4883ec20       sub rsp, 0x200x00401b18      488d7df8       lea rdi, [timer]            ; time_t *timer0x00401b1c      e88ff6ffff     call sym.imp.time           ; time_t time(time_t *timer)0x00401b21      488d7df8       lea rdi, [timer]            ; const time_t *timer0x00401b25      488945e0       mov qword [var_20h], rax0x00401b29      e852f6ffff     call sym.imp.localtime      ; tm*localtime(const time_t *timer)0x00401b2e      488945f0       mov qword [var_10h], rax0x00401b32      488b45f0       mov rax, qword [var_10h]0x00401b36      8b4804         mov ecx, dword [rax + 4]0x00401b39      894dec         mov dword [var_14h], ecx0x00401b3c      8b45ec         mov eax, dword [var_14h]0x00401b3f      4883c420       add rsp, 0x200x00401b43      5d             pop rbp0x00401b44      c3             ret

With that in mind, it looks like this function call can be patched out completely statically. I replaced the function with an instruction to populate the eax (accumulator) register with the expected value. I seek to the offset of the function at 0x004018c, verify my assembly using the pa print assembled command, and then write the instruction using the wa write assembled command.

[0x004018b0]> s 0x004018c
[0x004018c]> pa mov eax, 3
b903000000
[0x004018c]> wa mov eax, 3
[0x004018c]> pdf
...
│           0x004018bf      e8cc000000     call fcn.004019900x004018c4      b803000000     mov eax, 30x004018c9      8945f8         mov dword [var_8h], eax
...

This time the patch was successful in bypassing the rate limit restriction. Running the binary again reveals that I can now submit pin entries at any time.

➜ ./Cyberlock
[i] R2conCTF 2020 - CyberLock 1.3.3.7-alpha

     .--------.
    / .------. \
   / /        \ \
   | |        | |
  _| |________| |_
.' |_|        |_| '.
'._____ ____ _____.'
|     .'____'.     |
'.__.'.'    '.'.__.'
'.__  |!R2Con|  __.'
|   '.'.____.'.'   | 
'.____'.____.'____.'
'.________________.'

[i] The r2con Cyberlock is the world's only quantum, military-grade, agile, blockchain backed, hardened, bullet-proof 12-pin digital cryptographic cyber lock.
[i] Hackers will never make it past the first pin!!!!

[i] Pin 1:      2E7D2C03A9507AE265ECF5B5356885A53393A2029D241394997265A1A25AEFC6 - BINGO!!!
[i] Pin 2:      454349E422F05297191EAD13E21D3DB520E5ABEF52055E4964B82FB213F593A1 - WRONG!!!
[i] WARNING - This unlock attempt has been logged and @pancake has been notified!!!

[i] 1/12 pins correct.

Analysing the Pin Functionality

From the initial analysis I recognised the Pin 1 value from the main function dissasembly. That value was populated at offset 0x00401955 as seen below.

[0x004018c]> pdf @ main
...
│    │ └──> 0x00401955      488b3c25f040.  mov rdi, qword [0x4040f0]   ; [0x4040f0:8]=0x402004 str.2E7D2C03A9507AE265ECF5B5356885A53393A2029D241394997265A1A25AEFC6454349E422F05297191EAD13E21D3DB520E5ABEF52055E4964B82FB213F593A1 ; int64_t arg1
...

A quick grep reveals that the second half of this 128 character value is treated as Pin 2, therefore it is safe to assume that each pin is 64 characters in length. I presume this was placed here as a clue to provide context.

Using the information gathered so far, I created a file containing these values in /tmp/masterkey.in and tried to run the binary again. This produced similar output, however, I could see a new message [i] Procesing input code... as seen below.

➜ ./CyberLock 
[i] R2conCTF 2020 - CyberLock 1.3.3.7-alpha

     .--------.
    / .------. \
   / /        \ \
   | |        | |
  _| |________| |_
.' |_|        |_| '.
'._____ ____ _____.'
|     .'____'.     |
'.__.'.'    '.'.__.'
'.__  |!R2Con|  __.'
|   '.'.____.'.'   |
'.____'.____.'____.'
'.________________.'

[i] The r2con Cyberlock is the world's only quantum, military-grade, agile, blockchain backed, hardened, bullet-proof 12-pin digital cryptographic cyber lock.
[i] Hackers will never make it past the first pin!!!!

[i] Procesing input code...
[i] Pin 1:      2E7D2C03A9507AE265ECF5B5356885A53393A2029D241394997265A1A25AEFC6 - BINGO!!!
[i] Pin 2:      454349E422F05297191EAD13E21D3DB520E5ABEF52055E4964B82FB213F593A1 - WRONG!!!
[i] WARNING - This unlock attempt has been logged and @pancake has been notified!!!

[i] 1/12 pins correct.

This indicates that the binary is now reading the pin values from a file as the condition at offset 0x0040192a is no longer satisified. Meaning the binary is able to find the file and read the contents.

...
│      ││   0x00401926      837dd400       cmp dword [var_2ch], 0
│     ┌───< 0x0040192a      0f8420000000   je 0x401950
│     │││   0x00401930      48bf11274000.  movabs rdi, str.i__Procesing_input_code... ; 0x402711 ; "[i] Procesing input code...\n" ; const char *format
│     │││   0x0040193a      b000           mov al, 0
...

The next step is to figure out what is happening in the fcn.004014d0 function as this is clearly handling the pin values in some way. The dissasembly of this function is significantly harder to follow so I decided to work off of the decompiled code. For convinience I used the Ghidra decompiler plugin for r2, r2ghidra-dec. Through this plugin you’re able to leverage the Ghidra decompiler by running the pdg command in r2, the results of this can be seen below:

[0x004018c]> pdg
void FUN_004014d0(byte *param_1,uint param_2)

{
  int iVar1;
  ushort **ppuVar2;
  size_t sVar3;
  undefined8 uVar4;
  char **ppcVar5;
  long lVar6;
  ulong uVar7;
  char local_70b [3];
  char local_708 [128];
  char local_688 [1548];
  uint local_7c;
  long local_78;
  uint local_6c;
  byte local_68 [64];
  undefined uStack40;
  undefined8 local_20;
  uint local_14;
  byte *local_10;
  
  local_20 = 0x40;
  local_6c = 1;
  local_78 = 0;
  local_7c = 0;
  local_14 = param_2;
  local_10 = param_1;
  memset(local_688,0,0x601);
  do {
    if (*local_10 == 0) {
LAB_004017fb:
      if (local_7c == 0xc) {
        printf("\n[i] %s12%s/12 pins correct.\n",&DAT_004025b9,&DAT_004025bf);
        printf("[i] Welcome back %s@pancake%s, using your master key: %s\n",&DAT_004025b9,
               &DAT_004025bf,local_688);
        FUN_00401400(local_688);
      }
      else {
        printf("\n[i] %s%d%s/12 pins correct.\n",&DAT_00402628,(ulong)local_7c,&DAT_004025bf);
      }
      return;
    }
    if (0xc < (int)local_6c) {
      printf("[!] stack-based pin overflow!!!\n");
      goto LAB_004017fb;
    }
    ppuVar2 = __ctype_b_loc();
    if (((*ppuVar2)[(int)(uint)*local_10] & 0x2000) == 0) {
      lVar6 = local_78 + 1;
      local_68[local_78] = *local_10;
      local_78 = lVar6;
      if (lVar6 == 0x40) {
        uStack40 = 0;
        local_78 = 0;
        strcpy(local_708,(char *)local_68);
        strcat(local_708,"_");
        memset(local_70b,0,3);
        sprintf(local_70b,"%d",(ulong)local_14);
        strcat(local_708,local_70b);
        strcat(local_708,"_");
        strcat(local_708,PTR_s_r2con2020_004040f8);
        sVar3 = strlen(local_708);
        uVar4 = g_compute_checksum_for_string(3,local_708,sVar3);
        uVar4 = g_string_new(uVar4);
        ppcVar5 = (char **)g_string_ascii_up(uVar4);
        iVar1 = strncmp((&PTR_s_D9259A378FC0EA75C1828369059854DE_00404100)[(int)(local_6c - 1)],
                        *ppcVar5,0x80);
        if (iVar1 != 0) {
          uVar7 = (ulong)local_6c;
          local_6c = local_6c + 1;
          printf("[i] Pin %d:\t%s - %sWRONG!!!%s\n",uVar7,local_68,&DAT_00402628,&DAT_004025bf);
          printf(
                 "[i] %sWARNING - This unlock attempt has been logged and @pancake has beennotified!!!%s\n"
                 ,&DAT_00402628,&DAT_004025bf);
          goto LAB_004017fb;
        }
        uVar7 = (ulong)local_6c;
        local_6c = local_6c + 1;
        printf("[i] Pin %d:\t%s - %sBINGO!!!%s\n",uVar7,local_68,&DAT_004025b9,&DAT_004025bf);
        strcat(local_688,(char *)local_68);
        local_7c = local_7c + 1;
        if (local_7c == 0xc) goto LAB_004017fb;
      }
    }
    local_10 = local_10 + 1;
  } while( true );
}

In the decompiled code the first and second parameters of the function are stored in local_10 and local_14 variables respectively. The first parameter contains the pin values in what looks like an array, and the second parameter contains the value 3.

The following decompiled snippet takes the pin value in local_10 and copies it into local_708 using strcpy. The local_709 value is then concatinated with an underscore, the second parameter stored in local_14, another underscore, and finally r2con2020.

This is then hashed using g_compute_checksum_for_string. The documentation here highlights that the first parameter is the checksum type, the second is the string to hash, and the third is the string length. After some further digging, a CheckSumType value of 3 indicates it is using the SHA-512 algorithm.

...
local_68[local_78] = *local_10;
local_78 = lVar6;
if (lVar6 == 0x40) {
uStack40 = 0;
local_78 = 0;
strcpy(local_708,(char *)local_68);
strcat(local_708,"_");
memset(local_70b,0,3);
sprintf(local_70b,"%d",(ulong)local_14);
strcat(local_708,local_70b);
strcat(local_708,"_");
strcat(local_708,PTR_s_r2con2020_004040f8);
sVar3 = strlen(local_708);
uVar4 = g_compute_checksum_for_string(3,local_708,sVar3);
uVar4 = g_string_new(uVar4);
...

Finally, the output of this operation is then converted to upper case and compared against a value in an array. If the value matches, a success message is printed and the loop continues onto the next pin. If not, an error message is printed.

...
ppcVar5 = (char **)g_string_ascii_up(uVar4);
iVar1 = strncmp((&PTR_s_D9259A378FC0EA75C1828369059854DE_00404100)[(int)(local_6c - 1)],
                *ppcVar5,0x80);
...

To dig a bit deeper into the hash values, I dissasmbled the function and searched for any references to strncmp. As seen below, there is a hit at offset 0x004016d2, the first parameter to this function is populated at offset 0x004016d2, and the array containing the hashes is at offset 0x404100.

[0x004018b0]> s fcn.004014d0
[0x004014d0]> pdf
...
│   ││││╎   0x004016cf      4963c0         movsxd rax, r8d
│   ││││╎   0x004016d2      488b3cc50041.  mov rdi, qword [rax*8 + 0x404100] ; const char *s1
│   ││││╎   0x004016da      488b85e8f8ff.  mov rax, qword [var_718h]
│   ││││╎   0x004016e1      488b30         mov rsi, qword [rax]        ; const char *s2
│   ││││╎   0x004016e4      e877f9ffff     call sym.imp.strncmp        ; int strncmp(const char *s1, const char *s2, size_t n)
│   ││││╎   0x004016e9      8985e4f8ffff   mov dword [var_71ch], eax
│   ││││╎   0x004016ef      83bde4f8ffff.  cmp dword [var_71ch], 0
...

Printing the hexdump values at this offset highlights that this contains an array of pointers.

[0x004014d0]> px @ 0x404100
- offset -   0 1  2 3  4 5  6 7  8 9  A B  C D  E F  0123456789ABCDEF
0x00404100  8f20 4000 0000 0000 1021 4000 0000 0000  . @......!@.....
0x00404110  9121 4000 0000 0000 1222 4000 0000 0000  .!@......"@.....
0x00404120  9322 4000 0000 0000 1423 4000 0000 0000  ."@......#@.....
0x00404130  1222 4000 0000 0000 9523 4000 0000 0000  ."@......#@.....
0x00404140  1624 4000 0000 0000 9724 4000 0000 0000  .$@......$@.....
0x00404150  1825 4000 0000 0000 9322 4000 0000 0000  .%@......"@.....
0x00404160  0000 0000 0000 0000 ffff ffff ffff ffff  ................

Endianness makes this a bit more confusing, the first index of the array indicated at 8f204000 is actually 0040208f. Printing these values reveals the hash values as seen below.

[0x004014d0]> pd 12 @  0x0040208f
;-- str.D9259A378FC0EA75C1828369059854DEF76F91CABD01B56EF548C7ABA6441D59DD2DB9E51BD70C457179A123D53138F88132B29C691A9671F6C80E41E7D682A4:
0x0040208f     .string "D9259A378FC0EA75C1828369059854DEF76F91CABD01B56EF548C7ABA6441D59DD2DB9E51BD70C457179A123D53138F88132B29C691A9671F6C80E41E7D682A4" ; len=129
;-- str.3F675784DABBC1330806CB4D5FE67860C25E103B75EB7B22821B2D75264B60C4D2E042A1726E1B85E3936A2A95E835E92FEFAC2C2E34A249109D98AED3962C71:
0x00402110     .string "3F675784DABBC1330806CB4D5FE67860C25E103B75EB7B22821B2D75264B60C4D2E042A1726E1B85E3936A2A95E835E92FEFAC2C2E34A249109D98AED3962C71" ; len=129
;-- str.BD605F0A10170EC1F7610BCF696C518AAB31C235CCF47866A71C332368BFE8B7E9FE4E3907B3CD37F111B40EEFFE89287C25033A41369A3236751474AD522537:
0x00402191     .string "BD605F0A10170EC1F7610BCF696C518AAB31C235CCF47866A71C332368BFE8B7E9FE4E3907B3CD37F111B40EEFFE89287C25033A41369A3236751474AD522537" ; len=129
;-- str.B8CA8A22501EAB44AD44E6EB2F87791CA69955BFFC025988418866BDD8A1EAABA97F630A705D68B26370E49DAD9DE65AB478117C4289FFC240A8C85308D116A1:
0x00402212     .string "B8CA8A22501EAB44AD44E6EB2F87791CA69955BFFC025988418866BDD8A1EAABA97F630A705D68B26370E49DAD9DE65AB478117C4289FFC240A8C85308D116A1" ; len=129
;-- str.0694F2776E101C72092994B2F125973CBD3A608B3BA17CF8795DDDEEDE6773E0DE8D832EAE4EE3FF8C748FD6BDBC1BF843081CC6AEEFB09B1C908AD484382884:
0x00402293     .string "0694F2776E101C72092994B2F125973CBD3A608B3BA17CF8795DDDEEDE6773E0DE8D832EAE4EE3FF8C748FD6BDBC1BF843081CC6AEEFB09B1C908AD484382884" ; len=129
;-- str.F9B4F710E8ADC5D4A32779A865396B019A630E2B1DB475F0F1656BC195ED2E0171932C9629DE5417F5C2D96FECC4B89107C9E28477513F98D5147391DF374C3C:
0x00402314     .string "F9B4F710E8ADC5D4A32779A865396B019A630E2B1DB475F0F1656BC195ED2E0171932C9629DE5417F5C2D96FECC4B89107C9E28477513F98D5147391DF374C3C" ; len=129
;-- str.7280E46538FE2158921FA20E7A835E9F4900E8AFDA73E1D1EE03472E0B34D59AC28ABE964652B6EC8DF1B44D3F1B90A7684D470A191BB446FB2149918E69DE38:
0x00402395     .string "7280E46538FE2158921FA20E7A835E9F4900E8AFDA73E1D1EE03472E0B34D59AC28ABE964652B6EC8DF1B44D3F1B90A7684D470A191BB446FB2149918E69DE38" ; len=129
;-- str.F08DF5E5DFDACE8892F03B4475263D72D6999DCC499C741028935D7D167222086F5FB63C793C20BA02E56AB74E901B1E66D178206E91B469F6055E63EE824D7F:
0x00402416     .string "F08DF5E5DFDACE8892F03B4475263D72D6999DCC499C741028935D7D167222086F5FB63C793C20BA02E56AB74E901B1E66D178206E91B469F6055E63EE824D7F" ; len=129
;-- str.9D9A2B793485D2ADF818DB5DF1D97CD1072F826845940780ECA50B623F8D931DC9533AF77DF2B811645B8B852670188A17736E8A536F672D5639EA40F2D1FC40:
0x00402497     .string "9D9A2B793485D2ADF818DB5DF1D97CD1072F826845940780ECA50B623F8D931DC9533AF77DF2B811645B8B852670188A17736E8A536F672D5639EA40F2D1FC40" ; len=129
;-- str.4AC1807EC58C1BD56D177E3F409FBD0FBCDE7D97F9F8A71469FD92817BB866B4D71A0DF10EB501AF6CE4439861E46B89473555B08874CEBA33312AD3495E84F2:
0x00402518     .string "4AC1807EC58C1BD56D177E3F409FBD0FBCDE7D97F9F8A71469FD92817BB866B4D71A0DF10EB501AF6CE4439861E46B89473555B08874CEBA33312AD3495E84F2" ; len=129
; DATA XREF from fcn.004012b0 @ 0x4012ca
;-- str.000000000000:
0x00402599     .string "000000000000" ; len=13
; DATA XREF from fcn.00401400 @ 0x40149b
;-- str.i__Flag:__s_s_s:
0x004025a6     .string "\n[i] Flag: %s%s%s\n" ; len=19

Interestingly, I assumed there would be twelve distinct values hence why I printed twelve instructions above, however, it looks like some of the hashes are reused. So for example pin four is used again as pin seven, and pin five is used again as pin twelve. I verified this by taking a closer look at the hexdump values in the array of pointers (that point to the hash values).

Duplicate pin hashes

To confirm my hypothesis I constructed the first known pin using the proposed format of pin_3_r2con2020. Since the first pin is given for free, the first pin hash should equal the sha512 of the following input.

2E7D2C03A9507AE265ECF5B5356885A53393A2029D241394997265A1A25AEFC6_3_r2con2020

The upper case value of this opeation matches the hash at offset 0x0040208f.

Deciphering the Pin Values

So at this point I have the final hashes, I know how the final hashes are generated and salted but I don’t know anything about the pin values themselves. After a lot more digging I hit a dead end, I didn’t find any further clues and started doing some guess work.

Based off of the first pin I know that the pins are probably 64 characters long. I know that sha256 hashes are 256 bits long, 4 bits are enough to encode each character, so 64 hex characters matches the profile. I found the following “SHA-256 Decoder” here and attempted to “decode” the hash.

Online sha256 decoder

This found the hash contained the value c. I repeated this step on the second pin that is provded and the hash contained the value r. I confirmed this work flow by hasing the value c using sha256, converting the output to upper case, appending _3_r2con2020 to the output, hashing this using sha512, and then converting it to upper case again. The output of this work flow produced an identical hash value to the first pin.

Generating the Hashed Pins

Since the pin values seem to be a single character, brute-forcing the pin values seemed like the best option. I decided to write a script to automate this. The script performs the following operations for each letter of the alphabet, both upper and lower case:

  • Sha256 hashes the character
  • Converts the digest to upper case
  • Appends _3_r2con2020 to the digest
  • Sha512 hashes the concatenated digest
  • Converts the sha512 digest to upper case
  • Compares the final output against the known pin hash
  • If there is a match, it moves onto the next pin hash

This process is repeated for each pin hash. The working script is available here. As seen below, I was able to successfully identify the precomputed pin values.

[INFO] we have 12 hashy boys
[INFO] Solving has 0
        Match found for d9259a378fc0ea75c1828369059854def76f91cabd01b56ef548c7aba6441d59dd2db9e51bd70c457179a123d53138f88132b29c691a9671f6c80e41e7d682a4!
        Key 0 char is c, key is: 2E7D2C03A9507AE265ECF5B5356885A53393A2029D241394997265A1A25AEFC6
[INFO] Solving has 1
        Match found for 3f675784dabbc1330806cb4d5fe67860c25e103b75eb7b22821b2d75264b60c4d2e042a1726e1b85e3936a2a95e835e92fefac2c2e34a249109d98aed3962c71!
        Key 1 char is h, key is: AAA9402664F1A41F40EBBC52C9993EB66AEB366602958FDFAA283B71E64DB123
[INFO] Solving has 2
        Match found for bd605f0a10170ec1f7610bcf696c518aab31c235ccf47866a71c332368bfe8b7e9fe4e3907b3cd37f111b40eeffe89287c25033a41369a3236751474ad522537!
        Key 2 char is a, key is: CA978112CA1BBDCAFAC231B39A23DC4DA786EFF8147C4E72B9807785AFEE48BB
[INFO] Solving has 3
        Match found for b8ca8a22501eab44ad44e6eb2f87791ca69955bffc025988418866bdd8a1eaaba97f630a705d68b26370e49dad9de65ab478117c4289ffc240a8c85308d116a1!
        Key 3 char is o, key is: 65C74C15A686187BB6BBF9958F494FC6B80068034A659A9AD44991B08C58F2D2
[INFO] Solving has 4
        Match found for 0694f2776e101c72092994b2f125973cbd3a608b3ba17cf8795dddeede6773e0de8d832eae4ee3ff8c748fd6bdbc1bf843081cc6aeefb09b1c908ad484382884!
        Key 4 char is s, key is: 043A718774C572BD8A25ADBEB1BFCD5C0256AE11CECF9F9C3F925D0E52BEAF89
[INFO] Solving has 5
        Match found for f9b4f710e8adc5d4a32779a865396b019a630e2b1db475f0f1656bc195ed2e0171932c9629de5417f5c2d96fecc4b89107c9e28477513f98d5147391df374c3c!
        Key 5 char is M, key is: 08F271887CE94707DA822D5263BAE19D5519CB3614E0DAEDC4C7CE5DAB7473F1
[INFO] Solving has 6
        Match found for b8ca8a22501eab44ad44e6eb2f87791ca69955bffc025988418866bdd8a1eaaba97f630a705d68b26370e49dad9de65ab478117c4289ffc240a8c85308d116a1!
        Key 6 char is o, key is: 65C74C15A686187BB6BBF9958F494FC6B80068034A659A9AD44991B08C58F2D2
[INFO] Solving has 7
        Match found for 7280e46538fe2158921fa20e7a835e9f4900e8afda73e1d1ee03472e0b34d59ac28abe964652b6ec8df1b44d3f1b90a7684d470a191bb446fb2149918e69de38!
        Key 7 char is N, key is: 8CE86A6AE65D3692E7305E2C58AC62EEBD97D3D943E093F577DA25C36988246B
[INFO] Solving has 8
        Match found for f08df5e5dfdace8892f03b4475263d72d6999dcc499c741028935d7d167222086f5fb63c793c20ba02e56ab74e901b1e66d178206e91b469f6055e63ee824d7f!
        Key 8 char is K, key is: 86BE9A55762D316A3026C2836D044F5FC76E34DA10E1B45FEEE5F18BE7EDB177
[INFO] Solving has 9
        Match found for 9d9a2b793485d2adf818db5df1d97cd1072f826845940780eca50b623f8d931dc9533af77df2b811645b8b852670188a17736e8a536f672d5639ea40f2d1fc40!
        Key 9 char is e, key is: 3F79BB7B435B05321651DAEFD374CDC681DC06FAA65E374E38337B88CA046DEA
[INFO] Solving has 10
        Match found for 4ac1807ec58c1bd56d177e3f409fbd0fbcde7d97f9f8a71469fd92817bb866b4d71a0df10eb501af6ce4439861e46b89473555b08874ceba33312ad3495e84f2!
        Key 10 char is y, key is: A1FCE4363854FF888CFF4B8E7875D600C2682390412A8CF79B37D0B11148B0FA
[INFO] Solving has 11
        Match found for 0694f2776e101c72092994b2f125973cbd3a608b3ba17cf8795dddeede6773e0de8d832eae4ee3ff8c748fd6bdbc1bf843081cc6aeefb09b1c908ad484382884!
        Key 11 char is s, key is: 043A718774C572BD8A25ADBEB1BFCD5C0256AE11CECF9F9C3F925D0E52BEAF89
[INFO] Complete solution:
2E7D2C03A9507AE265ECF5B5356885A53393A2029D241394997265A1A25AEFC6AAA9402664F1A41F40EBBC52C9993EB66AEB366602958FDFAA283B71E64DB123CA978112CA1BBDCAFAC231B39A23DC4DA786EFF8147C4E72B9807785AFEE48BB65C74C15A686187BB6BBF9958F494FC6B80068034A659A9AD44991B08C58F2D2043A718774C5
72BD8A25ADBEB1BFCD5C0256AE11CECF9F9C3F925D0E52BEAF8908F271887CE94707DA822D5263BAE19D5519CB3614E0DAEDC4C7CE5DAB7473F165C74C15A686187BB6BBF9958F494FC6B80068034A659A9AD44991B08C58F2D28CE86A6AE65D3692E7305E2C58AC62EEBD97D3D943E093F577DA25C36988246B86BE9A55762D316A3026C283
6D044F5FC76E34DA10E1B45FEEE5F18BE7EDB1773F79BB7B435B05321651DAEFD374CDC681DC06FAA65E374E38337B88CA046DEAA1FCE4363854FF888CFF4B8E7875D600C2682390412A8CF79B37D0B11148B0FA043A718774C572BD8A25ADBEB1BFCD5C0256AE11CECF9F9C3F925D0E52BEAF89

The precomputed pin key spells out chaosMoNKeys. The last step is to copy and paste the upper case sha256 hash for each pin into the /tmp/masterkey.in file.

➜ cat /tmp/masterkey.in
2E7D2C03A9507AE265ECF5B5356885A53393A2029D241394997265A1A25AEFC6
AAA9402664F1A41F40EBBC52C9993EB66AEB366602958FDFAA283B71E64DB123
CA978112CA1BBDCAFAC231B39A23DC4DA786EFF8147C4E72B9807785AFEE48BB
65C74C15A686187BB6BBF9958F494FC6B80068034A659A9AD44991B08C58F2D2
043A718774C572BD8A25ADBEB1BFCD5C0256AE11CECF9F9C3F925D0E52BEAF89
08F271887CE94707DA822D5263BAE19D5519CB3614E0DAEDC4C7CE5DAB7473F1
65C74C15A686187BB6BBF9958F494FC6B80068034A659A9AD44991B08C58F2D2
8CE86A6AE65D3692E7305E2C58AC62EEBD97D3D943E093F577DA25C36988246B
86BE9A55762D316A3026C2836D044F5FC76E34DA10E1B45FEEE5F18BE7EDB177
3F79BB7B435B05321651DAEFD374CDC681DC06FAA65E374E38337B88CA046DEA
A1FCE4363854FF888CFF4B8E7875D600C2682390412A8CF79B37D0B11148B0FA
043A718774C572BD8A25ADBEB1BFCD5C0256AE11CECF9F9C3F925D0E52BEAF89

Run the Cyberlock binary again and verify the challenge is solved:

➜ ./CyberLock
[i] R2conCTF 2020 - CyberLock 1.3.3.7-alpha

     .--------.
    / .------. \
   / /        \ \
   | |        | |
  _| |________| |_
.' |_|        |_| '.
'._____ ____ _____.'
|     .'____'.     |
'.__.'.'    '.'.__.'
'.__  |!R2Con|  __.'
|   '.'.____.'.'   |
'.____'.____.'____.'
'.________________.'

[i] The r2con Cyberlock is the world's only quantum, military-grade, agile, blockchain backed, hardened, bullet-proof 12-pin digital cryptographic cyber lock.
[i] Hackers will never make it past the first pin!!!!

[i] Procesing input code...
[i] Pin 1:      2E7D2C03A9507AE265ECF5B5356885A53393A2029D241394997265A1A25AEFC6 - BINGO!!!
[i] Pin 2:      AAA9402664F1A41F40EBBC52C9993EB66AEB366602958FDFAA283B71E64DB123 - BINGO!!!
[i] Pin 3:      CA978112CA1BBDCAFAC231B39A23DC4DA786EFF8147C4E72B9807785AFEE48BB - BINGO!!!
[i] Pin 4:      65C74C15A686187BB6BBF9958F494FC6B80068034A659A9AD44991B08C58F2D2 - BINGO!!!
[i] Pin 5:      043A718774C572BD8A25ADBEB1BFCD5C0256AE11CECF9F9C3F925D0E52BEAF89 - BINGO!!!
[i] Pin 6:      08F271887CE94707DA822D5263BAE19D5519CB3614E0DAEDC4C7CE5DAB7473F1 - BINGO!!!
[i] Pin 7:      65C74C15A686187BB6BBF9958F494FC6B80068034A659A9AD44991B08C58F2D2 - BINGO!!!
[i] Pin 8:      8CE86A6AE65D3692E7305E2C58AC62EEBD97D3D943E093F577DA25C36988246B - BINGO!!!
[i] Pin 9:      86BE9A55762D316A3026C2836D044F5FC76E34DA10E1B45FEEE5F18BE7EDB177 - BINGO!!!
[i] Pin 10:     3F79BB7B435B05321651DAEFD374CDC681DC06FAA65E374E38337B88CA046DEA - BINGO!!!
[i] Pin 11:     A1FCE4363854FF888CFF4B8E7875D600C2682390412A8CF79B37D0B11148B0FA - BINGO!!!
[i] Pin 12:     043A718774C572BD8A25ADBEB1BFCD5C0256AE11CECF9F9C3F925D0E52BEAF89 - BINGO!!!

[i] 12/12 pins correct.
[i] Welcome back @pancake, using your master key: 2E7D2C03A9507AE265ECF5B5356885A53393A2029D241394997265A1A25AEFC6AAA9402664F1A41F40EBBC52C9993EB66AEB366602958FDFAA283B71E64DB123CA978112CA1BBDCAFAC231B39A23DC4DA786EFF8147C4E72B9807785AFEE48BB65C74C15A686187BB6BBF9958F
494FC6B80068034A659A9AD44991B08C58F2D2043A718774C572BD8A25ADBEB1BFCD5C0256AE11CECF9F9C3F925D0E52BEAF8908F271887CE94707DA822D5263BAE19D5519CB3614E0DAEDC4C7CE5DAB7473F165C74C15A686187BB6BBF9958F494FC6B80068034A659A9AD44991B08C58F2D28CE86A6AE65D3692E7305E2C58AC62EEBD97D3
D943E093F577DA25C36988246B86BE9A55762D316A3026C2836D044F5FC76E34DA10E1B45FEEE5F18BE7EDB1773F79BB7B435B05321651DAEFD374CDC681DC06FAA65E374E38337B88CA046DEAA1FCE4363854FF888CFF4B8E7875D600C2682390412A8CF79B37D0B11148B0FA043A718774C572BD8A25ADBEB1BFCD5C0256AE11CECF9F9C3F
925D0E52BEAF89

[i] Flag: r2con{StopL0ckPickingY0urNos3}

The flag for this challenge is r2con{StopL0ckPickingY0urNos3}.

Conclusion

Cyberlock is a really interesting and creative CTF challenge. The mechanic of making the flag depend on twelve ambiguous pins is really smart, it forces the attacker to think - I really enjoyed that. Also, I appreciated the attempt to rate limit attackers. It provided a nice entry level problem to teach players how to use r2 practically.

A big thank you to the Radare2 team for organising the conference and to @hexploitable for writing the challenge. I got in contact with @hexploitable and they were kind enough to share the challenge source here. They kindly gave me permission to include it in this post.

Useful References