writeups by ch4nsec

[Dice CTF 2026] Diceminer

Dice CTF 2026에서 출제된 Diceminer 문제 풀이

#ctf#web#DiceCTF#javascript

문제 개요

dicectf_diceminer1

채굴하여 1,000,000DC를 벌어야 한다. 250번의 횟수 제한이 존재한다.

핵심 코드

app.post('/api/dig', requireAuth, requireStarted, (req, res) => {
  const user = req.user;
  const { direction } = req.body;

  if (!['up', 'down', 'left', 'right'].includes(direction)) {
    return res.status(400).json({ error: 'invalid direction' });
  }

  const pickaxe = PICKAXES[user.pickaxe];

  if (user.energy < 1) {
    return res.status(400).json({ error: 'no energy left' });
  }

  let dx = 0, dy = 0;
  if (direction === 'left') dx = -1;
  if (direction === 'right') dx = 1;
  if (direction === 'down') dy = -1;
  if (direction === 'up') dy = 1;

  let mined = {};
  let earnings = 0;
  let cx = user.x;
  let cy = user.y;
  let remaining = pickaxe.range;

  while (remaining > 0) {
    cx += dx;
    cy += dy;

    if (cy > 0) break;

    const key = cx + ',' + cy;

    if (user.mined[key]) {
      remaining--;
      continue;
    }

    const blockType = getBlockType(cx, cy);
    const ore = ORES[blockType];
    if (!ore) break;

    if (ore.tier > pickaxe.tier) break;

    mined[key] = true;
    earnings += ore.reward;

    remaining--;
  }

  const blocks = Object.keys(mined);

  if (blocks.length === 0) {
    return res.status(400).json({ error: 'nothing to mine' });
  }

  let haulBase = 0;
  for (const key of blocks) {
    const [bx, by] = key.split(',').map(Number);
    const ore = ORES[getBlockType(bx, by)];
    haulBase += ore.reward;
  }
  const cost = Math.floor(haulBase * HAULING_RATE);
  const net = earnings - cost;

  user.energy -= 1;

  for (const key of blocks) {
    user.mined[key] = true;
  }

  user.balance += net;

  const entry = {
    time: Date.now(),
    direction,
    blocks: blocks.length,
    earnings,
    cost,
    net,
    position: { x: user.x, y: user.y },
  };
  user.log.push(entry);
  if (user.log.length > 50) user.log.shift();

  res.json({
    success: true,
    blocks: blocks.length,
    earnings,
    cost,
    net,
    balance: user.balance,
    energy: user.energy,
    position: { x: user.x, y: user.y },
  });
});

풀이

좌표값에 대한 검증이 없다. Javascript에서 사용하는 모든 수는 64비트 부동 소수점(IEEE 754) 형식으로 이루어져 있다. 따라서 2^53-1(9007199254740991)을 넘어가면 정밀도 손실이 발생한다.

실제로 javascript에서 x = 9007199254740992 라는 값을 입력한 뒤 x+1을 수행하면 9007199254740992라는 결과가 출력된다.

dicectf_diceminer2
으악 이게 뭐야

그래서 방향이 right일 때, cx += dx 에서 cx의 값이 변하지 않는 버그가 발생한다. 그래서 만약 9007199254740991(2에서는 오른쪽으로 캘 수가 없다)에서 오른쪽으로 채굴을 시도하면 x=9007199254740992 좌표에서 반복적으로 채굴을 시도하게 된다.

(거기다 mined[key] = true; 부분때문에 user.mined의 갱신이 전체 채굴 후에 이루어진다.)

그래서 아래와 같이 채굴을 진행하면

dicectf_diceminer3

금 타일 40회만큼의 채굴을 진행하면

dicectf_diceminer4

실제로 750(금)*39(범위40)에 해당하는 29288만큼 포인트가 증가하는 것을 확인할 수 있었다.

dicectf_diceminer5
dicectf_diceminer6