SECCON 予選の Rev 問を少しずつ解いていきます。
他の問題は後日追記予定です。
問題や Writeup は以下の公式リポジトリを参照しています。
参考:GitHub - SECCON/SECCON2022onlineCTF
もくじ
baby_cmp(Rev)
baby_mode = 1 👶
問題バイナリを Ghidra でデコンパイルしたところ、シンプルなパスワード検証を行うプログラムでした。
テンプレの angr スクリプトで Flag を取得できました。
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)
# 出力
simgr.found[0].solver.eval(flag, cast_to=bytes)
# SECCON{y0u_f0und_7h3_baby_flag_YaY}
eguite(Rev)
Crack me.
バイナリを実行してみると、以下のようなライセンス検証を行う GUI プログラムであることがわかります。
とりあえず Ghidra でデコンパイルしていきます。
GUI プログラムなので実際の検証処理を探すのが少々手間ですが、Invalid license
の文字列検索を行うことで処理を特定することができました。
以下の箇所の onClick 関数が Check ボタンを押した際に呼び出される関数のようです。
とりあえず Ghidra のデコンパイル結果を貼っておきます。
入力値に対してかなり複雑な検証処理を行っていることがわかります。
/* 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 );
}
このパターンの場合、Z3Py で制約を指定して解くことで Flag を取得できると思いますが、中々適切なスクリプトを書けません。
このような場合は、処理を地道に読み解いていくのがよいですね。
まず、以下の箇所ですが、GDB を利用しながら調べた結果、rdi + 0x90
には入力した文字列の長さが格納されていました。
つまり、Flag の文字列の長さは 0x2b であると言えます。
if (*(long *)(rdi + 0x90) != 0x2b) {
return false;
}
次に以下の箇所です。
rdi+0x80
には入力した文字列が格納されていました。
input = *(uint **)(rdi + 0x80);
if ((*(uint *)((long)input + 3) ^ 0x7b4e4f43 | *input ^ 0x43434553) != 0) {
return false;
}
if (*(byte *)((long)input + 0x2a) != 0x7d) {
return false;
}
ここでは、入力した文字列の先頭と末尾をそれぞれハードコードされた値と比較しているようです。
この値は以下の通り Flag のプレフィックスに該当するため、ここでは Flag のフォーマットを検証していることがわかります。
続いて、少し長いですが以下の箇所を見ていきます。
input + 0x2b
以降の領域は今のところ特に何の値も入っていないはずなので、buf と rename しました。
do - while 文の中では、0x13 が代入された i をデクリメントする中で、色々な処理を行うようです。
ただ、よく見ると入力した文字列のバイト値は常に 0 より大きくなるので、if ((char)c < 0x0)
内の処理は行わず、ただ単に input_ptr を 0x13 回インクリメントするだけでループが終了することがわかります。
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);
さらに処理を追いかけていきます。
続く以下の行では、インデックスを 0x13 進めた inputptr の 1 文字を抜き出して ix13 に格納した上で比較を行っていますが、こちらも 0 未満になることはないため、実質的に i_x13 が 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;
}
さらに以降の処理を追います。
ここでは、input_ptr のアドレスを入力文字の先頭のインデックスに戻しています。
do-while 文の中では色々な処理が定義されていますが、ここでも入力文字は 0 未満になることはないので、単純にインデックスを 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);
そろそろパターンが見えてきました。
今までの処理も含めて、無視していい行を削除すると以下のようなコードが見えてきます。
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;
}
つまり、ここまでの 176 行のコードは、単に Flag 文字列の Prefix と特定の位置の文字が -
であるかを検証しているだけのコードであることがわかります。
ここまで読めたところで、後半のコードを見ていきます。
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 のデコンパイル結果ではうまく引数を拾えていないようでしたが、from_iter 関数を呼び出しています。
実際に GDB で正しい挙動を確認してみたところ、インデックス 7 から 12 文字分の文字列を抜き出したコレクションを作成するような挙動になっていました。
つまり、SECCON{
から 1 つめの -
までの間の文字列を抜き出しているというわけです。
そして、続けて fromstrradix を使用して文字列を数値変換しています。
参考:u64 - Rust
こうなると、その後のパターンも同一であるという予測が立ちます。
実際に以下のデコンパイル結果を並べてみてみると、SECCON{ABCDEFGHIJKL-NOPQRS-UVWXYZ-bcdefghi}
というフォーマットの Flag のハイフンで区切られた区間の文字列をそれぞれ取得して数値変換しているということがわかります。
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);
}
つまり、これだけ複雑そうに見えるコードを使用しながらも、実際に Flag の検証を行っている箇所はほとんど以下の箇所のみとなるわけです。
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;
}
しかし、上記については Ghidra 上で変数のリネームを雑に進めてしまったせいでどういう処理なのかわかりづらくなってしまいました。
IDA でデコンパイルした結果は以下の通りでした。
if ( v21 + v24 == 0x8B228BF35F6ALL
&& v24 + v26 == 15172161
&& v28 + v26 == 4199291551LL
&& v28 + v21 == 0x8B238557F7C8LL )
{
return (v28 ^ v24 ^ v26) == 4184371021LL;
}
これを制約として以下の 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")
上記のスクリプトを実行すると正しい Flag を取得でき、プログラムの検証にも成功します。
Devil Hunter(Rev)
Clam Devil; Asari no Akuma
問題バイナリとして以下のシェルスクリプトと CBC ファイルが与えられます。
#!/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`
CBC ファイルは、ClamAV のカスタム検出シグネチャをコンパイルしたもののようです。
参考:Bytecode Signatures - ClamAV Documentation
clamscan --bytecode-unsigned=yes --quiet -dflag.cbc flag.txt
のコマンド実行時に検出条件を満たすファイルが Flag になりそうだということがわかります。
ここで、CBC ファイルについては clambc コマンドの --printsrc
や --printbcir
オプションで元のソースや中間言語のディスアセンブル結果を取得できるようです。
今回のバイナリでは --printsrc
オプションは有効に動作しなかったため、--printbcir
オプションでディスアセンブル結果を取得しました。
clambc --printbcir flag.cbc > decomp.txt
読み方がいまいちよくわかりませんでしたが、どうも関数ごとに Function id が割り当てられ、オペコードとオペランドがセットで出力されるようです。
########################################################################
####################### 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
------------------------------------------------------------------------
########################################################################
勘で読んでいく感じですが、まず初めに call F.1 ()
を呼び出し、その結果に応じて br 0 ? bb.1 : bb.2
で分岐をかけているように見えます。
bb というのは恐らく一番左の列の番号と対応しており、bb.1
にジャンプした場合は 1 = setvirusname[4] (p.-2147483645, 2)
に到達し、ファイルの検出情報が登録されるようです。
つまり、F.1
関数の戻り値が 0 になればよさそうです。
続いて F.1
関数を見てみます。
かなり長いですが、100 行ほどなので頑張れば読める、、かもしれないと思ったものの、読み方がさっぱりわかりませんでした。
------------------------------------------------------------------------
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
------------------------------------------------------------------------
公式の Writeup を参考にすると、以下の Solver で Flag を取得できるようでしたので、これを参考に何とか読み解いていこうと思います。
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