前幾天的XCTF最後一場終於打完了,三場比賽下來對逆向部分的大概感受是從第一場的啥都不會作(一道lua+一道apk)到後來的終於能有參與度,至少後兩場的題目都是pc逆向,雖然特殊架構但好歹能作(tcl。html
本文是三場XCTF全部逆向題目的wp+復現整理。賽中拿到了7/10的flag,不少是跟着隊裏的大佬作出來的(tqltql),這邊就試着獨立復現一下,至少打完應該長長記性(前端
P.S. 不會的題暫時標了【TODO】,等wp出了回來填坑= =python
比賽官網:XCTF高校網絡安全專題挑戰賽ios
【TODO】算法
lua是第一次接觸,.lua文件無法反編譯,估計虛擬機被魔改了shell
【TODO】數組
apk邏輯沒看出來,廢了廢了安全
真·送分題,惋惜當時要上課沒來得及搶一血(下午2點放題絕了網絡
老傳統走迷宮架構
mips架構。
ida反編譯之後能夠看到
v4是咱們輸入的字符串,很明顯是迷宮邏輯,上下左右用wasd走,迷宮存在dword_100111F0裏。
sub_10000744()這個初始函數是用來找起點用的(就是迷宮中3所在的地方,在後面能夠看到3其實表示的是當前位置)。
這裏也能夠看到應該有多個迷宮(dword_10011D10是用來表示第幾個迷宮的,且<=2,一個迷宮有225個數)+一個迷宮寬爲15=三個迷宮,每一個迷宮爲15*15。
而後就是下面的四個函數,隨便挑一個出來(好比sub_10000D28())能夠看到
很明顯是個往右走的函數,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
import hashlib s=b"sssssssdddddddsssssssssssddddddddddsddssddwddssssssdddssssdddss" print("flag{%s}"%hashlib.md5(s).hexdigest())
flag{999ea6aa6c365ab43eec2a0f0e5968d5}
把題目文件拖進ida,搜索字符串能看到
猜想是pyinstaller打包的文件。
也就是這個題讓我忽然發現pyinstaller還能打包成elf的,因而比賽結束之後趕忙把以前總結的解包指南更新了:RE套路 - 關於pyinstaller打包文件的復原 | c10udlnk_Log。
走流程解包,獲得python源碼。
看到這種混淆變量名,果斷替換成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,注意編碼問題。
關於編碼問題,能夠看:
這裏由於反編譯作了轉換成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))
flag{snake_bao_is_really_lucky}
【TODO】
這個題感受大概知道怎麼作,但就是不會啊(等wp...
貼一下當時的想法,看了看邏輯只有sprintf這種函數,除此之外沒有別的能夠改寫內存數據的操做了。
動態調試跟了一下,猜想是sprintf格式化字符串漏洞寫入?
pwn太菜了還沒搞懂要怎麼往output那裏寫(雖然這是逆向題orz
setup函數那裏有一些format的初始化,主要是loop()那裏,控制input(輸入的字符串,所有爲可見字符且長度>11),來改變使得output!=原來的output且output-1==48('0')。
可貴有一場ak逆向了!(雖然有大佬帶着
有三道題都是卡着四血交,實慘TAT
用file命令能夠看到是MS Windows HtmlHelp Data文件(即.chm),查看文件頭也能夠知道。
因此添加後綴名.chm。
關於chm文件有一個經常使用的反編譯器ChmDecompiler,能夠釋放CHM裏面的所有源文件(包括網頁、文本、圖片、CHM、ZIP、EXE等所有源文件),而且完美地恢復源文件的所有目錄結構 (摘抄的簡介。
因此用ChmDecompiler打開re.chm,解壓縮,能夠看到目錄下出現一個包含四個文件的文件夾(其實源文件只有三個,.hhp是ChmDecompiler自動生成的)。
一個一個翻能夠看到doc.htm裏有一段奇怪的Item1。
大概能夠看到是powershell的語法?(感受像win後門,這麼多no的參數
查了一下其實就是把後面那大段進行base64解碼而已,用wsl解一下base64有
而後獲得了一段.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)
很明顯的邏輯了,把doc.chm(應該是原來的re.chm)中"xxxxxxxx"後面的部分提取出來,仍是用base64解碼獲得文件。
把這後面的內容手動複製出來到cont.txt裏,進行base64解碼,最後存在theFile中。
base64 -d cont.txt > theFile
查看theFile能夠猜想是exe(畢竟最開始給的就是有powershell指令的base64),把文件頭補上,並改後綴名(即theFile.exe)。
用ida打開,經過FindCrypt插件能夠看到AES,跟過去能看到AES加密時的S盒(其實這裏前兩個都是S盒,第三個是逆S盒),猜想用到了AES加密。
往上回溯找到主函數
顯然,這裏是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))
flag{youcangues}
mips架構。
加載進ida之後,經過字符串回溯找到主函數。
能夠看到很明顯的sub_401134()這個check,先往這裏面看。
看到是一個疑似maze的邏輯(
不過sub_400FA8()點進去之後能夠看到是swap的功能
因此應該不是maze,是一個以交換爲主的邏輯。
至於dword_4A0010,能夠看到是一個九個數的數組。
v4和v5的出處在switch邏輯上面一點
能夠看到最後(v4,v5)其實表示了數組裏0的位置,且數組實際能夠當作是3*3。
即:
4 0 3 7 2 6 8 1 5
最後sub_400FFC()的檢查邏輯:
實際上就是要讓這個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換表編碼。
因而嘗試寫腳本編碼。
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)
試試能不能過check。
wsl運行:(要裝qemu才能執行,畢竟特殊架構。
cp $(which qemu-mips) . ./qemu-mips -L . ./puzzle
執行mips程序,輸入腳本中解出的字符串,發現成功了,get flag。
flag{8xOi6R2k8xOk6R2i7xOm}
arm架構。
照例經過字符串回溯找到主函數。
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
解釋不了,因而只好繞個小路了。
轉到彙編窗口,記一下這裏的loc,等會要跳過來的。
看回去原來二重循環裏出v5那個地方
這幾條語句的意思就是f5裏面的那行v5 = (unsigned __int8)(sub_1714C() + 1);
,咱們從再下一行開始改。
注意能夠改的範圍在藍框這裏,這是咱們不須要的v6[j] += (unsigned __int8)v9[k] * v5;
,在這個範圍裏能夠盡情修改,剩下的nop掉。
用keypatch直接輸入彙編,patch後面的語句爲
(其實就是改了一行B loc_105D4
,剩下的直接Fill with NOPs就好)
接下來去往loc_105D4,改造一下。
咱們知道,如今R3寄存器裏實際上存的是v5的值,咱們調用printf直接輸出R3的值就能達成目標。
在ARM彙編裏,函數傳參用R0、R1……因此咱們這裏給R1一個R3的值就好。
這裏原本就是MOV R1, R3
不用改,因此直接把前面nop掉。
由於v5那裏是取(unsigned __int8),因此把這裏改一下,把"%08x"改爲"%02x",就是出來的v5。
別忘了後面還要跳回去,找到地址:
patch:
記得把調用sub_105B4()的地方也nop掉。
最後把patch的字節保存一下。
運行測試一下,有:
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())
爆破獲得:
可知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)
flag{94bb46eb-a0a2-4a4a-a3d5-2ba877deb448}
arm架構,沒環境調不動,只能硬看了XD。這題有好多奇怪的函數,並且經過僞代碼跟的話就能看到函數套函數套函數……因此基本靠猜出來的(
繼續經過字符串回溯找主函數。
根據參數猜想,sub_1400023C8()是strcmp()的做用,咱們須要讓v9="KIMLXDWRZXTHXTHQTXTXHZWC"。
再往上走,sub_1400015B0這個函數調用了v9,因而跟進去看功能。
感受是某種加密,以相鄰的兩字符爲一組,對這兩個字符作相同的操做,再作後續處理。
跟進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}
先去肝ddl回來再補,反正就是個查md5的題(