Trend Micro CTF 2021 - Raimund Genes Cup - Online Qualifier Writeup

この大会は2021/9/18 13:00(JST)~2021/9/19 13:00(JST)に開催されました。
今回もチームで参戦。結果は100点で81チーム中52位でした。
自分で解けた問題をWriteupとして書いておきます。

Reversing-I 100

ファイル名などからPythonの実行ファイル化したものと推測できる。Pythonコードにデコンパイルする。

$ python pyinstxtractor.py PyPuzzle.exe 
[*] Processing PyPuzzle.exe
[*] Pyinstaller version: 2.1+
[*] Python version: 37
[*] Length of package: 8032677 bytes
[*] Found 76 files in CArchive
[*] Beginning extraction...please standby
[!] Warning: The script is running in a different python version than the one used to build the executable
    Run this script in Python37 to prevent extraction errors(if any) during unmarshalling
[!] Unmarshalling FAILED. Cannot extract PYZ-00.pyz. Extracting remaining files.
[*] Successfully extracted pyinstaller archive: PyPuzzle.exe

You can now use a python decompiler on the pyc files within the extracted directory

PyPuzzle.exe_extracted\PyPuzzleを編集し、ヘッダを付けpycにする。ヘッダは以下を16バイト付けた。

3e 0d 0d 0a 00 00 00 00 00 00 00 00 00 00 00 00
$ uncompyle6 PyPuzzle.pyc
# uncompyle6 version 3.7.4
# Python bytecode 3.7 (3390)
# Decompiled from: Python 3.6.9 (default, Jan 26 2021, 15:33:00) 
# [GCC 8.4.0]
# Embedded file name: PyPuzzle.py
import pygame, sys, random
from pygame.locals import *
BOARDWIDTH = 4
BOARDHEIGHT = 4
TILESIZE = 100
WINDOWWIDTH = 500
WINDOWHEIGHT = 550
FPS = 30
BLANK = None
BLACK = (0, 0, 0)
WHITE = (248, 240, 239)
BRIGHTBLUE = (0, 50, 255)
DARKTURQUOISE = (3, 54, 73)
BLUE = (214, 25, 32)
GREEN = (0, 128, 0)
RED = (255, 0, 0)
BGCOLOR = (248, 240, 239)
TILECOLOR = BLUE
TEXTCOLOR = WHITE
BORDERCOLOR = RED
BASICFONTSIZE = 20
TEXT = (214, 25, 32)
BUTTONCOLOR = (214, 25, 32)
BUTTONTEXTCOLOR = (214, 25, 32)
MESSAGECOLOR = (214, 25, 32)
XMARGIN = int((WINDOWWIDTH - (TILESIZE * BOARDWIDTH + (BOARDWIDTH - 1))) / 4)
YMARGIN = int((WINDOWHEIGHT - (TILESIZE * BOARDHEIGHT + (BOARDHEIGHT - 1))) / 2)
UP = 'up'
DOWN = 'down'
LEFT = 'left'
RIGHT = 'right'

def main():
    global BASICFONT
    global DISPLAYSURF
    global FPSCLOCK
    global NEW_RECT
    global NEW_SURF
    global RESET_RECT
    global RESET_SURF
    global SOLVE_RECT
    global SOLVE_SURF
    pygame.init()
    FPSCLOCK = pygame.time.Clock()
    DISPLAYSURF = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT))
    pygame.display.set_caption('Slide Puzzle')
    BASICFONT = pygame.font.Font('freesansbold.ttf', BASICFONTSIZE)
    RESET_SURF, RESET_RECT = makeText('Reset', TEXT, BGCOLOR, WINDOWWIDTH - 375, WINDOWHEIGHT - 40)
    NEW_SURF, NEW_RECT = makeText('New Game', TEXT, BGCOLOR, WINDOWWIDTH - 250, WINDOWHEIGHT - 40)
    SOLVE_SURF, SOLVE_RECT = makeText('Solve', WHITE, BGCOLOR, WINDOWWIDTH - 80, WINDOWHEIGHT - 40)
    mainBoard, solutionSeq = generateNewPuzzle(80)
    SOLVEDBOARD = getStartingBoard()
    allMoves = []
    while True:
        slideTo = None
        msg = 'Click tile or press arrow keys to slide.'
        if mainBoard == SOLVEDBOARD:
            msg = 'Solved! But Wait you need to decrypt the flag *_^'
            flag = '3blKlvIKqQoG0D6B4XDcZxQjbLbg4KVJQzbm8b'
        drawBoard(mainBoard, msg)
        checkForQuit()
        for event in pygame.event.get():
            if event.type == MOUSEBUTTONUP:
                spotx, spoty = getSpotClicked(mainBoard, event.pos[0], event.pos[1])
                if (
                 spotx, spoty) == (None, None):
                    if RESET_RECT.collidepoint(event.pos):
                        resetAnimation(mainBoard, allMoves)
                        allMoves = []
                    else:
                        if NEW_RECT.collidepoint(event.pos):
                            mainBoard, solutionSeq = generateNewPuzzle(80)
                            allMoves = []
                        else:
                            if SOLVE_RECT.collidepoint(event.pos):
                                resetAnimation(mainBoard, solutionSeq + allMoves)
                                allMoves = []
                else:
                    blankx, blanky = getBlankPosition(mainBoard)
                    if spotx == blankx + 1 and spoty == blanky:
                        slideTo = LEFT
                    else:
                        if spotx == blankx - 1 and spoty == blanky:
                            slideTo = RIGHT
                        else:
                            if spotx == blankx and spoty == blanky + 1:
                                slideTo = UP
                            else:
                                if spotx == blankx:
                                    if spoty == blanky - 1:
                                        slideTo = DOWN
                                    elif event.type == KEYUP:
                                        if event.key in (K_LEFT, K_a):
                                            if isValidMove(mainBoard, LEFT):
                                                slideTo = LEFT
                                        else:
                                            if event.key in (K_RIGHT, K_d):
                                                if isValidMove(mainBoard, RIGHT):
                                                    slideTo = RIGHT
                                            if event.key in (K_UP, K_w):
                                                if isValidMove(mainBoard, UP):
                                                    slideTo = UP
                                        if event.key in (K_DOWN, K_s):
                                            if isValidMove(mainBoard, DOWN):
                                                slideTo = DOWN

        if slideTo:
            slideAnimation(mainBoard, slideTo, 'Click tile or press arrow keys to slide.', 8)
            makeMove(mainBoard, slideTo)
            allMoves.append(slideTo)
        pygame.display.update()
        FPSCLOCK.tick(FPS)


def terminate():
    pygame.quit()
    sys.exit()


def checkForQuit():
    for event in pygame.event.get(QUIT):
        terminate()

    for event in pygame.event.get(KEYUP):
        if event.key == K_ESCAPE:
            terminate()
        pygame.event.post(event)


def getStartingBoard():
    counter = 1
    board = []
    for x in range(BOARDWIDTH):
        column = []
        for y in range(BOARDHEIGHT):
            column.append(counter)
            counter += BOARDWIDTH

        board.append(column)
        counter -= BOARDWIDTH * (BOARDHEIGHT - 1) + BOARDWIDTH - 1

    board[(BOARDWIDTH - 1)][BOARDHEIGHT - 1] = BLANK
    return board


def getBlankPosition(board):
    for x in range(BOARDWIDTH):
        for y in range(BOARDHEIGHT):
            if board[x][y] == BLANK:
                return (
                 x, y)


def makeMove(board, move):
    blankx, blanky = getBlankPosition(board)
    if move == UP:
        board[blankx][blanky], board[blankx][blanky + 1] = board[blankx][(blanky + 1)], board[blankx][blanky]
    else:
        if move == DOWN:
            board[blankx][blanky], board[blankx][blanky - 1] = board[blankx][(blanky - 1)], board[blankx][blanky]
        else:
            if move == LEFT:
                board[blankx][blanky], board[(blankx + 1)][blanky] = board[(blankx + 1)][blanky], board[blankx][blanky]
            else:
                if move == RIGHT:
                    board[blankx][blanky], board[(blankx - 1)][blanky] = board[(blankx - 1)][blanky], board[blankx][blanky]


def isValidMove(board, move):
    blankx, blanky = getBlankPosition(board)
    return move == UP and blanky != len(board[0]) - 1 or move == DOWN and blanky != 0 or move == LEFT and blankx != len(board) - 1 or move == RIGHT and blankx != 0


def getRandomMove(board, lastMove=None):
    validMoves = [
     UP, DOWN, LEFT, RIGHT]
    if not (lastMove == UP or isValidMove(board, DOWN)):
        validMoves.remove(DOWN)
    if not (lastMove == DOWN or isValidMove(board, UP)):
        validMoves.remove(UP)
    if not (lastMove == LEFT or isValidMove(board, RIGHT)):
        validMoves.remove(RIGHT)
    if not (lastMove == RIGHT or isValidMove(board, LEFT)):
        validMoves.remove(LEFT)
    return random.choice(validMoves)


def getLeftTopOfTile(tileX, tileY):
    left = XMARGIN + tileX * TILESIZE + (tileX - 1)
    top = YMARGIN + tileY * TILESIZE + (tileY - 1)
    return (left, top)


def getSpotClicked(board, x, y):
    for tileX in range(len(board)):
        for tileY in range(len(board[0])):
            left, top = getLeftTopOfTile(tileX, tileY)
            tileRect = pygame.Rect(left, top, TILESIZE, TILESIZE)
            if tileRect.collidepoint(x, y):
                return (
                 tileX, tileY)

    return (None, None)


def drawTile(tilex, tiley, number, adjx=0, adjy=0):
    left, top = getLeftTopOfTile(tilex, tiley)
    pygame.draw.rect(DISPLAYSURF, TILECOLOR, (left + adjx, top + adjy, TILESIZE, TILESIZE))
    textSurf = BASICFONT.render(str(number), True, TEXTCOLOR)
    textRect = textSurf.get_rect()
    textRect.center = (left + int(TILESIZE / 2) + adjx, top + int(TILESIZE / 2) + adjy)
    DISPLAYSURF.blit(textSurf, textRect)


def makeText(text, color, bgcolor, top, left):
    textSurf = BASICFONT.render(text, True, color, bgcolor)
    textRect = textSurf.get_rect()
    textRect.topleft = (top, left)
    return (textSurf, textRect)


def drawBoard(board, message):
    DISPLAYSURF.fill(BGCOLOR)
    if message:
        textSurf, textRect = makeText(message, MESSAGECOLOR, BGCOLOR, 5, 5)
        DISPLAYSURF.blit(textSurf, textRect)
    for tilex in range(len(board)):
        for tiley in range(len(board[0])):
            if board[tilex][tiley]:
                drawTile(tilex, tiley, board[tilex][tiley])

    left, top = getLeftTopOfTile(0, 0)
    width = BOARDWIDTH * TILESIZE
    height = BOARDHEIGHT * TILESIZE
    pygame.draw.rect(DISPLAYSURF, BORDERCOLOR, (left - 5, top - 5, width + 11, height + 11), 4)
    DISPLAYSURF.blit(RESET_SURF, RESET_RECT)
    DISPLAYSURF.blit(NEW_SURF, NEW_RECT)
    DISPLAYSURF.blit(SOLVE_SURF, SOLVE_RECT)


def slideAnimation(board, direction, message, animationSpeed):
    blankx, blanky = getBlankPosition(board)
    if direction == UP:
        movex = blankx
        movey = blanky + 1
    else:
        if direction == DOWN:
            movex = blankx
            movey = blanky - 1
        else:
            if direction == LEFT:
                movex = blankx + 1
                movey = blanky
            else:
                if direction == RIGHT:
                    movex = blankx - 1
                    movey = blanky
    drawBoard(board, message)
    baseSurf = DISPLAYSURF.copy()
    moveLeft, moveTop = getLeftTopOfTile(movex, movey)
    pygame.draw.rect(baseSurf, BGCOLOR, (moveLeft, moveTop, TILESIZE, TILESIZE))
    for i in range(0, TILESIZE, animationSpeed):
        checkForQuit()
        DISPLAYSURF.blit(baseSurf, (0, 0))
        if direction == UP:
            drawTile(movex, movey, board[movex][movey], 0, -i)
        if direction == DOWN:
            drawTile(movex, movey, board[movex][movey], 0, i)
        if direction == LEFT:
            drawTile(movex, movey, board[movex][movey], -i, 0)
        if direction == RIGHT:
            drawTile(movex, movey, board[movex][movey], i, 0)
        pygame.display.update()
        FPSCLOCK.tick(FPS)


def generateNewPuzzle(numSlides):
    sequence = []
    board = getStartingBoard()
    drawBoard(board, '')
    pygame.display.update()
    pygame.time.wait(500)
    lastMove = None
    for i in range(numSlides):
        move = getRandomMove(board, lastMove)
        slideAnimation(board, move, 'Generating new puzzle...', animationSpeed=(int(TILESIZE / 3)))
        makeMove(board, move)
        sequence.append(move)
        lastMove = move

    return (
     board, sequence)


def resetAnimation(board, allMoves):
    revAllMoves = allMoves[:]
    revAllMoves.reverse()
    for move in revAllMoves:
        if move == UP:
            oppositeMove = DOWN
        else:
            if move == DOWN:
                oppositeMove = UP
            else:
                if move == RIGHT:
                    oppositeMove = LEFT
                else:
                    if move == LEFT:
                        oppositeMove = RIGHT
        slideAnimation(board, oppositeMove, '', animationSpeed=(int(TILESIZE / 2)))
        makeMove(board, oppositeMove)


if __name__ == '__main__':
    main()
# okay decompiling PyPuzzle.pyc

flagの値'3blKlvIKqQoG0D6B4XDcZxQjbLbg4KVJQzbm8b'を復号する必要がある。
base62と推測して、https://www.better-converter.com/Encoders-Decoders/Base62-Decodeで復号する。

GZPGS{EriRe$!at_Vf_@_Chmmyr}

さらにシーザー暗号と推測して、https://www.geocachingtoolbox.com/index.php?lang=en&page=caesarCipherで復号する。

Rotation 13:
TMCTF{RevEr$!ng_Is_@_Puzzle}
TMCTF{RevEr$!ng_Is_@_Puzzle}