0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【MapleCTF2023】Rev - Writeup

Last updated at Posted at 2023-10-02

初めに

どうも、クソ雑魚のなんちゃてエンジニアです。
本記事は MapleCTF2023 のRev問3題のWrtiteupとなります。

3題だけ解けたので記載します。

JaVieScript

以下のJavascript(とHTMLがあったがHTMLはあまり関係ない。)が渡されます。

var flag = "maple{";
var honk = {};

async function hash(string) {
	const utf8 = new TextEncoder().encode(string);
	const hashBuffer = await crypto.subtle.digest('SHA-256', utf8);
	const hashArray = Array.from(new Uint8Array(hashBuffer));
	const hashHex = hashArray
	  .map((bytes) => bytes.toString(16).padStart(2, '0'))
	  .join('');
	return hashHex;
  }

async function constructflag() {
	const urlParams = new URLSearchParams(window.location.search);
	var fleg = "maple{";
	for (const pair of urlParams.entries()) {
		honk[pair[0]] = JSON.parse(pair[1]); 
	}

	if (honk.toString() === {}.toString()) {
		fleg += honk.toString()[9];
	}

	if (Object.keys(honk).length === 0) {
		const test = eval(honk.one);
		if (typeof test === 'number' && test * test + '' == test + '' && !/\d/.test(test)) {
			fleg += 'a' + test.toString()[0];
		}

		const quack = honk.two;

		if (quack.toString().length === 0 & quack.length === 1) {
			fleg += 'a' + (quack[0] + '')[0].repeat(4) + 'as';
		}

		const hiss = honk.three;

		if (hiss === "_are_a_mId_FruiT}") {
			fleg += hiss;
		}
	}
	if (await hash(fleg) == "bfe06d1e92942a0eca51881a879a0a9aef3fe75acaece04877eb0a26ceb8710d") {
		console.log(fleg);
	}
}

constructflag();

フラグを作成する挙動とそれがあっているかハッシュで突合する機能があります。
確認するとmaple{?a?a????as_are_a_mId_FruiT}のようなフラグを作るようです。
後半の????は4つの同じ文字のリピートなので実質3変数です。
ハッシュ復号の解法よりはBruteForceでしょう。
0xffまでの255までで以下の脳筋コードを書きます。

var flag = "maple{";
var honk = {};

async function hash(string) {
	const utf8 = new TextEncoder().encode(string);
	const hashBuffer = await crypto.subtle.digest('SHA-256', utf8);
	const hashArray = Array.from(new Uint8Array(hashBuffer));
	const hashHex = hashArray
	  .map((bytes) => bytes.toString(16).padStart(2, '0'))
	  .join('');
	return hashHex;
  }

async function constructflag() {
	const urlParams = new URLSearchParams(window.location.search);
	var fleg = "maple{";
	var fleg_mid = "a";
	var fleglast = "as_are_a_mId_FruiT}";

	for (const pair of urlParams.entries()) {
		honk[pair[0]] = JSON.parse(pair[1]); 
	}

	for (let i=0; i < 255; i++) {
		str1 = String.fromCharCode(i);
		fleg_tmp1 = fleg + str1 + fleg_mid

		for (let j = 0; j < 255; j++) {
			str2 = String.fromCharCode(j);
			fleg_tmp2 = fleg_tmp1 + str2 + fleg_mid

				for (let k = 0; k < 255; k++) {
					str3 = String.fromCharCode(k);
					str3 = str3.repeat(4);
					fleg_true = fleg_tmp2 + str3 + fleglast;

					if (await hash(fleg_true) == "bfe06d1e92942a0eca51881a879a0a9aef3fe75acaece04877eb0a26ceb8710d") {
						console.log(fleg_true);
						break;
					}
				}
		}
		console.log(i);
	}
	
	console.log('finished');
}

constructflag();

後は回すだけで1分かからないうちにフラグが現れます。
スクリーンショット 2023-10-01 130009.png

Actually Baby Rev

Revのベビー問かな??
1つの実行ファイルchalが渡されます。実行します。
image.png
何か入力を促されますが、Babyはそれが嫌いなようです。
Ghidraに食わせます。
image.png

undefined8 FUN_00401996(void)

{
  int iVar1;
  undefined8 uVar2;
  
  FUN_00401953();
  puts("This baby does NOT stop crying. Please help I don\'t know what it wants!");
  puts("Maybe we should try to make it happy");
  printf("> ");
  iVar1 = FUN_00401524();
  if (iVar1 == 1) {
    puts("I don\'t think that the baby wants to play...");
    puts("Try something else, maybe it\'s hungry? ");
    printf("> ");
    iVar1 = FUN_00401632();
    if (iVar1 == 1) {
      puts("Okay, the baby is full. I can probably put it to sleep.");
      printf("> ");
      iVar1 = FUN_004017cd();
      if (iVar1 == 1) {
        printf("oops you forgot to burp the baby, the baby begins to burp: ");
        FUN_004011c6();
        puts(". What a strange choice of a baby\'s first word you wonder...");
        uVar2 = 0;
      }
      else {
        uVar2 = 0xffffffff;
      }
    }
    else {
      uVar2 = 0xffffffff;
    }
  }
  else {
    uVar2 = 0xffffffff;
  }
  return uVar2;
}

mainの関数です。シンボルは消されてますが、エントリーポイントからわかりますね。
まずはFUN_00401524()の関数の返り値で条件分岐してるのでこの関数をみます。

undefined8 FUN_00401524(void)

{
  char *pcVar1;
  undefined8 uVar2;
  size_t sVar3;
  ushort **ppuVar4;
  int local_3c;
  char local_38 [40];
  int local_10;
  int local_c;
  
  pcVar1 = fgets(local_38,0x1e,stdin);
  if (pcVar1 == (char *)0x0) {
    uVar2 = 0xffffffff;
  }
  else {
    local_3c = 0;
    sVar3 = strcspn(local_38,"\n");
    local_38[sVar3] = '\0';
    for (local_c = 0; local_38[local_c] != '\0'; local_c = local_c + 1) {
      ppuVar4 = __ctype_b_loc();
      if (((*ppuVar4)[local_38[local_c]] & 0x800) == 0) {
        puts("The baby did not like that.");
        return 0xffffffff;
      }
      local_10 = local_38[local_c] + -0x30;
      if (local_10 == 5) {
        FUN_00401246(&local_3c);
      }
      else {
        if (local_10 != 9) {
          puts("The baby is now confused and is crying even louder...");
          return 0;
        }
        FUN_0040127d(&local_3c);
      }
    }
    if (local_3c == 0x7f) {
      uVar2 = 1;
    }
    else {
      uVar2 = 0;
    }
  }
  return uVar2;
}

以下の部分を確認すると入力値を1文字ずつ確認して数字かどうか判定しています。& 0x800は数字のビットマスクなので。またASCIIの数字の文字から数値を割り出すのに-48してます。

    for (local_c = 0; local_38[local_c] != '\0'; local_c = local_c + 1) {
      ppuVar4 = __ctype_b_loc();
      if (((*ppuVar4)[local_38[local_c]] & 0x800) == 0) {
        puts("The baby did not like that.");
        return 0xffffffff;
      }
    local_10 = local_38[local_c] + -0x30;

続いて以下の奴。入力数値が5ならFUN_00401246を実施。9ならFUN_0040127dを実施し、それ以外の数字ならBabyが泣いちゃう。5または9の数字の羅列を投入してlocal_3c127にすれば勝ちです。

      if (local_10 == 5) {
        FUN_00401246(&local_3c);
      }
      else {
        if (local_10 != 9) {
          puts("The baby is now confused and is crying even louder...");
          return 0;
        }
        FUN_0040127d(&local_3c);
      }
    }
    if (local_3c == 0x7f) {
      uVar2 = 1;
    }
    else {
      uVar2 = 0;
    }

FUN_00401246を見ます。

void FUN_00401246(int *param_1)

{
  printf("You started playing baby shark...");
  puts("The baby is hypnotized for a few seconds by the colors and starts crying again.");
  *param_1 = *param_1 + 5;
  return;
}

5のコマンドは+5であることが分かりました。続いてFUN_0040127d

void FUN_0040127d(int *param_1)

{
  int local_14;
  int local_10;
  int local_c;
  
  printf("You hand the baby a cute squish toy in the shape of a cat...");
  puts("The baby flings it accross the room and starts crying again. ");
  *param_1 = *param_1 + 100;
  for (local_c = 0; local_c < 4; local_c = local_c + 1) {
    *param_1 = *param_1 + -4;
  }
  local_10 = 6;
  while (local_10 = local_10 + -1, local_10 != 0) {
    *param_1 = *param_1 + -9;
  }
  *param_1 = *param_1 + -0xf;
  local_14 = 2;
  do {
    *param_1 = *param_1 + -7;
    local_14 = local_14 + -1;
  } while (0 < local_14);
  *param_1 = *param_1 + -1;
  *param_1 = *param_1 + -1;
  *param_1 = *param_1 + -1;
  *param_1 = *param_1 + -1;
  *param_1 = *param_1 + -1;
  *param_1 = *param_1 + -1;
  return;
}

とても面倒なのですが、これしっかり流れを追うと+4となります。
よって以下の整数問題を解けば勝ちです。

5n+4m=127  #nは5の回数、mは9の回数

image.png
スタックの構成上、29文字までしか受け付けないので5のコマンドを多めにとって55555555555555555555555999を打ち込めばOK。
これで続いての項目が出現します。
image.png
次はFUN_00401632の判定に入ります。

undefined8 FUN_00401632(void)

{
  char *pcVar1;
  undefined8 uVar2;
  size_t sVar3;
  ushort **ppuVar4;
  int local_1c;
  char local_16 [6];
  int local_10;
  int local_c;
  
  pcVar1 = fgets(local_16,6,stdin);
  if (pcVar1 == (char *)0x0) {
    uVar2 = 0xffffffff;
  }
  else {
    local_1c = 0;
    sVar3 = strcspn(local_16,"\n");
    local_16[sVar3] = '\0';
    for (local_c = 0; local_16[local_c] != '\0'; local_c = local_c + 1) {
      ppuVar4 = __ctype_b_loc();
      if (((*ppuVar4)[local_16[local_c]] & 0x800) == 0) {
        puts("The baby did not like that.");
        return 0xffffffff;
      }
      local_10 = local_16[local_c] + -0x30;
      if (local_10 == 4) {
        FUN_004013ba(&local_1c);
      }
      else {
        if (4 < local_10) {
LAB_00401726:
          puts("The baby is now confused and is crying even louder...");
          return 0;
        }
        if (local_10 == 1) {
          FUN_00401381();
          return 0;
        }
        if (local_10 != 2) goto LAB_00401726;
        FUN_00401392(&local_1c);
      }
    }
    if (((local_1c % 3 == 1) && (local_1c % 4 == 3)) && (local_1c % 5 == 1)) {
      uVar2 = 1;
    }
    else {
      uVar2 = 0;
    }
  }
  return uVar2;
}

似たようなもんですね。
今度はコマンド4と2です。(1はダミー関数へ、砂糖の取り過ぎには注意ですね)
同じように解析すると4は+62は+13とわかります。
以下を解けば勝ちですね。

(6n+13m) mod 3 = 1 and
(6n+13m) mod 4 = 3 and
(6n+13m) mod 5 = 1       #nは4の回数、mは2の回数。

image.png
スタックの文字数制限から4442しか受け付けてくれません。
image.png
最後にFUN_004017cdを確認します。この条件をクリアすればフラグです。


undefined8 FUN_004017cd(void)

{
  int iVar1;
  char *pcVar2;
  undefined8 uVar3;
  size_t sVar4;
  ushort **ppuVar5;
  int local_1c;
  char local_16 [6];
  int local_10;
  int local_c;
  
  pcVar2 = fgets(local_16,6,stdin);
  if (pcVar2 == (char *)0x0) {
    uVar3 = 0xffffffff;
  }
  else {
    local_1c = 1;
    sVar4 = strcspn(local_16,"\n");
    local_16[sVar4] = '\0';
    for (local_c = 0; local_16[local_c] != '\0'; local_c = local_c + 1) {
      ppuVar5 = __ctype_b_loc();
      if (((*ppuVar5)[local_16[local_c]] & 0x800) == 0) {
        puts("The baby did not like that.");
        return 0xffffffff;
      }
      local_10 = local_16[local_c] + -0x30;
      switch(local_10) {
      case 0:
        iVar1 = FUN_00401418(&local_1c);
        if (iVar1 == 0) {
          return 0;
        }
        break;
      default:
        puts("The baby is now confused and is crying even louder...");
        return 0;
      case 3:
        iVar1 = FUN_004014e4(&local_1c);
        if (iVar1 == 0) {
          return 0;
        }
        break;
      case 6:
        iVar1 = FUN_0040145c(&local_1c);
        if (iVar1 == 0) {
          return 0;
        }
        break;
      case 7:
        iVar1 = FUN_0040149e(&local_1c);
        if (iVar1 == 0) {
          return 0;
        }
        break;
      case 8:
        iVar1 = FUN_004013f1(&local_1c);
        if (iVar1 == 0) {
          return 0;
        }
      }
    }
    if (local_1c == 0x906) {
      uVar3 = 1;
    }
    else {
      uVar3 = 0;
    }
  }
  return uVar3;
}

同じように0,3,6,7,8のコマンドがあります。
これらのコマンドの関数を解析すると、
image.png
8コマンドは3を生成
image.png
0コマンドは3の場合に7をかける
image.png
6コマンドは21の場合に5をかける
image.png
7コマンドは105の場合に11をかける
image.png
3コマンドは1155の場合に2をかける。
これらのコマンドを使って2310にすればいいのでこの順番で実行し80673を入れる。これでフラグゲットだ。
これらのソルバーを書いた。

from pwn import *

context.log_level = "debug"

binfile = './chal'
rhost = 'actually-baby-rev.ctf.maplebacon.org'
rport = 1337

gdb_script = '''
b main
'''

elf = ELF(binfile)
context.binary = elf

def conn():
    if args.REMOTE:
        p = remote(rhost, rport)
    elif args.GDB:
        p = process(elf.path)
        gdb.attach(p, gdbscript=gdb_script)
    else:
        p = process(elf.path)
    return p

p = conn()
p.recvuntil(b'>')
p.sendline(b'55555555555555555555555999')
p.recvuntil(b'>')
p.sendline(b'4442')
p.recvuntil(b'>')
p.sendline(b'80673')

print(p.recvrepeat().decode())

実行するとフラグが帰ってくる。
image.png

Tarpit

chalの実行ファイルが渡される。
image.png
フラグ判定ファイルみたいだ。Ghidraに食わせる。
image.png


undefined8 FUN_00101159(void)

{
  undefined uVar1;
  undefined4 uVar2;
  long lVar3;
  bool bVar4;
  byte bVar5;
  byte bVar6;
  long in_FS_OFFSET;
  byte local_55;
  undefined2 uStack_54;
  undefined uStack_52;
  undefined local_48 [16];
  undefined local_38 [16];
  undefined local_28 [16];
  undefined8 local_18;
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  puts("Input Flag: ");
  local_48 = (undefined  [16])0x0;
  local_38 = (undefined  [16])0x0;
  local_28 = (undefined  [16])0x0;
  local_18 = 0;
  fgets(local_38,0x20,stdin);
  bVar4 = true;
  do {
    while( true ) {
      while( true ) {
        if (!bVar4) {
          if (local_48._8_8_ == 0x1f) {
            puts("Success");
          }
          else {
            puts("Failure");
          }
          if (local_10 == *(long *)(in_FS_OFFSET + 0x28)) {
            return 0;
          }
                    /* WARNING: Subroutine does not return */
          __stack_chk_fail();
        }
        lVar3 = (local_18 & 0xffff) * 5;
        uVar2 = *(undefined4 *)(&DAT_00104040 + lVar3);
        local_55 = (byte)uVar2;
        uStack_54 = (undefined2)((uint)uVar2 >> 8);
        uStack_52 = (undefined)((uint)uVar2 >> 0x18);
        uVar1 = (&DAT_00104044)[lVar3];
        bVar5 = local_55 >> 2;
        bVar6 = local_55 >> 1 & 1;
        if (bVar5 != 2) break;
        *(ulong *)(local_48 + (long)(int)(uint)bVar6 * 8) =
             (ulong)(byte)local_38[*(long *)(local_48 + (long)(int)(uint)(local_55 & 1) * 8)];
        local_18 = CONCAT62(local_18._2_6_,CONCAT11(uVar1,uStack_52));
      }
      if (bVar5 < 3) break;
LAB_001012b6:
      bVar4 = false;
    }
    if (bVar5 == 0) {
      *(long *)(local_48 + (long)(int)(uint)bVar6 * 8) =
           *(long *)(local_48 + (long)(int)(uint)bVar6 * 8) + 1;
      local_18 = CONCAT62(local_18._2_6_,CONCAT11(uVar1,uStack_52));
    }
    else {
      if (bVar5 != 1) goto LAB_001012b6;
      if (*(long *)(local_48 + (long)(int)(uint)bVar6 * 8) == 0) {
        local_18 = CONCAT62(local_18._2_6_,uStack_54);
      }
      else {
        *(long *)(local_48 + (long)(int)(uint)bVar6 * 8) =
             *(long *)(local_48 + (long)(int)(uint)bVar6 * 8) + -1;
        local_18 = CONCAT62(local_18._2_6_,CONCAT11(uVar1,uStack_52));
      }
    }
  } while( true );
}

なんのこっちゃ。
dogboltに食わせてみる。

BinaryNinjaがまだ分かりやすい出力をしてくれた。流石スポンサー。

int32_t main(int32_t argc, char** argv, char** envp)
{
    int32_t argc_1 = argc;
    char** argv_1 = argv;
    void* fsbase;
    int64_t rax = *(fsbase + 0x28);
    puts("Input Flag: ");
    int128_t s;
    __builtin_memset(&s, 0, 0x32);
    int128_t buf;
    fgets(&buf, 0x20, stdin);
    char i = 1;
    while (i != 0)
    {
        int16_t var_18_1;
        uint64_t rdx_1 = var_18_1;
        int32_t rdx_3 = *(&data_4040 + (rdx_1 * 5));
        char var_51_1 = *((rdx_1 * 5) + 0x4044);
        char rax_8 = ((rdx_3 >> 1) & 1);
        uint32_t rax_11 = (rdx_3 >> 2);
        if (rax_11 == 2)
        {
            *(&s + (rax_8 << 3)) = *(&buf + *(&s + ((rdx_3 & 1) << 3)));
            var_18_1 = *rdx_3[3];
        }
        else
        {
            if (rax_11 == 0)
            {
                uint32_t rax_12 = rax_8;
                *(&s + (rax_12 << 3)) = (*(&s + (rax_12 << 3)) + 1);
                var_18_1 = *rdx_3[3];
                continue;
            }
            else if (rax_11 == 1)
            {
                if (*(&s + (rax_8 << 3)) != 0)
                {
                    uint32_t rax_19 = rax_8;
                    *(&s + (rax_19 << 3)) = (*(&s + (rax_19 << 3)) - 1);
                    var_18_1 = *rdx_3[3];
                    continue;
                }
                else
                {
                    var_18_1 = *rdx_3[1];
                    continue;
                }
            }
            i = 0;
        }
    }
    if (*s[8] != 0x1f)
    {
        puts("Failure");
    }
    else
    {
        puts("Success");
    }
    *(fsbase + 0x28);
    if (rax == *(fsbase + 0x28))
    {
        return 0;
    }
    __stack_chk_fail();
    /* no return */
}

なんかDAT_00104040DAT_00104044を取ってきて、そっから5バイト分取り出したりシフトしたりインクリメントやデクリメントしてナンチャラカンチャラしてフラグ判定してそうですね。
うん、何もわからん。
こういう時は動的にデバックしてみるに限る。その前にこのDATの出力を追っておく(レジストリ追ったときにわかりやすいので)
image.png
0x2007104とか0x3007104とか連続したバイナリが入ってますね。
まぁ動かしてみます。GDBを使います。フラグは判定用にmaple{flag}を入力します。
image.png
RAXにDAT_00104040が入ってきているのが分かります。
RDXには5バイト分取り出す命令の数字5バイトが来ます。これでDAT_00104044を取得できます。
取得した値はRDXに入ります。
image.png
RAXにはシフトした値が入っています。1ですね。これが条件分岐の判定の値になりcmp eaxで比較されます。
image.png
その後、最初に代入したmの文字はlsub rdx 01でデクリメントされます。
image.png
次に5*2のバイト0xa分のDATを引っ張り出します。
image.png
2週目のDATがRDXに入ってきます。
image.png
条件分岐はまた1に飛びます。
image.png
lsub rdx 01でデクリメントされkになりました。
image.png
次の週もデクリメントされます。
image.png
0x70週目で0までシフトされた文字を消しに1の分岐へ飛びますが、デクリメントされません。
これは恐らく以下のコードの分岐でelseになっているからでしょう。

if (*(&s + (rax_8 << 3)) != 0)

image.png
次に2の分岐で何か保存してます、次の精査DATのポイントを指定しているのでしょう。
image.png
0の分岐の後(インクリメント?)に次の文字の精査に入ります。

何となくわかりました。
このDAT_00104044DAT_001081a0のバイト(ほぼバイナリ全部JAN)でデクリメントの数(何回デクリメントできる文字なのか)によって文字を判定しているなと考察できます。
ここら辺はエスパーしつつソルバーを組み立てます。

DAT_00104044DAT_001081a0のバイトを抽出してPythonでコードを書きます。
image.png
実行します。
image.png
ん?うまくいかない。
よくよく見てみると、各文字でLoop回数分文字がデクリメントされていることが分かります。
以下に修正コードを記載します。

from pwn import *

raws = [0x04,0x71,0x00,0x02,0x00,0x04,0x71,0x00,0x03,0x00,.........] #自分で埋めてね


num = 0
st = 0
out = 0
flag=[]
while((num+1)*5<=len(raws)):
    tmp = raws[num*5:(num+1)*5]
    rdx_3 = b"".join([p8(x) for x in tmp])
    rax_11 = bytes([(byte >> 2) & 0xFF for byte in rdx_3])[0]
    
    if rax_11 == 1:
        st += 1
    elif rax_11 == 2:
        flag.append(chr(st+out))
        print(chr(st+out))
        out += 1
        st = 0
    elif rax_11 == 0:
        st += -1
    else:
        break
      
    if (num+1)*5> len(raws):
        break
      
    num += 1
      
print("".join(flag))

image.png
実行するとフラグをゲットできます。

まとめ

難しかったです。
Reversingは根性ですね。
楽しかったです。ありがとうございました。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?