初めに
どうも、クソ雑魚のなんちゃてエンジニアです。
本記事は 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();
Actually Baby Rev
Revのベビー問かな??
1つの実行ファイルchal
が渡されます。実行します。
何か入力を促されますが、Babyはそれが嫌いなようです。
Ghidraに食わせます。
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_3c
を127
にすれば勝ちです。
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の回数
スタックの構成上、29文字までしか受け付けないので5のコマンドを多めにとって55555555555555555555555999
を打ち込めばOK。
これで続いての項目が出現します。
次は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は+6
2は+13
とわかります。
以下を解けば勝ちですね。
(6n+13m) mod 3 = 1 and
(6n+13m) mod 4 = 3 and
(6n+13m) mod 5 = 1 #nは4の回数、mは2の回数。
スタックの文字数制限から4442
しか受け付けてくれません。
最後に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のコマンドがあります。
これらのコマンドの関数を解析すると、
8コマンドは3を生成
0コマンドは3の場合に7をかける
6コマンドは21の場合に5をかける
7コマンドは105の場合に11をかける
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())
Tarpit
chalの実行ファイルが渡される。
フラグ判定ファイルみたいだ。Ghidraに食わせる。
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_00104040
とDAT_00104044
を取ってきて、そっから5バイト分取り出したりシフトしたりインクリメントやデクリメントしてナンチャラカンチャラしてフラグ判定してそうですね。
うん、何もわからん。
こういう時は動的にデバックしてみるに限る。その前にこのDATの出力を追っておく(レジストリ追ったときにわかりやすいので)
0x2007104とか0x3007104とか連続したバイナリが入ってますね。
まぁ動かしてみます。GDBを使います。フラグは判定用にmaple{flag}
を入力します。
RAXにDAT_00104040
が入ってきているのが分かります。
RDXには5バイト分取り出す命令の数字5バイトが来ます。これでDAT_00104044
を取得できます。
取得した値はRDXに入ります。
RAXにはシフトした値が入っています。1
ですね。これが条件分岐の判定の値になりcmp eax
で比較されます。
その後、最初に代入したm
の文字はl
にsub rdx 01
でデクリメントされます。
次に5*2
のバイト0xa
分のDATを引っ張り出します。
2週目のDATがRDXに入ってきます。
条件分岐はまた1に飛びます。
l
はsub rdx 01
でデクリメントされk
になりました。
次の週もデクリメントされます。
0x70
週目で0までシフトされた文字を消しに1の分岐へ飛びますが、デクリメントされません。
これは恐らく以下のコードの分岐でelseになっているからでしょう。
if (*(&s + (rax_8 << 3)) != 0)
次に2の分岐で何か保存してます、次の精査DATのポイントを指定しているのでしょう。
0の分岐の後(インクリメント?)に次の文字の精査に入ります。
何となくわかりました。
このDAT_00104044
~DAT_001081a0
のバイト(ほぼバイナリ全部JAN)でデクリメントの数(何回デクリメントできる文字なのか)によって文字を判定しているなと考察できます。
ここら辺はエスパーしつつソルバーを組み立てます。
DAT_00104044
~DAT_001081a0
のバイトを抽出してPythonでコードを書きます。
実行します。
ん?うまくいかない。
よくよく見てみると、各文字で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))
まとめ
難しかったです。
Reversingは根性ですね。
楽しかったです。ありがとうございました。