All Articles

SECCON 2022 Qualifier Challenges (Rev only)

This page has been machine-translated from the original page.

I will work through the Rev challenges from the SECCON qualifiers one by one.

I plan to add the other challenges later.

For the challenges and write-ups, refer to the following official repository.

Reference: GitHub - SECCON/SECCON2022onlineCTF

Table of Contents

baby_cmp(Rev)

baby_mode = 1 👶

When I decompiled the challenge binary in Ghidra, it turned out to be a simple password-checking program.

I was able to recover the flag with a template angr script.

import angr
import claripy

proj = angr.Project("chall.baby", auto_load_libs=False)
obj = proj.loader.main_object
print("Entry", hex(obj.entry))

find = 0x4012c9
avoids = 0x40127a

flag = claripy.BVS("flag", 0x23 * 8)
state = proj.factory.entry_state(args=["./chall.baby", flag])
for i in range(0x23):
    state.solver.add(flag.get_byte(i) >= 0x21)
    state.solver.add(flag.get_byte(i) <= 0x7f)

simgr = proj.factory.simgr(state)
simgr.explore(find=find, avoid=avoids)

# Output
simgr.found[0].solver.eval(flag, cast_to=bytes)

# SECCON{y0u_f0und_7h3_baby_flag_YaY}

eguite(Rev)

Crack me.

Running the binary shows that it is a GUI program that performs license verification, as shown below.

image-20230819214020573

I started by decompiling it with Ghidra.

Because it is a GUI program, locating the actual verification logic takes a bit of work, but searching for the string Invalid license let me identify the relevant code.

The onClick function below appears to be the one called when the Check button is pressed.

image-20230819215345672

For reference, here is Ghidra’s decompiled output.

You can see that it performs fairly complex validation on the input.

/* eguite::Crackme::onclick */
bool eguite::Crackme::onclick(long param_1)
{
  uint *puVar1;
  byte bVar2;
  void *pvVar3;
  uint *puVar4;
  long lVar5;
  uint uVar6;
  undefined **ppuVar7;
  uint *puVar8;
  uint uVar9;
  uint *puVar10;
  uint *puVar11;
  uint *local_68;
  uint *local_60;
  undefined4 local_58;
  undefined4 uStack_54;
  undefined4 uStack_50;
  undefined4 uStack_4c;
  void *local_48;
  long local_40;
  undefined8 local_38;
  
  if (*(long *)(param_1 + 0x90) != 0x2b) {
    return false;
  }
  puVar11 = *(uint **)(param_1 + 0x80);
  if ((*(uint *)((long)puVar11 + 3) ^ 0x7b4e4f43 | *puVar11 ^ 0x43434553) != 0) {
    return false;
  }
  if (*(byte *)((long)puVar11 + 0x2a) != 0x7d) {
    return false;
  }
  puVar1 = (uint *)((long)puVar11 + 0x2b);
  lVar5 = 0x13;
  puVar4 = puVar11;
  do {
    if (puVar4 == puVar1) goto LAB_0016027f;
    bVar2 = *(byte *)puVar4;
    if ((char)bVar2 < '\0') {
      if (bVar2 < 0xe0) {
        puVar4 = (uint *)((long)puVar4 + 2);
      }
      else if (bVar2 < 0xf0) {
        puVar4 = (uint *)((long)puVar4 + 3);
      }
      else {
        if ((*(byte *)((long)puVar4 + 3) & 0x3f |
             (*(byte *)((long)puVar4 + 2) & 0x3f) << 6 | (*(byte *)((long)puVar4 + 1) & 0x3f) << 0xc
            | (bVar2 & 7) << 0x12) == 0x110000) goto LAB_0016027f;
        puVar4 = puVar4 + 1;
      }
    }
    else {
      puVar4 = (uint *)((long)puVar4 + 1);
    }
    lVar5 = lVar5 + -1;
  } while (lVar5 != 0);
  if (puVar4 == puVar1) {
LAB_0016027f:
    ppuVar7 = &PTR_s_src/main.rsgetrandom_getrandom(_00725118;
    goto LAB_0016046d;
  }
  bVar2 = *(byte *)puVar4;
  uVar9 = (uint)bVar2;
  if ((char)bVar2 < '\0') {
    uVar6 = bVar2 & 0x1f;
    uVar9 = *(byte *)((long)puVar4 + 1) & 0x3f;
    if (bVar2 < 0xe0) {
      uVar9 = uVar6 << 6 | uVar9;
    }
    else {
      uVar9 = *(byte *)((long)puVar4 + 2) & 0x3f | uVar9 << 6;
      if (bVar2 < 0xf0) {
        uVar9 = uVar9 | uVar6 << 0xc;
      }
      else {
        uVar9 = *(byte *)((long)puVar4 + 3) & 0x3f | uVar9 << 6 | (bVar2 & 7) << 0x12;
        if (uVar9 == 0x110000) goto LAB_0016027f;
      }
    }
  }
  if (uVar9 != 0x2d) {
    return false;
  }
  lVar5 = 0x1a;
  puVar4 = puVar11;
  do {
    if (puVar4 == puVar1) goto LAB_0016036f;
    bVar2 = *(byte *)puVar4;
    if ((char)bVar2 < '\0') {
      if (bVar2 < 0xe0) {
        puVar4 = (uint *)((long)puVar4 + 2);
      }
      else if (bVar2 < 0xf0) {
        puVar4 = (uint *)((long)puVar4 + 3);
      }
      else {
        if ((*(byte *)((long)puVar4 + 3) & 0x3f |
             (*(byte *)((long)puVar4 + 2) & 0x3f) << 6 | (*(byte *)((long)puVar4 + 1) & 0x3f) << 0xc
            | (bVar2 & 7) << 0x12) == 0x110000) goto LAB_0016036f;
        puVar4 = puVar4 + 1;
      }
    }
    else {
      puVar4 = (uint *)((long)puVar4 + 1);
    }
    lVar5 = lVar5 + -1;
  } while (lVar5 != 0);
  if (puVar4 == puVar1) {
LAB_0016036f:
    ppuVar7 = &PTR_s_src/main.rsgetrandom_getrandom(_00725130;
  }
  else {
    bVar2 = *(byte *)puVar4;
    uVar9 = (uint)bVar2;
    if ((char)bVar2 < '\0') {
      uVar6 = bVar2 & 0x1f;
      uVar9 = *(byte *)((long)puVar4 + 1) & 0x3f;
      if (bVar2 < 0xe0) {
        uVar9 = uVar6 << 6 | uVar9;
      }
      else {
        uVar9 = *(byte *)((long)puVar4 + 2) & 0x3f | uVar9 << 6;
        if (bVar2 < 0xf0) {
          uVar9 = uVar9 | uVar6 << 0xc;
        }
        else {
          uVar9 = *(byte *)((long)puVar4 + 3) & 0x3f | uVar9 << 6 | (bVar2 & 7) << 0x12;
          if (uVar9 == 0x110000) goto LAB_0016036f;
        }
      }
    }
    if (uVar9 != 0x2d) {
      return false;
    }
    lVar5 = 0x21;
    puVar4 = puVar11;
    do {
      if (puVar4 == puVar1) goto LAB_0016045f;
      bVar2 = *(byte *)puVar4;
      if ((char)bVar2 < '\0') {
        if (bVar2 < 0xe0) {
          puVar4 = (uint *)((long)puVar4 + 2);
        }
        else if (bVar2 < 0xf0) {
          puVar4 = (uint *)((long)puVar4 + 3);
        }
        else {
          if ((*(byte *)((long)puVar4 + 3) & 0x3f |
               (*(byte *)((long)puVar4 + 2) & 0x3f) << 6 |
               (*(byte *)((long)puVar4 + 1) & 0x3f) << 0xc | (bVar2 & 7) << 0x12) == 0x110000)
          goto LAB_0016045f;
          puVar4 = puVar4 + 1;
        }
      }
      else {
        puVar4 = (uint *)((long)puVar4 + 1);
      }
      lVar5 = lVar5 + -1;
    } while (lVar5 != 0);
    if (puVar4 != puVar1) {
      bVar2 = *(byte *)puVar4;
      uVar9 = (uint)bVar2;
      if (-1 < (char)bVar2) {
LAB_0016048a:
        if (uVar9 != 0x2d) {
          return false;
        }
        local_58 = 7;
        uStack_54 = 0;
        uStack_50 = 0xc;
        uStack_4c = 0;
        local_68 = puVar11;
        local_60 = puVar1;
        <>::from_iter(&local_48,&local_68);
        pvVar3 = local_48;
        core::num::<impl_u64>::from_str_radix(&local_68,local_48,local_38,0x10);
        puVar4 = local_60;
        if ((char)local_68 != '\0') {
          puVar4 = (uint *)0x0;
        }
        if (local_40 != 0) {
          __rust_dealloc(pvVar3);
        }
        local_58 = 0x14;
        uStack_54 = 0;
        uStack_50 = 6;
        uStack_4c = 0;
        local_68 = puVar11;
        local_60 = puVar1;
        <>::from_iter(&local_48,&local_68);
        pvVar3 = local_48;
        core::num::<impl_u64>::from_str_radix(&local_68,local_48,local_38,0x10);
        puVar10 = local_60;
        if ((char)local_68 != '\0') {
          puVar10 = (uint *)0x0;
        }
        if (local_40 != 0) {
          __rust_dealloc(pvVar3);
        }
        local_58 = 0x1b;
        uStack_54 = 0;
        uStack_50 = 6;
        uStack_4c = 0;
        local_68 = puVar11;
        local_60 = puVar1;
        <>::from_iter(&local_48,&local_68);
        pvVar3 = local_48;
        core::num::<impl_u64>::from_str_radix(&local_68,local_48,local_38,0x10);
        puVar8 = local_60;
        if ((char)local_68 != '\0') {
          puVar8 = (uint *)0x0;
        }
        if (local_40 != 0) {
          __rust_dealloc(pvVar3);
        }
        local_58 = 0x22;
        uStack_54 = 0;
        uStack_50 = 8;
        uStack_4c = 0;
        local_68 = puVar11;
        local_60 = puVar1;
        <>::from_iter(&local_48,&local_68);
        core::num::<impl_u64>::from_str_radix(&local_68,local_48,local_38,0x10);
        puVar11 = local_60;
        if ((char)local_68 != '\0') {
          puVar11 = (uint *)0x0;
        }
        if (local_40 != 0) {
          __rust_dealloc(local_48);
        }
        if ((byte *)((long)puVar4 + (long)puVar10) != (byte *)0x8b228bf35f6a) {
          return false;
        }
        if ((byte *)((long)puVar8 + (long)puVar10) != (byte *)0xe78241) {
          return false;
        }
        if ((byte *)((long)puVar11 + (long)puVar8) == (byte *)0xfa4c1a9f) {
          if ((byte *)((long)puVar4 + (long)puVar11) == (byte *)0x8b238557f7c8) {
            return ((ulong)puVar8 ^ (ulong)puVar10 ^ (ulong)puVar11) == 0xf9686f4d;
          }
          return false;
        }
        return false;
      }
      uVar6 = bVar2 & 0x1f;
      uVar9 = *(byte *)((long)puVar4 + 1) & 0x3f;
      if (bVar2 < 0xe0) {
        uVar9 = uVar6 << 6 | uVar9;
        goto LAB_0016048a;
      }
      uVar9 = *(byte *)((long)puVar4 + 2) & 0x3f | uVar9 << 6;
      if (bVar2 < 0xf0) {
        uVar9 = uVar9 | uVar6 << 0xc;
        goto LAB_0016048a;
      }
      uVar9 = *(byte *)((long)puVar4 + 3) & 0x3f | uVar9 << 6 | (bVar2 & 7) << 0x12;
      if (uVar9 != 0x110000) goto LAB_0016048a;
    }
LAB_0016045f:
    ppuVar7 = &PTR_s_src/main.rsgetrandom_getrandom(_00725148;
  }
LAB_0016046d:
  core::panicking::panic(&DAT_0048e6b0,0x2b,ppuVar7);
  do {
    invalidInstructionException();
  } while( true );
}

For this kind of pattern, I think you could recover the flag by describing the constraints in Z3Py, but I could not quite write an appropriate script.

In cases like this, it is better to patiently read through the logic.

First, in the following snippet, I investigated with GDB and found that rdi + 0x90 stores the length of the input string.

In other words, the flag string length must be 0x2b.

if (*(long *)(rdi + 0x90) != 0x2b) {
    return false;
}

Next, consider the following snippet.

rdi + 0x80 stored the input string.

input = *(uint **)(rdi + 0x80);
if ((*(uint *)((long)input + 3) ^ 0x7b4e4f43 | *input ^ 0x43434553) != 0) {
    return false;
}
if (*(byte *)((long)input + 0x2a) != 0x7d) {
    return false;
}

Here, it appears to compare the beginning and end of the input string against hard-coded values.

As shown below, these values correspond to the flag prefix, so this section is checking the flag format.

image-20230819224718006

Next, this part is a little long, but let us go through it.

The region after input + 0x2b should not contain any meaningful values yet, so I renamed it to buf.

Inside the do-while loop, it performs various operations while decrementing i, which starts at 0x13.

However, if you look closely, every byte of the input string is always greater than 0, so the code inside if ((char)c < 0x0) never runs. The loop simply increments input_ptr 0x13 times and then ends.

buf = (uint *)((long)input + 0x2b);
i = 0x13;
input_ptr = input;
do {
    if (input_ptr == buf) goto FAIL;
    c = *(byte *)input_ptr;
    if ((char)c < 0x0) {
        if (c < 0xe0) {
            input_ptr = (uint *)((long)input_ptr + 2);
        }
        else if (c < 0xf0) {
            input_ptr = (uint *)((long)input_ptr + 3);
        }
        else {
            if ((*(byte *)((long)input_ptr + 3) & 0x3f |
                    (*(byte *)((long)input_ptr + 2) & 0x3f) << 6 |
                    (*(byte *)((long)input_ptr + 1) & 0x3f) << 0xc | (c & 7) << 0x12) == 0x110000)
            goto FAIL;
            input_ptr = input_ptr + 1;
        }
    }
    else {
        input_ptr = (uint *)((long)input_ptr + 1);
    }
    i = i + -1;
} while (i != 0);

Let us continue following the logic.

In the next lines, it extracts the character from input_ptr after advancing by 0x13, stores it in i_x13, and compares it. Since this value also never becomes less than 0, in practice it is only checking whether i_x13 equals 0x2d (-).

c = *(byte *)input_ptr;
i_x13 = (uint)c;
if ((char)c < '\0') {
    uVar2 = c & 0x1f;
    uVar5 = *(byte *)((long)input_ptr + 1) & 0x3f;
    if (c < 0xe0) {
        uVar5 = uVar2 << 6 | uVar5;
    }
    else {
        uVar5 = *(byte *)((long)input_ptr + 2) & 0x3f | uVar5 << 6;
        if (c < 0xf0) {
        uVar5 = uVar5 | uVar2 << 0xc;
        }
        else {
        uVar5 = *(byte *)((long)input_ptr + 3) & 0x3f | uVar5 << 6 | (c & 7) << 0x12;
        if (uVar5 == 0x110000) goto FAIL;
        }
    }
}
if (i_x13 != 0x2d) {
    return false;
}

Let us continue with the following logic.

Here, it resets input_ptr back to the beginning of the input string.

Although various operations are defined inside the do-while loop, the input characters still never become less than 0 here, so it effectively just advances the index to 0x1a.

i = 0x1a;
input_ptr = input;
do {
if (input_ptr == buf) goto LAB_5555555b436f;
c = *(byte *)input_ptr;
if ((char)c < '\0') {
    if (c < 0xe0) {
    input_ptr = (uint *)((long)input_ptr + 2);
    }
    else if (c < 0xf0) {
    input_ptr = (uint *)((long)input_ptr + 3);
    }
    else {
    if ((*(byte *)((long)input_ptr + 3) & 0x3f |
            (*(byte *)((long)input_ptr + 2) & 0x3f) << 6 |
            (*(byte *)((long)input_ptr + 1) & 0x3f) << 0xc | (c & 7) << 0x12) == 0x110000)
    goto LAB_5555555b436f;
    input_ptr = input_ptr + 1;
    }
}
else {
    input_ptr = (uint *)((long)input_ptr + 1);
}
i = i + -1;
} while (i != 0);

The pattern should be clear by now.

Including the logic so far, if we remove the lines that can be ignored, the code looks like this.

buf = (uint *)((long)input + 0x2b);

i = 0x13;
input_ptr = input;
c = *(byte *)input_ptr;
if (c != 0x2d) {
    return false;
}

i = 0x1a;
input_ptr = input;
c = *(byte *)input_ptr;
if (c != 0x2d) {
    return false;
}

i = 0x21;
input_ptr = input;
c = *(byte *)input_ptr;
if (c != 0x2d) {
    return false;
}

In other words, these 176 lines of code are simply checking the flag prefix and whether the characters at specific positions are -.

Now that we have read this far, let us look at the second half of the code.

A = 7;
B = 0;
C = 0xc;
D = 0;
input_ptr2 = input;
buf2 = buf;
<>::from_iter(&input_arr,&input_ptr2);

input_arr_ptr = input_arr;
core::num::<impl_u64>::from_str_radix(&input_ptr2,input_arr,local_38,0x10);
input_ptr = buf2;

if ((char)input_ptr2 != '\0') {
  input_ptr = (uint *)0x0;
}
if (local_40 != 0) {
  __rust_dealloc(input_arr_ptr);
}

Ghidra’s decompilation did not seem to recover the arguments correctly, but it is calling the from_iter function.

After checking the actual behavior in GDB, it turned out to create a collection by extracting a 12-character substring starting at index 7.

In other words, it extracts the substring between SECCON{ and the first -.

It then uses from_str_radix to convert that string to a number.

Reference: from_iter - Rust

Reference: Rust: Convert a base-n string to a number

Reference: u64 - Rust

At that point, I expected the later patterns to be the same.

When I actually lined up the decompiled output below, it became clear that the program extracts each hyphen-separated segment of a flag in the format SECCON{ABCDEFGHIJKL-NOPQRS-UVWXYZ-bcdefghi} and converts each segment to a number.

A = 7;
B = 0;
C = 0xc;
D = 0;
input_ptr2 = input;
buf2 = buf;
<>::from_iter(&input_arr,&input_ptr2);
input_arr_ptr = input_arr;
core::num::<impl_u64>::from_str_radix(&input_ptr2,input_arr,local_38,0x10);
input_ptr = buf2;
if ((char)input_ptr2 != '\0') {
    input_ptr = (uint *)0x0;
}
if (local_40 != 0) {
    __rust_dealloc(input_arr_ptr);
}

A = 0x14;
B = 0;
C = 6;
D = 0;
input_ptr2 = input;
buf2 = buf;
<>::from_iter(&input_arr,&input_ptr2);
input_arr_ptr = input_arr;
core::num::<impl_u64>::from_str_radix(&input_ptr2,input_arr,local_38,0x10);
puVar5 = buf2;
if ((char)input_ptr2 != '\0') {
    puVar5 = (uint *)0x0;
}
if (local_40 != 0) {
    __rust_dealloc(input_arr_ptr);
}

A = 0x1b;
B = 0;
C = 6;
D = 0;
input_ptr2 = input;
buf2 = buf;
<>::from_iter(&input_arr,&input_ptr2);
input_arr_ptr = input_arr;
core::num::<impl_u64>::from_str_radix(&input_ptr2,input_arr,local_38,0x10);
puVar3 = buf2;
if ((char)input_ptr2 != '\0') {
    puVar3 = (uint *)0x0;
}
if (local_40 != 0) {
    __rust_dealloc(input_arr_ptr);
}

A = 0x22;
B = 0;
C = 8;
D = 0;
input_ptr2 = input;
buf2 = buf;
<>::from_iter(&input_arr,&input_ptr2);
core::num::<impl_u64>::from_str_radix(&input_ptr2,input_arr,local_38,0x10);
input = buf2;
if ((char)input_ptr2 != '\0') {
    input = (uint *)0x0;
}
if (local_40 != 0) {
    __rust_dealloc(input_arr);
}

In other words, despite how complicated this code looks, almost all of the actual flag validation comes down to the following part.

if ((byte *)((long)input_ptr + (long)puVar5) != (byte *)0x8b228bf35f6a) {
    return false;
}
if ((byte *)((long)puVar3 + (long)puVar5) != (byte *)0xe78241) {
    return false;
}
if ((byte *)((long)input + (long)puVar3) == (byte *)0xfa4c1a9f) {
    if ((byte *)((long)input_ptr + (long)input) == (byte *)0x8b238557f7c8) {
    return ((ulong)puVar3 ^ (ulong)puVar5 ^ (ulong)input) == 0xf9686f4d;
    }
    return false;
}

However, because I had renamed variables rather roughly in Ghidra, it became hard to tell what this code was doing.

The result decompiled in IDA looked like this.

if ( v21 + v24 == 0x8B228BF35F6ALL
&& v24 + v26 == 15172161
&& v28 + v26 == 4199291551LL
&& v28 + v21 == 0x8B238557F7C8LL )
{
    return (v28 ^ v24 ^ v26) == 4184371021LL;
}

Using these as constraints, I created the following solver.

from z3 import *

s = Solver()
i1 = BitVec('i1', 64)
i2 = BitVec('i2', 64)
i3 = BitVec('i3', 64)
i4 = BitVec('i4', 64)

s.add(i1 + i2 == 0x8b228bf35f6a)
s.add(i2 + i3 == 0xe78241)
s.add(i4 + i3 == 0xfa4c1a9f)
s.add(i4 + i1 == 0x8B238557F7C8)
s.add(i4 ^ i2 ^ i3 == 0xf9686f4d)

if s.check() == sat:
    model = s.model()
    parts = [f"{model[key].as_long():x}"for key in [i1, i2, i3, i4]]
    flag = "SECCON{" + "-".join(parts) + "}"
    print(flag)
else:
    print("unsat")

Running the script above yields the correct flag and also passes the program’s verification.

image-20230820122416629

Devil Hunter(Rev)

Clam Devil; Asari no Akuma

The challenge provides the following shell script and CBC file.

#!/bin/sh
if [ -z "$1" ]
then
    echo "[+] ${0} <flag.txt>"
    exit 1
else
    clamscan --bytecode-unsigned=yes --quiet -dflag.cbc "$1"
    if [ $? -eq 1 ]
    then
        echo "Correct!"
    else
        echo "Wrong..."
    fi
fi
ClamBCafhaio`lfcf|aa```c``a```|ah`cnbac`cecnb`c``beaacp`clamcoincidencejb:4096
Seccon.Reversing.{FLAG};Engine:56-255,Target:0;0;0:534543434f4e7b
Teddaaahdabahdacahdadahdaeahdafahdagahebdeebaddbdbahebndebceaacb`bbadb`baacb`bb`bb`bdaib`bdbfaah
Eaeacabbae|aebgefafdf``adbbe|aecgefefkf``aebae|amcgefdgfgifbgegcgnfafmfef``
G`ad`@`bdeBceBefBcfBcfBofBnfBnbBbeBefBfgBefBbgBcgBifBnfBgfBnbBfdBldBadBgd@`bad@Aa`bad@Aa`
A`b`bLabaa`b`b`Faeac
Baa``b`abTaa`aaab
Bb`baaabbaeAc`BeadTbaab
BTcab`b@dE
A`aaLbhfb`dab`dab`daahabndabad`bndabad`b`b`aa`b`d`b`d`b`d`b`b`bad`bad`b`b`aa`b`d`b`b`aa`ah`aa`aa`b`b`aa`b`d`b`d`b`d`b`b`bad`bad`b`b`b`b`b`d`b`d`b`b`b`b`bad`b`b`bad`b`d`aa`b`b`aa`b`b`bad`b`b`bad`b`b`aa`aa`b`b`bad`b`b`bad`b`b`aa`aa`b`b`bad`b`b`bad`b`b`aa`aa`b`b`bad`b`b`bad`b`b`aa`aa`b`b`bad`b`b`bad`b`b`aa`aa`b`b`bad`b`b`bad`b`b`aa`aa`b`b`bad`b`b`bad`b`b`aa`aa`b`b`bad`b`b`bad`b`b`aa`aa`b`d`b`d`aa`Fbcgah
Bbadaedbbodad@dbadagdbbodaf@db`bahabbadAgd@db`d`bb@habTbaab
Baaaiiab`dbbaBdbhb`d`bbbbaabTaaaiabac
Bb`dajbbabajb`dakh`ajB`bhb`dalj`akB`bhb`bamn`albadandbbodad@dbadaocbbadanamb`bb`aabbabaoAadaabaanab`bb`aAadb`dbbaa`ajAahb`d`bb@h`Taabaaagaa
Bb`bbcaabbabacAadaabdakab`bbca@dahbeabbacbeaaabfaeaahbeaBmgaaabgak`bdabfab`d`bb@h`Taabgaadag
Bb`bbhaabbabacAadaabiakab`bbha@db`d`bb@haab`d`bb@h`Taabiaagae
Bb`dbjabbaabjab`dbkah`bjaB`bhb`dblaj`bkaB`bhb`bbman`blabadbnadbbodad@dbadboacbbadbnabmab`bb`bgbboab`bbab`baacb`bb`dbbbh`bjaBnahb`dbcbj`bbbB`bhb`bbdbn`bcbb`bbebc`Add@dbadbfbcbbadagbebb`bbgbc`Addbdbbadbhbcbbadbfbbgbb`b`fbbabbhbb`dbiba`bjaAdhaabjbiab`dbibBdbhb`d`bbbibaaTaabjbaeaf
Bb`bbkbgbagaablbeab`bbkbHbj`hnicgdb`bbmbc`Add@dbadbnbcbbadagbmbb`bbobc`AddAadbadb`ccbbadbnbbobb`bbacgbb`caabbceab`bbacHcj`hnjjcdaabcck`blbbbcb`bbdcc`Add@dbadbeccbbadagbdcb`bbfcc`AddAbdbadbgccbbadbecbfcb`bbhcgbbgcaabiceab`bbhcHoigndjkcdaabjck`bccbicb`bbkcc`Add@dbadblccbbadagbkcb`bbmcc`AddAcdbadbnccbbadblcbmcb`bbocgbbncaab`deab`bbocHcoaljkhgdaabadk`bjcb`db`bbbdc`Add@dbadbcdcbbadagbbdb`bbddc`AddAddbadbedcbbadbcdbddb`bbfdgbbedaabgdeab`bbfdHcoalionedaabhdk`badbgdb`bbidc`Add@dbadbjdcbbadagbidb`bbkdc`AddAedbadbldcbbadbjdbkdb`bbmdgbbldaabndeab`bbmdHoilnikkcdaabodk`bhdbndb`bb`ec`Add@dbadbaecbbadagb`eb`bbbec`AddAfdbadbcecbbadbaebbeb`bbdegbbceaabeeeab`bbdeHdochfheedaabfek`bodbeeb`bbgec`Add@dbadbhecbbadagbgeb`bbiec`AddAgdbadbjecbbadbhebieb`bbkegbbjeaableeab`bbkeHdiemjoeedaabmek`bfebleb`bbnec`Add@dbadboecbbadagbneb`bb`fc`AddAhdbadbafcbbadboeb`fb`bbbfgbbafaabcfeab`bbbfHoimmoklfdaabdfk`bmebcfb`dbefo`bdfb`d`bbbef`Tbaag
Bb`dbffbb`bffaabgfn`bffTcaaabgfE
Aab`bLbaab`b`b`dab`dab`d`b`d`b`b`b`b`b`b`b`b`b`b`b`b`b`b`b`b`b`b`b`b`aa`b`d`b`d`Fbfaac
Bb`d`bb@habb`d`bbG`lckjljhaaTbaaa
Bb`dacbbaaacb`dadbbabadb`baen`acb`bafn`adb`bagh`afAcdb`bahi``agb`baik`ahBoodb`bajm`aiaeb`bakh`ajAhdb`bali`aeBhadb`baml`akalb`bana`afAadaaaoeab`banAddb`db`ao`anb`dbaao`amb`d`bbb`aabb`d`bbbaaaaTaaaoabaa
BTcab`bamE
Snfofdg`bcgof`befafcgig`bjc`ej`

The CBC file appears to be a compiled ClamAV custom detection signature.

Reference: Bytecode Signatures - ClamAV Documentation

This suggests that the flag should be a file that satisfies the detection condition when running clamscan --bytecode-unsigned=yes --quiet -dflag.cbc flag.txt.

For CBC files, it seems that the clambc command can use the --printsrc and --printbcir options to obtain the original source or the disassembled intermediate representation.

Reference: clamav-bytecode-compiler/docs/user/clambc-user.pdf at main · Cisco-Talos/clamav-bytecode-compiler · GitHub

In this challenge, the --printsrc option did not work properly, so I used --printbcir to obtain the disassembly.

clambc --printbcir flag.cbc > decomp.txt

I did not fully understand how to read it at first, but it seems that each function is assigned a Function id and the output lists opcodes together with operands.

########################################################################
####################### Function id   0 ################################
########################################################################
found a total of 4 globals
GID  ID    VALUE
------------------------------------------------------------------------
  0 [  0]: i0 unknown
  1 [  1]: [22 x i8] unknown
  2 [  2]: i8* unknown
  3 [  3]: i8* unknown
------------------------------------------------------------------------
found 2 values with 0 arguments and 2 locals
VID  ID    VALUE
------------------------------------------------------------------------
  0 [  0]: i1
  1 [  1]: i32
------------------------------------------------------------------------
found a total of 2 constants
CID  ID    VALUE
------------------------------------------------------------------------
  0 [  2]: 21(0x15)
  1 [  3]: 0(0x0)
------------------------------------------------------------------------
found a total of 4 total values
------------------------------------------------------------------------
FUNCTION ID: F.0 -> NUMINSTS 5
BB   IDX  OPCODE              [ID /IID/MOD]  INST
------------------------------------------------------------------------
  0    0  OP_BC_CALL_DIRECT   [32 /160/  0]  0 = call F.1 ()
  0    1  OP_BC_BRANCH        [17 / 85/  0]  br 0 ? bb.1 : bb.2

  1    2  OP_BC_CALL_API      [33 /168/  3]  1 = setvirusname[4] (p.-2147483645, 2)
  1    3  OP_BC_JMP           [18 / 90/  0]  jmp bb.2

  2    4  OP_BC_RET           [19 / 98/  3]  ret 3
------------------------------------------------------------------------
########################################################################

This is more of an educated guess, but it seems to first call call F.1 () and then branch with br 0 ? bb.1 : bb.2 depending on the result.

bb probably corresponds to the number in the leftmost column. If execution jumps to bb.1, it reaches 1 = setvirusname[4] (p.-2147483645, 2), which seems to register the file’s detection information.

So it looks like we want the return value of F.1 to be 0.

Next, let us look at function F.1.

It is fairly long—about 100 lines—so I thought I might be able to brute-force my way through it, but I could not really understand how to read it.

------------------------------------------------------------------------
FUNCTION ID: F.1 -> NUMINSTS 115
BB   IDX  OPCODE              [ID /IID/MOD]  INST
------------------------------------------------------------------------
  0    0  OP_BC_GEPZ          [36 /184/  4]  5 = gepz p.4 + (104)
  0    1  OP_BC_GEPZ          [36 /184/  4]  7 = gepz p.6 + (105)
  0    2  OP_BC_CALL_API      [33 /168/  3]  8 = seek[3] (106, 107)
  0    3  OP_BC_COPY          [34 /174/  4]  cp 108 -> 2
  0    4  OP_BC_JMP           [18 / 90/  0]  jmp bb.2

  1    5  OP_BC_ICMP_ULT      [25 /129/  4]  9 = (18 < 109)
  1    6  OP_BC_COPY          [34 /174/  4]  cp 18 -> 2
  1    7  OP_BC_BRANCH        [17 / 85/  0]  br 9 ? bb.2 : bb.3

  2    8  OP_BC_COPY          [34 /174/  4]  cp 2 -> 10
  2    9  OP_BC_SHL           [8  / 44/  4]  11 = 10 << 110
  2   10  OP_BC_ASHR          [10 / 54/  4]  12 = 11 >> 111
  2   11  OP_BC_TRUNC         [14 / 73/  3]  13 = 12 trunc ffffffffffffffff
  2   12  OP_BC_GEPZ          [36 /184/  4]  14 = gepz p.4 + (112)
  2   13  OP_BC_GEP1          [35 /179/  4]  15 = gep1 p.14 + (13 * 65)
  2   14  OP_BC_CALL_API      [33 /168/  3]  16 = read[1] (p.15, 113)
  2   15  OP_BC_ICMP_SLT      [30 /153/  3]  17 = (16 < 114)
  2   16  OP_BC_ADD           [1  /  9/  0]  18 = 10 + 115
  2   17  OP_BC_COPY          [34 /174/  4]  cp 116 -> 0
  2   18  OP_BC_BRANCH        [17 / 85/  0]  br 17 ? bb.7 : bb.1

  3   19  OP_BC_CALL_API      [33 /168/  3]  19 = read[1] (p.3, 117)
  3   20  OP_BC_ICMP_SGT      [27 /138/  3]  20 = (19 > 118)
  3   21  OP_BC_COPY          [34 /171/  1]  cp 3 -> 21
  3   22  OP_BC_ICMP_EQ       [21 /106/  1]  22 = (21 == 119)
  3   23  OP_BC_AND           [11 / 55/  0]  23 = 20 & 22
  3   24  OP_BC_COPY          [34 /174/  4]  cp 120 -> 0
  3   25  OP_BC_BRANCH        [17 / 85/  0]  br 23 ? bb.4 : bb.7

  4   26  OP_BC_CALL_API      [33 /168/  3]  24 = read[1] (p.3, 121)
  4   27  OP_BC_ICMP_SGT      [27 /138/  3]  25 = (24 > 122)
  4   28  OP_BC_COPY          [34 /174/  4]  cp 123 -> 1
  4   29  OP_BC_COPY          [34 /174/  4]  cp 124 -> 0
  4   30  OP_BC_BRANCH        [17 / 85/  0]  br 25 ? bb.7 : bb.5

  5   31  OP_BC_COPY          [34 /174/  4]  cp 1 -> 26
  5   32  OP_BC_SHL           [8  / 44/  4]  27 = 26 << 125
  5   33  OP_BC_ASHR          [10 / 54/  4]  28 = 27 >> 126
  5   34  OP_BC_TRUNC         [14 / 73/  3]  29 = 28 trunc ffffffffffffffff
  5   35  OP_BC_GEPZ          [36 /184/  4]  30 = gepz p.4 + (127)
  5   36  OP_BC_GEP1          [35 /179/  4]  31 = gep1 p.30 + (29 * 65)
  5   37  OP_BC_LOAD          [39 /198/  3]  load  32 <- p.31
  5   38  OP_BC_CALL_DIRECT   [32 /163/  3]  33 = call F.2 (32)
  5   39  OP_BC_SHL           [8  / 44/  4]  34 = 26 << 128
  5   40  OP_BC_ASHR          [10 / 54/  4]  35 = 34 >> 129
  5   41  OP_BC_TRUNC         [14 / 73/  3]  36 = 35 trunc ffffffffffffffff
  5   42  OP_BC_MUL           [3  / 18/  0]  37 = 130 * 131
  5   43  OP_BC_GEP1          [35 /179/  4]  38 = gep1 p.7 + (37 * 65)
  5   44  OP_BC_MUL           [3  / 18/  0]  39 = 132 * 36
  5   45  OP_BC_GEP1          [35 /179/  4]  40 = gep1 p.38 + (39 * 65)
  5   46  OP_BC_STORE         [38 /193/  3]  store 33 -> p.40
  5   47  OP_BC_ADD           [1  /  9/  0]  41 = 26 + 133
  5   48  OP_BC_ICMP_ULT      [25 /129/  4]  42 = (41 < 134)
  5   49  OP_BC_COPY          [34 /174/  4]  cp 41 -> 1
  5   50  OP_BC_BRANCH        [17 / 85/  0]  br 42 ? bb.5 : bb.6

  6   51  OP_BC_LOAD          [39 /198/  3]  load  43 <- p.7
  6   52  OP_BC_ICMP_EQ       [21 /108/  3]  44 = (43 == 135)
  6   53  OP_BC_MUL           [3  / 18/  0]  45 = 136 * 137
  6   54  OP_BC_GEP1          [35 /179/  4]  46 = gep1 p.7 + (45 * 65)
  6   55  OP_BC_MUL           [3  / 18/  0]  47 = 138 * 139
  6   56  OP_BC_GEP1          [35 /179/  4]  48 = gep1 p.46 + (47 * 65)
  6   57  OP_BC_LOAD          [39 /198/  3]  load  49 <- p.48
  6   58  OP_BC_ICMP_EQ       [21 /108/  3]  50 = (49 == 140)
  6   59  OP_BC_AND           [11 / 55/  0]  51 = 44 & 50
  6   60  OP_BC_MUL           [3  / 18/  0]  52 = 141 * 142
  6   61  OP_BC_GEP1          [35 /179/  4]  53 = gep1 p.7 + (52 * 65)
  6   62  OP_BC_MUL           [3  / 18/  0]  54 = 143 * 144
  6   63  OP_BC_GEP1          [35 /179/  4]  55 = gep1 p.53 + (54 * 65)
  6   64  OP_BC_LOAD          [39 /198/  3]  load  56 <- p.55
  6   65  OP_BC_ICMP_EQ       [21 /108/  3]  57 = (56 == 145)
  6   66  OP_BC_AND           [11 / 55/  0]  58 = 51 & 57
  6   67  OP_BC_MUL           [3  / 18/  0]  59 = 146 * 147
  6   68  OP_BC_GEP1          [35 /179/  4]  60 = gep1 p.7 + (59 * 65)
  6   69  OP_BC_MUL           [3  / 18/  0]  61 = 148 * 149
  6   70  OP_BC_GEP1          [35 /179/  4]  62 = gep1 p.60 + (61 * 65)
  6   71  OP_BC_LOAD          [39 /198/  3]  load  63 <- p.62
  6   72  OP_BC_ICMP_EQ       [21 /108/  3]  64 = (63 == 150)
  6   73  OP_BC_AND           [11 / 55/  0]  65 = 58 & 64
  6   74  OP_BC_MUL           [3  / 18/  0]  66 = 151 * 152
  6   75  OP_BC_GEP1          [35 /179/  4]  67 = gep1 p.7 + (66 * 65)
  6   76  OP_BC_MUL           [3  / 18/  0]  68 = 153 * 154
  6   77  OP_BC_GEP1          [35 /179/  4]  69 = gep1 p.67 + (68 * 65)
  6   78  OP_BC_LOAD          [39 /198/  3]  load  70 <- p.69
  6   79  OP_BC_ICMP_EQ       [21 /108/  3]  71 = (70 == 155)
  6   80  OP_BC_AND           [11 / 55/  0]  72 = 65 & 71
  6   81  OP_BC_MUL           [3  / 18/  0]  73 = 156 * 157
  6   82  OP_BC_GEP1          [35 /179/  4]  74 = gep1 p.7 + (73 * 65)
  6   83  OP_BC_MUL           [3  / 18/  0]  75 = 158 * 159
  6   84  OP_BC_GEP1          [35 /179/  4]  76 = gep1 p.74 + (75 * 65)
  6   85  OP_BC_LOAD          [39 /198/  3]  load  77 <- p.76
  6   86  OP_BC_ICMP_EQ       [21 /108/  3]  78 = (77 == 160)
  6   87  OP_BC_AND           [11 / 55/  0]  79 = 72 & 78
  6   88  OP_BC_MUL           [3  / 18/  0]  80 = 161 * 162
  6   89  OP_BC_GEP1          [35 /179/  4]  81 = gep1 p.7 + (80 * 65)
  6   90  OP_BC_MUL           [3  / 18/  0]  82 = 163 * 164
  6   91  OP_BC_GEP1          [35 /179/  4]  83 = gep1 p.81 + (82 * 65)
  6   92  OP_BC_LOAD          [39 /198/  3]  load  84 <- p.83
  6   93  OP_BC_ICMP_EQ       [21 /108/  3]  85 = (84 == 165)
  6   94  OP_BC_AND           [11 / 55/  0]  86 = 79 & 85
  6   95  OP_BC_MUL           [3  / 18/  0]  87 = 166 * 167
  6   96  OP_BC_GEP1          [35 /179/  4]  88 = gep1 p.7 + (87 * 65)
  6   97  OP_BC_MUL           [3  / 18/  0]  89 = 168 * 169
  6   98  OP_BC_GEP1          [35 /179/  4]  90 = gep1 p.88 + (89 * 65)
  6   99  OP_BC_LOAD          [39 /198/  3]  load  91 <- p.90
  6  100  OP_BC_ICMP_EQ       [21 /108/  3]  92 = (91 == 170)
  6  101  OP_BC_AND           [11 / 55/  0]  93 = 86 & 92
  6  102  OP_BC_MUL           [3  / 18/  0]  94 = 171 * 172
  6  103  OP_BC_GEP1          [35 /179/  4]  95 = gep1 p.7 + (94 * 65)
  6  104  OP_BC_MUL           [3  / 18/  0]  96 = 173 * 174
  6  105  OP_BC_GEP1          [35 /179/  4]  97 = gep1 p.95 + (96 * 65)
  6  106  OP_BC_LOAD          [39 /198/  3]  load  98 <- p.97
  6  107  OP_BC_ICMP_EQ       [21 /108/  3]  99 = (98 == 175)
  6  108  OP_BC_AND           [11 / 55/  0]  100 = 93 & 99
  6  109  OP_BC_SEXT          [15 / 79/  4]  101 = 100 sext 1
  6  110  OP_BC_COPY          [34 /174/  4]  cp 101 -> 0
  6  111  OP_BC_JMP           [18 / 90/  0]  jmp bb.7

  7  112  OP_BC_COPY          [34 /174/  4]  cp 0 -> 102
  7  113  OP_BC_TRUNC         [14 / 70/  0]  103 = 102 trunc ffffffffffffffff
  7  114  OP_BC_RET           [19 / 95/  0]  ret 103
------------------------------------------------------------------------

Referring to the official write-up, it seems that the following solver can recover the flag, so I used it as a clue while trying to understand the logic.

from z3 import *

hashlist = [
    0x739e80a2, 0x3aae80a3, 0x3ba4e79f, 0x78bac1f3,
    0x5ef9c1f3, 0x3bb9ec9f, 0x558683f4, 0x55fad594,
    0x6cbfdd9f
]

flag = [BitVec(f'flag_{i}', 32) for i in range(len(hashlist))]

s = Solver()
for hashvalue, block in zip(hashlist, flag):
    h = 0xacab3c0
    for i in range(4):
        h = RotateLeft(h ^ ((block >> (i*8)) & 0xff), 8)
    s.add(h == hashvalue)

r = s.check()
if r != sat:
    print(r)
    exit(1)

m = s.model()
s_flag = b""
for block in flag:
    s_flag += int.to_bytes(m[block].as_long(), 4, 'little')
print("SECCON{" + s_flag.decode() + "}")

DoroboH(Rev)

I found a suspicious process named “araiguma.exe” running on my computer. Before removing it, I captured my network and dumped the process memory. Could you investigate what the malware is doing?

doroboh

The program is a malware. Do not run it unless you understand its behavior.

TODO

eldercmp(Rev)

baby_mode = 0 👴

chall.elder

  • The program is tested on Ubuntu 20.04 and 22.04 LTS (host machine). It might not run correctly on the other platforms such as WSL.

TODO