【wp】2020XCTF_逆向

c10udlnk發表於2020-12-31

前幾天的XCTF最後一場終於打完了,三場比賽下來對逆向部分的大概感覺是從第一場的啥都不會做(一道lua+一道apk)到後來的終於能有參與度,至少後兩場的題目都是pc逆向,雖然特殊架構但好歹能做(tcl。

本文是三場XCTF所有逆向題目的wp+復現整理。賽中拿到了7/10的flag,很多是跟著隊裡的大佬做出來的(tqltql),這邊就試著獨立復現一下,至少打完應該長長記性(

P.S. 不會的題暫時標了【TODO】,等wp出了回來填坑= =

比賽官網:XCTF高校網路安全專題挑戰賽


[12.20] 華為雲專場

Weird_lua【TODO】

【TODO】

lua是第一次接觸,.lua檔案沒法反編譯,估計虛擬機器被魔改了

divination【TODO】

【TODO】

apk邏輯沒看出來,廢了廢了


[12.23] 鯤鵬計算專場

mips

真·送分題,可惜當時要上課沒來得及搶一血(下午2點放題絕了

老傳統走迷宮

mips架構。

ida反編譯以後可以看到

image-20201229125551113

v4是我們輸入的字串,很明顯是迷宮邏輯,上下左右用wasd走,迷宮存在dword_100111F0裡。

sub_10000744()這個初始函式是用來找起點用的(就是迷宮中3所在的地方,在後面可以看到3其實表示的是當前位置)。

image-20201229125822518

這裡也可以看到應該有多個迷宮(dword_10011D10是用來表示第幾個迷宮的,且<=2,一個迷宮有225個數)+一個迷宮寬為15=三個迷宮,每個迷宮為15*15。

然後就是下面的四個函式,隨便挑一個出來(比如sub_10000D28())可以看到

image-20201229130146475

很明顯是個往右走的函式,3表示當前位置,並把上一個當前位置標為1(可走路徑)。並且可以看到終點是4,就是說我們要把每個迷宮從3走到4。

dump迷宮陣列,寫指令碼列印迷宮:

aMap=[1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 3, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 3, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0]
for i in range(45):
    for j in range(15):
        if aMap[i*15+j]==0:
            tmp='*'
        elif aMap[i*15+j]==1:
            tmp='.'
        elif aMap[i*15+j]==3:
            tmp='@'
        else:
            tmp='#'
        print(tmp,end='')
    print()
    if i==14 or i==29:
        print()

可以看到列印出了三個迷宮,為了看得清楚所以選用幾個特定字元列印。

.....**********
.....*@*.******
.....*.*.******
.....*.*.******
.....*.*.....**
.....*.*****.**
.....*.*****.**
.....*.*****..*
.....*........*
.....********#*
...............
...............
...............
...............
...............
#sssssssddddddds

..*************
..*@*....******
..*.****.******
..*.****.******
..*..***.....**
..*..*******.**
..*..*******.**
..*..*****....*
..*..*****.**.*
..*..*****.****
..*......*.*..*
..*...........*
..***********#*
...............
...............
#ssssssssssdddddddddds

***************
*@..***********
***.*...*******
***...*.*******
****.**.*******
*..*.**.*******
**...**.*******
*******.*******
*******....****
**********.****
**********.****
**********.****
**********....*
*************.*
*************#*
#ddssddwddssssssdddssssdddss

走迷宮,然後把路徑拼起來,根據提示轉md5,get flag。

(有個疑惑哈,第二個迷宮理論上說就算是最短路也有多解?是題目出鍋了還是我哪裡看漏了= =

(再補一句,題目似乎甚至沒要求最短路???神奇.jpg

image-20201229163439245

import hashlib
s=b"sssssssdddddddsssssssssssddddddddddsddssddwddssssssdddssssdddss"
print("flag{%s}"%hashlib.md5(s).hexdigest())

flag{999ea6aa6c365ab43eec2a0f0e5968d5}

pypy

把題目檔案拖進ida,搜尋字串能看到

image-20201229164109092

猜測是pyinstaller打包的檔案。

也就是這個題讓我突然發現pyinstaller還能打包成elf的,於是比賽結束以後趕緊把之前總結的解包指南更新了:RE套路 - 關於pyinstaller打包檔案的復原 | c10udlnk_Log

走流程解包,得到python原始碼。

image-20201229165610468

看到這種混淆變數名,果斷替換成ida style變數名(。

放一下原始碼:

# uncompyle6 version 3.7.4
# Python bytecode 3.8 (3413)
# Decompiled from: Python 2.7.18 (v2.7.18:8d21aa21f2, Apr 20 2020, 13:25:05) [MSC v.1500 64 bit (AMD64)]
# Warning: this version of Python has problems handling the Python 3 "byte" type in constants properly.

# Embedded file name: main.py
# Compiled at: 1995-09-28 00:18:56
# Size of source mod 2**32: 257 bytes
import random, codecs, sys, time, pygame
from pygame.locals import *
from collections import deque
SCREEN_WIDTH = 600
SCREEN_HEIGHT = 480
SIZE = 20
LINE_WIDTH = 1
flag = 'flag{this is a fake flag}'
SCOPE_X = (0, SCREEN_WIDTH // SIZE - 1)
SCOPE_Y = (2, SCREEN_HEIGHT // SIZE - 1)
FOOD_STYLE_LIST = [(10, (255, 100, 100)), (20, (100, 255, 100)), (30, (100, 100, 255))]
LIGHT = (100, 100, 100)
DARK = (200, 200, 200)
BLACK = (0, 0, 0)
RED = (200, 30, 30)
BGCOLOR = (40, 40, 60)

def print_text(v1, v2, v3, v4, v5, fcolor=(255, 255, 255)):
    v6 = v2.render(v5, True, fcolor)
    v1.blit(v6, (v3, v4))


def init_snake():
    v7 = deque()
    v7.append((2, SCOPE_Y[0]))
    v7.append((1, SCOPE_Y[0]))
    v7.append((0, SCOPE_Y[0]))
    return v7


def create_food(v8):
    v9 = random.randint(SCOPE_X[0], SCOPE_X[1])
    v10 = random.randint(SCOPE_Y[0], SCOPE_Y[1])
    while (v9, v10) in v8:
        v9 = random.randint(SCOPE_X[0], SCOPE_X[1])
        v10 = random.randint(SCOPE_Y[0], SCOPE_Y[1])

    return (
     v9, v10)


def get_food_style():
    return FOOD_STYLE_LIST[random.randint(0, 2)]


DEFAULT_KEY = u'Y\xf3\x02\xc3%\x9a\x820\x0b\xbb%\x7f~;\xd2\xdc'

def rc4(v11, key=DEFAULT_KEY, skip=1024):
    v12 = 0
    v13 = bytearray([v14 for v14 in range(256)])
    v12 = 0
    for v15 in range(256):
        v12 = (v12 + v13[v15] + ord(key[(v15 % len(key))])) % 256
        v16 = v13[v15]
        v17 = v13[v12]
        v13[v15] = v13[v12]
        v13[v12] = v16
    else:
        v12 = 0
        v18 = 0
        v19 = []
        if skip > 0:
            for v15 in range(skip):
                v12 = (v12 + 1) % 256
                v18 = (v18 + v13[v12]) % 256
                v13[v12], v13[v18] = v13[v18], v13[v12]

        for v20 in v11:
            v12 = (v12 + 1) % 256
            v18 = (v18 + v13[v12]) % 256
            v13[v12], v13[v18] = v13[v18], v13[v12]
            v21 = v13[((v13[v12] + v13[v18]) % 256)]
            v19.append(chr(ord(v20) ^ v21))
        else:
            return ''.join(v19)


def func(v22):
    v23 = rc4(v22)
    if v23.encode('utf-8').hex() == '275b39c381c28b701ac3972338456022c2ba06c3b04f5501471c47c38ac380c29b72c3b5c38a7ec2a5c2a0':
        return 'YOU WIN'
    return 'YOU LOSE'


def main():
    pygame.init()
    v24 = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
    pygame.display.set_caption(u'\u8d2a\u5403\u86c7')
    v25 = pygame.font.SysFont('SimHei', 24)
    v26 = pygame.font.Font(None, 72)
    v27, v28 = v26.size('GAME OVER')
    v29 = True
    v30 = init_snake()
    v31 = create_food(v30)
    v32 = get_food_style()
    v33 = (1, 0)
    v34 = True
    v35 = False
    v36 = 0
    v37 = 0.5
    v38 = v37
    v39 = None
    v41 = False
    for v40 in pygame.event.get():
        if v40.type == QUIT:
            sys.exit()
        elif v40.type == KEYDOWN:
            if v40.key == K_RETURN:
                if v34:
                    v35 = True
                    v34 = False
                    v29 = True
                    v30 = init_snake()
                    v31 = create_food(v30)
                    v32 = get_food_style()
                    v33 = (1, 0)
                    v36 = 0
                    v39 = time.time()
            elif v40.key == K_SPACE:
                if not v34:
                    v41 = not v41
            elif v40.key in (K_w, K_UP):
                if v29:
                    v33 = v33[1] or (0, -1)
                    v29 = False
            elif v40.key in (K_s, K_DOWN):
                if v29:
                    v33 = v33[1] or (0, 1)
                    v29 = False
            elif v40.key in (K_a, K_LEFT):
                if v29:
                    if not v33[0]:
                        v33 = (-1, 0)
                        v29 = False
                    elif v40.key in (K_d, K_RIGHT):
                        if v29:
                            if not v33[0]:
                                v33 = (1, 0)
                                v29 = False
        else:
            v24.fill(BGCOLOR)
            for v42 in range(SIZE, SCREEN_WIDTH, SIZE):
                pygame.draw.line(v24, BLACK, (v42, SCOPE_Y[0] * SIZE), (v42, SCREEN_HEIGHT), LINE_WIDTH)
            else:
                for v43 in range(SCOPE_Y[0] * SIZE, SCREEN_HEIGHT, SIZE):
                    pygame.draw.line(v24, BLACK, (0, v43), (SCREEN_WIDTH, v43), LINE_WIDTH)
                else:
                    v44 = v34 or time.time()

            if v44 - v39 > v38 and not v41:
                v29 = True
                v39 = v44
                v45 = (v30[0][0] + v33[0], v30[0][1] + v33[1])
                if v45 == v31:
                    v30.appendleft(v45)
                    v36 += v32[0]
                    v38 = v37 - 0.03 * (v36 // 100)
                    v31 = create_food(v30)
                    v32 = get_food_style()
                else:
                    if SCOPE_X[0] <= v45[0] <= SCOPE_X[1]:
                        if SCOPE_Y[0] <= v45[1] <= SCOPE_Y[1]:
                            if v45 not in v30:
                                v30.appendleft(v45)
                                v30.pop()
                            else:
                                v34 = True
                    if not v34:
                        pygame.draw.rect(v24, v32[1], (v31[0] * SIZE, v31[1] * SIZE, SIZE, SIZE), 0)
                for v46 in v30:
                    pygame.draw.rect(v24, DARK, (v46[0] * SIZE + LINE_WIDTH, v46[1] * SIZE + LINE_WIDTH, SIZE - LINE_WIDTH * 2, SIZE - LINE_WIDTH * 2), 0)
                else:
                    print_text(v24, v25, 30, 7, f"speed: {v36 // 100}")
                    print_text(v24, v25, 450, 7, f"score: {v36}")
                    if v36 >= 5192296858534827628530496329220096:
                        v47 = flag
                        print_text(v24, v26, (SCREEN_WIDTH - v27) // 2, (SCREEN_HEIGHT - v28) // 2, func(v47), RED)
                    if v34:
                        if v35:
                            print_text(v24, v26, (SCREEN_WIDTH - v27) // 2, (SCREEN_HEIGHT - v28) // 2, 'GAME OVER', RED)
                    pygame.display.update()


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

可以看到最後getflag這裡(func())的程式邏輯就一個rc4加密,由rc4的特性可知加密和解密流程相同,故複用程式中的rc4()來得到flag。

uncompyle反編譯出來的原始碼是python3,但是題目本身的原始碼是python2,注意編碼問題。

關於編碼問題,可以看:

Unicode之痛 — PyCoder's Weelky CN

關於python2中的unicode和str以及python3中的str和bytes - 明王不動心 - 部落格園

這裡因為反編譯做了轉換成python3的處理,所以指令碼用python3寫。

DEFAULT_KEY = u'Y\xf3\x02\xc3%\x9a\x820\x0b\xbb%\x7f~;\xd2\xdc'
def rc4(v11, key=DEFAULT_KEY, skip=1024):
    v12 = 0
    v13 = bytearray([v14 for v14 in range(256)])
    v12 = 0
    for v15 in range(256):
        v12 = (v12 + v13[v15] + ord(key[(v15 % len(key))])) % 256
        v16 = v13[v15]
        v17 = v13[v12]
        v13[v15] = v13[v12]
        v13[v12] = v16
    else:
        v12 = 0
        v18 = 0
        v19 = []
        if skip > 0:
            for v15 in range(skip):
                v12 = (v12 + 1) % 256
                v18 = (v18 + v13[v12]) % 256
                v13[v12], v13[v18] = v13[v18], v13[v12]

        for v20 in v11:
            v12 = (v12 + 1) % 256
            v18 = (v18 + v13[v12]) % 256
            v13[v12], v13[v18] = v13[v18], v13[v12]
            v21 = v13[((v13[v12] + v13[v18]) % 256)]
            v19.append(chr(ord(v20) ^ v21))
        else:
            return ''.join(v19)
# def func(v22):
#     v23 = rc4(v22)
#     if v23.encode('utf-8').hex() == '275b39c381c28b701ac3972338456022c2ba06c3b04f5501471c47c38ac380c29b72c3b5c38a7ec2a5c2a0':
#         return 'YOU WIN'
#     return 'YOU LOSE'

# -=-=-=以上所有為原始碼中原函式-=-=-=

cipher='275b39c381c28b701ac3972338456022c2ba06c3b04f5501471c47c38ac380c29b72c3b5c38a7ec2a5c2a0'
flag=bytes.fromhex(cipher).decode('utf-8')
print(rc4(flag))

image-20201229190337791

flag{snake_bao_is_really_lucky}

print【TODO】

【TODO】

這個題感覺大概知道怎麼做,但就是不會啊(等wp...

貼一下當時的想法,看了看邏輯只有sprintf這種函式,除此以外沒有別的可以改寫記憶體資料的操作了。

動態除錯跟了一下,猜測是sprintf格式化字串漏洞寫入?

Introduction to format string exploits

sprintf - stm32學習中 - 部落格園

pwn太菜了還沒搞懂要怎麼往output那裡寫(雖然這是逆向題orz

setup函式那裡有一些format的初始化,主要是loop()那裡,控制input(輸入的字串,全部為可見字元且長度>11),來改變使得output!=原來的output且output-1==48('0')。

img


[12.27] HarmonyOS和HMS專場

難得有一場ak逆向了!(雖然有大佬帶著

有三道題都是卡著四血交,實慘TAT

re123

用file命令可以看到是MS Windows HtmlHelp Data檔案(即.chm),檢視檔案頭也可以知道。

image-20201229191137544

image-20201229191253926

所以新增字尾名.chm。

關於chm檔案有一個常用的反編譯器ChmDecompiler,可以釋放CHM裡面的全部原始檔(包括網頁、文字、圖片、CHM、ZIP、EXE等全部原始檔),並且完美地恢復原始檔的全部目錄結構 (摘抄的簡介

所以用ChmDecompiler開啟re.chm,解壓縮,可以看到目錄下出現一個包含四個檔案的資料夾(其實原始檔只有三個,.hhp是ChmDecompiler自動生成的)。

image-20201229191554582

一個一個翻可以看到doc.htm裡有一段奇怪的Item1。

image-20201229192220277

大概可以看到是powershell的語法?(感覺像win後門,這麼多no的引數

查了一下其實就是把後面那大段進行base64解碼而已,用wsl解一下base64有

img

然後得到了一段.NET程式碼(白字)。

通過查微軟文件可以知道,這裡是把base64解碼以後的字元進行Deflate解壓的過程,所以用指令碼把中間那段base64解碼,並整理輸出。

import base64
import zlib
 
def deflate(data):
    try:
        return zlib.decompress(data, -zlib.MAX_WBITS)
    except zlib.error:
        return zlib.decompress(data)

code='TY5BC4IwGIbvgv9hjB2McJhEhNChJMGTkN2qg7qvFHQT/bL575vpoV2/53n2skJJBInkQG5xwqOqhkcQXCATx7q+gkaHsvYj7kIVvCgburItVgm9MTxbVB5LATp5OlQvb6IMV0LdQvdPpu+8x66SL2eOrMl+Ck7naUA69ggND5UcoEOzI+pUc8p62G3TRZubv34K6IbLespADoGR27vv+R7HpqXzt8Q9y0IJI5N8RLCtLw=='
de_code=deflate(base64.b64decode(code)).decode()
for x in de_code.split('\r\n'):
    print(x)

image-20201229193302890

很明顯的邏輯了,把doc.chm(應該是原來的re.chm)中"xxxxxxxx"後面的部分提取出來,還是用base64解碼得到檔案。

img

把這後面的內容手動複製出來到cont.txt裡,進行base64解碼,最後存在theFile中。

base64 -d cont.txt > theFile

檢視theFile可以猜測是exe(畢竟最開始給的就是有powershell指令的base64),把檔案頭補上,並改字尾名(即theFile.exe)。

img

用ida開啟,通過FindCrypt外掛可以看到AES,跟過去能看到AES加密時的S盒(其實這裡前兩個都是S盒,第三個是逆S盒),猜測用到了AES加密。

image-20201229195213434

image-20201229195456197

往上回溯找到主函式

image-20201229195646671

顯然,這裡是AES加密過程,sub_180001100()是金鑰擴充過程,sub_1800015B0()是AES加密。

關於逆向中各種常見密碼的記錄,指路:對稱加密演算法&&Hash演算法 文件 | feng's blog

看了一下感覺是原裝無魔改的AES,密文金鑰都給了,那就直接寫指令碼解密。

注意這裡是以整數形式給出的,別忘了小端序。

from Crypto.Cipher import AES
from binascii import *

arr=[0x16157E2B,0xA6D2AE28,0x8815F7AB,0x3C4FCF09]
key=""
for i in range(4):
    key=hex(arr[i])[2:]+key
key=unhexlify(key)[::-1] #注意小端序的問題
tmp=0x46C42084AA2A1B56E799D643453FF4B5
cipher=unhexlify(hex(tmp)[2:])[::-1]
enc=AES.new(key,AES.MODE_ECB)
print(enc.decrypt(cipher))

image-20201229202801230

flag{youcangues}

puzzle

mips架構。

載入進ida以後,通過字串回溯找到主函式。

image-20201229203258179

image-20201229203319240

可以看到很明顯的sub_401134()這個check,先往這裡面看。

image-20201229203405086

看到是一個疑似maze的邏輯(

不過sub_400FA8()點進去以後可以看到是swap的功能

image-20201229203651570

所以應該不是maze,是一個以交換為主的邏輯。

至於dword_4A0010,可以看到是一個九個數的陣列。

image-20201229203733240

v4和v5的出處在switch邏輯上面一點

image-20201229204029832

可以看到最後(v4,v5)其實表示了陣列裡0的位置,且陣列實際可以看成是3*3。

即:

4 0 3
7 2 6
8 1 5

最後sub_400FFC()的檢查邏輯:

image-20201229204442580

實際上就是要讓這個3*3等於

1 2 3
4 5 6
7 8 0

把0看成空位的話,很容易就想到3*3的華容道了。

(或者玩演算法的小夥伴可能對八數碼問題這個名字更熟悉?

有本事下次出數織啊!20*20我都給你火速解出來(來自數織愛好者的吐槽)

這裡實際上是求最短能得到的路徑(15步),懶得想了,直接去網上抓了個現成程式碼下來改了改。

八數碼問題的程式碼見:八數碼問題-A*(AStar)演算法實現_Broken Geeker-CSDN部落格

#include <iostream>
#include <vector>
#include <ctime>
#include <cstdlib>
#define maxState 10000
#define N 3
using namespace std;

bool isEqual(int a[N][N][maxState],int b[N][N],int n){
    for(int i = 0;i < N;i ++){
        for(int j = 0;j < N;j ++){
            if(a[i][j][n] != b[i][j]) return false;
        }
    }
    return true;
}
bool isEqual(int a[N][N],int b[N][N]){
    for(int i = 0;i < N;i ++){
        for(int j = 0;j < N;j ++){
            if(a[i][j] != b[i][j]) return false;
        }
    }
    return true;
}
int evalute(int state[N][N],int target[N][N]){
    int num = 0;
    for(int i = 0;i < N;i ++){
        for(int j = 0;j < N;j ++)
            if(state[i][j] != target[i][j]) num ++;
    }
    return num;
}
void findBrack(int a[N][N],int x,int y){
    for(int i = 0;i < N;i ++){
        for(int j = 0;j < N;j ++){
            if(a[i][j] == 0) {
                x = i;y = j;return;
            }
        }
    }
}
bool move(int a[N][N],int b[N][N],int dir){
    //1 up 2 down 3 left 4 right
    int x = 0,y = 0;
    for(int i = 0;i < N;i ++){
        for(int j = 0;j < N;j ++){
            b[i][j] = a[i][j];
            if(a[i][j] == 0) {
                x = i;y = j;
            }
        }
    }
    if(x == 0 && dir == 1) return false;
    if(x == N-1 && dir == 2) return false;
    if(y == 0 && dir == 3) return false;
    if(y == N-1 && dir == 4) return false;
    if(dir == 1){b[x-1][y] = 0;b[x][y] = a[x-1][y];}
    else if(dir == 2){b[x+1][y] = 0;b[x][y] = a[x+1][y];}
    else if(dir == 3){b[x][y-1] = 0;b[x][y] = a[x][y-1];}
    else if(dir == 4){b[x][y+1] = 0;b[x][y] = a[x][y+1];}
    else return false;
    return true;
}
void statecpy(int a[N][N][maxState],int b[N][N],int n){
    for(int i = 0;i < N;i ++){
        for(int j = 0;j < N;j ++){
            a[i][j][n] = b[i][j];
        }
    }
}
void getState(int a[N][N][maxState],int b[N][N],int n){
    for(int i = 0;i < N;i ++){
        for(int j = 0;j < N;j ++){
            b[i][j] = a[i][j][n];
        }
    }
}
void statecpy(int a[N][N],int b[N][N]){
    for(int i = 0;i < N;i++){
        for(int j = 0;j < N;j++)
            a[i][j] = b[i][j];
    }
}
int checkAdd(int a[N][N][maxState],int b[N][N],int n){
    for(int i = 0;i < n;i ++){
        if(isEqual(a,b,i)) return i;
    }
    return -1;
}
int Astar(int a[N][N][maxState],int start[N][N],int target[N][N],int path[maxState]){
    bool visited[maxState] = {false};
    int fitness[maxState] = {0};
    int passLen[maxState] = {0};
    int curpos[N][N];
    statecpy(curpos,start);
    int id = 0,Curid = 0;
    fitness[id] = evalute(curpos,target);
    statecpy(a,start,id++);
    while(!isEqual(curpos,target)){
        for(int i = 1;i < 5;i ++){//向四周找方向
            int tmp[N][N] = {0};
            if(move(curpos,tmp,i)){
                int state = checkAdd(a,tmp,id);
                if(state == -1){//not add
                    path[id] = Curid;
                    passLen[id] = passLen[Curid] + 1;
                    fitness[id] = evalute(tmp,target) + passLen[id];
                    statecpy(a,tmp,id++);
                }else{//add
                    int len = passLen[Curid] + 1,fit = evalute(tmp,target) + len;
                    if(fit < fitness[state]){
                        path[state] = Curid;
                        passLen[state] = len;
                        fitness[state] = fit;
                        visited[state] = false;
                    }
                }
            }
        }
        visited[Curid] = true;
        //找到適應度最小的最為下一個帶搜尋節點
        int minCur = -1;
        for(int i = 0;i < id;i ++)
            if(!visited[i] && (minCur == -1 || fitness[i] < fitness[minCur])) minCur = i;
        Curid = minCur;
        getState(a,curpos,Curid);
        if(id == maxState) return -1;
    }
    return Curid;
}
void show(int a[N][N][maxState],int n){
    cout << "-------------------------------\n";
    for(int i = 0;i < N;i ++){
        for(int j =0;j < N;j ++){
            cout << a[i][j][n] << " ";
        }
        cout << endl;
    }
    cout << "-------------------------------\n";
}
int calDe(int a[N][N]){
    int sum = 0;
    for(int i = 0;i < N*N;i ++){
        for(int j = i+1;j < N*N;j ++){
            int m,n,c,d;
            m = i/N;n = i%N;
            c = j/N;d = j%N;
            if(a[c][d] == 0) continue;
            if(a[m][n] > a[c][d]) sum ++;
        }
    }
    return sum;
}
void autoGenerate(int a[N][N]){
    int maxMove = 50;
    srand((unsigned)time(NULL));
    int tmp[N][N];
    while(maxMove --){
        int dir = rand()%4 + 1;
        if(move(a,tmp,dir)) statecpy(a,tmp);
    }
}
int main(){
    int a[N][N][maxState] = {0};
    // int start[N][N] = {1,2,3,4,5,6,7,8,0};
    // autoGenerate(start);
    // cout << start[0][0] << start[1][1];
    int start[N][N] = {4,0,3,7,2,6,8,1,5};
    int target[N][N] = {1,2,3,4,5,6,7,8,0};
    if(!(calDe(start)%2 == calDe(target)%2)){
        cout << "無解\n";
        return 0;
    }
    int path[maxState] = {0};
    int res =  Astar(a,start,target,path);
    if(res == -1){
        cout << "達到最大搜尋能力\n";
        return 0;
    }
    int shortest[maxState] = {0},j = 0;
    while(res != 0){
        shortest[j++] = res;
        res = path[res];
    }
    cout << "第 0 步\n";
    show(a,0);
    for(int i = j - 1;i >= 0;i --){
        cout << "第 " << j-i << " 步\n";
        show(a,shortest[i]);
    }
    return 0;
}

得到每一步的情況,進而根據switch寫出路徑。

第 0 步
-------------------------------
4 0 3
7 2 6
8 1 5
-------------------------------
第 1 步
-------------------------------
4 2 3
7 0 6
8 1 5
-------------------------------
第 2 步
-------------------------------
4 2 3
7 1 6
8 0 5
-------------------------------
第 3 步
-------------------------------
4 2 3
7 1 6
8 5 0
-------------------------------
第 4 步
-------------------------------
4 2 3
7 1 0
8 5 6
-------------------------------
第 5 步
-------------------------------
4 2 0
7 1 3
8 5 6
-------------------------------
第 6 步
-------------------------------
4 0 2
7 1 3
8 5 6
-------------------------------
第 7 步
-------------------------------
4 1 2
7 0 3
8 5 6
-------------------------------
第 8 步
-------------------------------
4 1 2
7 5 3
8 0 6
-------------------------------
第 9 步
-------------------------------
4 1 2
7 5 3
0 8 6
-------------------------------
第 10 步
-------------------------------
4 1 2
0 5 3
7 8 6
-------------------------------
第 11 步
-------------------------------
0 1 2
4 5 3
7 8 6
-------------------------------
第 12 步
-------------------------------
1 0 2
4 5 3
7 8 6
-------------------------------
第 13 步
-------------------------------
1 2 0
4 5 3
7 8 6
-------------------------------
第 14 步
-------------------------------
1 2 3
4 5 0
7 8 6
-------------------------------
第 15 步
-------------------------------
1 2 3
4 5 6
7 8 0
-------------------------------

6 左
2 上
4 右
8 下
// 884226886224488

路徑為“884226886224488”。

接下來看主函式裡check上面的部分,看到sub_409070()實際上是一個scanf,而dword_4A1B60是我們的輸入,也就是最後的flag,中間對輸入進行處理以後才得到“884226886224488”這個字串。

在裡面翻可以翻到一個sub_400B58(),猜測是base64換表編碼。

image-20201229205535378

於是嘗試寫指令碼編碼。

import base64
b64table="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
mytable=""
offset=-18
for i in range(len(b64table)):
    mytable+=b64table[(i+offset)%len(b64table)]
text="884226886224488".encode()
cipher=base64.b64encode(text).decode().translate(str.maketrans(b64table,mytable))
print(cipher)

image-20201229210535246

試試能不能過check。

wsl執行:(要裝qemu才能執行,畢竟特殊架構。

cp $(which qemu-mips) .
./qemu-mips -L . ./puzzle

執行mips程式,輸入指令碼中解出的字串,發現成功了,get flag。

image-20201229210455631

flag{8xOi6R2k8xOk6R2i7xOm}

aRm

arm架構。

照例通過字串回溯找到主函式。

image-20201229220910936

image-20201229222635806

v1是key,v9是輸入的flag,對輸入的限制就是長度為42且頭尾是“flag{”和“}”。

動態調一下可以發現,sub_27770()這個函式實際上是把unk_723A0陣列裡的42個資料複製到v8裡。

./qemu-arm -L ./ -g 12345 ./aRm

(Debugger選Remote GDB debugger,把埠號填上就好,其餘配置具體見RE套路 - 關於使用IDA 7.0前端進行的動態除錯 | c10udlnk_Log中除錯elf部分。

現在我們未知的數就剩v5和v6了,v5要看sub_1714C()的輸出,v6這裡相當於是42條42元一次方程組(輸入未知的情況下)。

而sub_105B4()是輸出42個結果,於是可以知道只要輸出了output.txt裡的42個數就是正確的flag了。

由於前面有一個sub_169AC(key),這邊又是一個無參的sub_1714C()+1,於是猜測是srand(seed)和rand()。

為了證明猜測,多次執行程式輸入同一個key和相同/不同的flag,發現每一次的v5是一樣的,結合rand()的偽隨機性,確定這就是隨機函式。

由於key只有一位元組(0~255),乾脆直接爆破。把output.txt的資料讀入,用sympy庫解方程,只要第一個解x0等於ord('f')^v8[0]=102^0xA0=198,就說明這個key有極大可能性是正確的key。

當然,在此之前,我們得先知道每一次的v5(即方程的係數)是多少。

於是hook函式,在v5生成之後複用程式原來就有的print函式及格式符,把每次生成的v5都列印出來。

還記得有個函式是可以輸出八位十六進位制數的吧,就是那個sub_105B4(),我們可以用這裡面的printf,然後把呼叫這個函式的地方nop掉(目標要明確,現在是為了爆破key,沒必要管程式的正常性hahah)。

本來是想自己堆個呼叫printf出來的,不知道為什麼keypatch對LDR R0, =a08x解釋不了,於是只好繞個小路了。

image-20201230134100558

轉到彙編視窗,記一下這裡的loc,等會要跳過來的。

image-20201230134235154

看回去原來二重迴圈裡出v5那個地方

image-20201230134326684

這幾條語句的意思就是f5裡面的那行v5 = (unsigned __int8)(sub_1714C() + 1);,我們從再下一行開始改。

注意可以改的範圍在藍框這裡,這是我們不需要的v6[j] += (unsigned __int8)v9[k] * v5;,在這個範圍裡可以盡情修改,剩下的nop掉。

image-20201230134904926

用keypatch直接輸入彙編,patch後面的語句為

image-20201230135257443

(其實就是改了一行B loc_105D4,剩下的直接Fill with NOPs就好)

接下來去往loc_105D4,改造一下。

我們知道,現在R3暫存器裡實際上存的是v5的值,我們呼叫printf直接輸出R3的值就能達成目標。

在ARM彙編裡,函式傳參用R0、R1……所以我們這裡給R1一個R3的值就好。

image-20201230135756039

這裡本來就是MOV R1, R3不用改,所以直接把前面nop掉。

因為v5那裡是取(unsigned __int8),所以把這裡改一下,把"%08x"改成"%02x",就是出來的v5。

image-20201230141148584

別忘了後面還要跳回去,找到地址:

image-20201230140112007

patch:

image-20201230140621270

記得把呼叫sub_105B4()的地方也nop掉。

image-20201230140838366

最後把patch的位元組儲存一下。

image-20201230140754908

執行測試一下,有:

image-20201230142921253

ok,hook成功,開始爆破。

用pexpect進行批量的自動化互動見:【wp】2020ChaMd5聖誕題 | c10udlnk_Log

多虧了週五做的那個題,才有了這個題的爆破指令碼(Doge。

import pexpect
from sympy import *

data=[]
with open('output.txt','r') as f:
    tmp=f.read().split('\r\n')
    data=[int(x,16) for x in tmp]
src=[0xA0, 0xE4, 0xBA, 0xFB, 0x10, 0xDD, 0xAC, 0x65, 0x8D, 0x0B, 0x57, 0x1A, 0xE4, 0x28, 0x96, 0xB3, 0x0C, 0x79, 0x4D, 0x80, 0x90, 0x99, 0x58, 0xFE, 0x50, 0xD3, 0xF9, 0x3C, 0x0F, 0xC1, 0xE3, 0xA6, 0x39, 0xC3, 0x28, 0x75, 0xF8, 0xC9, 0xC8, 0xCD, 0x78, 0x26]
flag='flag{000000000000000000000000000000000000}'

var=[]
for num in range(42):
    exec("x"+str(num)+"=Symbol('x'+str(num))")
    var.append("x"+str(num)) #建立42個變數x0~x41
for i in range(256):
    r=pexpect.spawn('./qemu-arm -L ./ ./aRm_getRand')
    r.sendline(str(i))
    r.sendline(flag)
    r.readline()
    r.readline()
    rand=[]
    for j in range(42*42):
        s=r.readline()
        rand.append(int(str(s)[2:-5],16))
    r.wait()
    exper=[]
    for j in range(42):
        anEx=""
        for k in range(42):
            anEx+=str(rand[j*42+k])+"*"+var[k]+"+"
        anEx=anEx[:-1]+"-"+str(data[j])
        exper.append(anEx)
    res=solve(exper,var)
    print(str(i)+": ")
    print(res.values())

爆破得到:

image-20201228135416441

可知key是82,而v9在xor以後的陣列也爆出來了,簡單xor得flag:

arr=[0xA0, 0xE4, 0xBA, 0xFB, 0x10, 0xDD, 0xAC, 0x65, 0x8D, 0x0B, 0x57, 0x1A, 0xE4, 0x28, 0x96, 0xB3, 0x0C, 0x79, 0x4D, 0x80, 0x90, 0x99, 0x58, 0xFE, 0x50, 0xD3, 0xF9, 0x3C, 0x0F, 0xC1, 0xE3, 0xA6, 0x39, 0xC3, 0x28, 0x75, 0xF8, 0xC9, 0xC8, 0xCD, 0x78, 0x26]
x=[198, 136, 219, 156, 107, 228, 152, 7, 239, 63, 97, 127, 134, 5, 247, 131, 109, 75, 96, 180, 241, 173, 57, 211, 49, 224, 157, 9, 34, 243, 129, 199, 1, 244, 31, 17, 157, 171, 252, 249, 64, 91]
flag=""
for i in range(42):
    flag+=chr(x[i]^arr[i])
print(flag)

image-20201230002706300

flag{94bb46eb-a0a2-4a4a-a3d5-2ba877deb448}

pe

arm架構,沒環境調不動,只能硬看了XD。這題有好多奇怪的函式,而且通過虛擬碼跟的話就能看到函式套函式套函式……所以基本靠猜出來的(

繼續通過字串回溯找主函式。

image-20201230004038936

image-20201230004436275

根據引數猜測,sub_1400023C8()是strcmp()的作用,我們需要讓v9="KIMLXDWRZXTHXTHQTXTXHZWC"。

再往上走,sub_1400015B0這個函式呼叫了v9,於是跟進去看功能。

image-20201230085247002

感覺是某種加密,以相鄰的兩字元為一組,對這兩個字元做相同的操作,再做後續處理。

跟進sub_1400012B8()裡看,可以看到大概是一個搜尋的過程

如果不等於-1就說明在表中找到了這個元素,然後返回一個索引(?

再往下看好像就看不太懂了,然後就是玄學的猜猜猜= =

回去看string可以看到一個這個,猜測是金鑰表之類的?

往上回溯也看不到什麼線索,不過可以發現這25個數字剛好沒有相同的。

現在總結一下這個古典加密演算法的特點,大概是兩個為一組處理+已定義的金鑰表(即不是通過輸入生成的)5*5+處理時用到索引。

很久很久以前想寫某對cp的AU同人時想把ctf元素混進去,就看了很多簡單又奇奇怪怪的編碼/古典密碼(現代密碼太學術了XD),沒想到現在有用武之地了(手動狗頭。

安利一個編碼/古典密碼的集合:CTF中那些腦洞大開的編碼和加密 - jack_Meng - 部落格園

然後翻到了一個符合這個特點的密碼,Playfair Cipher:

不同的是密碼錶是直接給出的,不過加密流程再對回ida裡的反編譯感覺挺像的,於是果斷試試。

按照Playfair Cipher的加解密流程寫出指令碼:

def getIndex(c):
    for i in range(len(key)):
        if key[i].find(c)!=-1:
            return i,key[i].find(c)
letter_list="ABCDEFGHJKLMNOPQRSTUVWXYZ"
key=["CREIH","TQGNU","AOVXL","DZKYM","PBWFS"]
cipher="KIMLXDWRZXTHXTHQTXTXHZWC"
text=""
for i in range(0,len(cipher),2):
    j=i+1
    x1,y1=getIndex(cipher[i])
    x2,y2=getIndex(cipher[j])
    if x1==x2:
        text+=key[x1][(y1+1)%5]+key[x2][(y2+1)%5]
    elif y1==y2:
        text+=key[(x1+1)%5][y1]+key[(x2+1)%5][y2]
    else:
        text+=key[x1][y2]+key[x2][y1]
    i+=2
print(text)

走一遍指令碼解密可以得到:

YES MAYBE YOU CAN RUN AN ARM PE

No, I can't ?

看起來能讀的通,成功get flag。

flag{YESMAYBEYOUCANRUNANARMPE}

crash

先去肝ddl回來再補,反正就是個查md5的題(