分形空间最后一关解析(技术干货l第五空间线上赛中的两道逆向题解)

分形空间最后一关解析(技术干货l第五空间线上赛中的两道逆向题解)(1)

看到题目文件:

分形空间最后一关解析(技术干货l第五空间线上赛中的两道逆向题解)(2)

从图标来看明显是python打包成的exe程序,使用pyinstxtractor.py来对这个exe程序解包。

Python pyinstxtractor.py main.exe

然后在目录下得到一个main.exe_extracted文件夹,找到其中与程序同名的main文件。因为解包得到的核心pyc文件是去除了文件头的,所以还要找到目录下的struct文件来得到文件头。

分形空间最后一关解析(技术干货l第五空间线上赛中的两道逆向题解)(3)

从struct得到文件头:55 0D 0D 0A 00 00 00 00 70 79 69 30 10 01 00 00 将其填充到main文件的开头16字节。对上面填充好的main文件重命名为main.pyc文件,用uncompyle6进行反编译:

uncompyle6 -o main.py main.pyc

打开main.py文件:

# uncompyle6 version 3.7.4 # Python bytecode 3.8 (3413) # Decompiled from: Python 3.8.6 (tags/v3.8.6:db45529, Sep 23 2020, 15:52:53) [MSC v.1927 64 bit (AMD64)] # Embedded file name: main.py # Compiled at: 1995-09-28 00:18:56 # Size of source mod 2**32: 272 bytes import brainfuck brainfuck.main_check()

到之前解包的目录下找到brainfuck.cp38-win_amd64.pyd,原来这个题给的核心部分在pyd文件,这类似于winodws下的动态链接库。

将该pyd文件与main.py文件放在同一目录下然后执行main.py,随便输入后反馈nonono

接着ida分析该pyd文件,正如模块名字,从其中找到了brainfuck代码。

分形空间最后一关解析(技术干货l第五空间线上赛中的两道逆向题解)(4)

用idapython导出该brainfuck代码:

from ida_bytes import * addr = 0x18000B740 a = "" while addr < 0x18000E8D8: a = chr(get_byte(addr)) addr = 1 f = open("code.txt", "w") f.write(a) f.close() print('*'*100)

再简单的按照brainfuck代码的运算来解析成C代码:

#include <stdio.h> #include <iostream> #include <stdlib.h> #include <string.h> using namespace std; string translate(char c) { switch (c) { case '>': return "p "; case '<': return "p--"; case ' ': return "*p = *p 1"; case '-': return "*p = *p - 1"; case '.': return "cout<<char(*p)"; case ',': return "*p=getchar()"; case '[': return "while(*p){"; case ']': return "}"; default: return ""; } } int main() { FILE *fp = fopen("code.txt", "rb"); FILE *fp1 = fopen("ans.txt", "wb"); char c; while ((c = fgetc(fp)) != EOF) { fputs(translate(c).c_str(), fp1); if (c != '[') fputs(";\n", fp1); } return 0; }

得到1w多行指针运算代码,这也是brainfuck代码的特性,维护几个变量做加减法运算完成程序所有的功能。将得到的C代码处理一下后编译成exe程序:

#include <stdio.h> #include <string.h> #include <iostream> using namespace std; char a[1000]; char *p = a; int main(void) { p ; p ; p ; p ; p ; p ; p ; p ; p ; p ; p ; ... ... ... p--; p--; p--; p--; p--; }

在ida中调试编译得到exe程序。技巧就看它维护几个变量的内存值的变化吧。不断调试可以知道这个程序在比较2个值是否相同用的减法,就是对要比较的2个数依次做减法,看最后他们是否同时为0,若是则相等,否则反之。

还是调试的时候看内存,得到以下信息:首先将输入存入程序中的一块内存区域,然后依次判断开始的几个字符是否是flag{和偏移 0x25的位置是否是}(如下图的内存区域)

分形空间最后一关解析(技术干货l第五空间线上赛中的两道逆向题解)(5)

如果上面比较成功的话就开始对flag{}中的32字节开始进行运算。

发现第一个字节0x61变成0x50,其实就是当前字节和后一个字节异或运算的结果(0x61^0x31)。

继续调试发现这一串密文除了最后一个都变成了0x50

分形空间最后一关解析(技术干货l第五空间线上赛中的两道逆向题解)(6)

所以从我的输入与密文结果可以得出加密逻辑:flag[i] ^= flag[i 1],且在附近的区域找到密文:

分形空间最后一关解析(技术干货l第五空间线上赛中的两道逆向题解)(7)

最后异或回去得到flag:

>>> s = [0x53, 0x0F, 0x5A, 0x54, 0x50, 0x55, 0x03, 0x02, 0x00, 0x07, 0x56, 0x07, 0x07, 0x5B, 0x09, 0x00, 0x50, 0x05, 0x02, 0x03, 0x5D, 0x5C, 0x50, 0x51, 0x52, 0x54, 0x5A, 0x5F, 0x02, 0x57, 0x07, 0x34] >>> for i in range(31): ... s[30-i] ^= s[31-i] ... >>> bytes(s) b'd78b6f30225cdc811adfe8d4e7c9fd34'

分形空间最后一关解析(技术干货l第五空间线上赛中的两道逆向题解)(8)

从题目名称看是使用uni-app这个前端框架。jeb中看了看并没有发现什么关键点,想到题目的提示:JS不只能写网页哦!然后从\assets\appsUNI14D1880\www目录下找到了很多js文件。接着安装好app运行来搜集一下app的字符串信息,发现有Please input...Try again,再使用notepad 的文件夹搜索功能来搜索上面得到的字符串信息,开始的Please input...并没有搜到,但搜到了Try again,也通过这找到关键js文件:app-service.js

分形空间最后一关解析(技术干货l第五空间线上赛中的两道逆向题解)(9)

定位到app-service.js文件中的关键点:输入先与108异或后再经过f["encrypt"]加密,最后与p密文对比。

分形空间最后一关解析(技术干货l第五空间线上赛中的两道逆向题解)(10)

来看到加密函数:就是一个异或运算,关键就是获取这个_keystream

分形空间最后一关解析(技术干货l第五空间线上赛中的两道逆向题解)(11)

找到_keystream生成的地方:从[1634760805, 857760878, 2036477234, 1797285236]定位到这其实是一个chacha20序列密码。

分形空间最后一关解析(技术干货l第五空间线上赛中的两道逆向题解)(12)

从github找到一份python实现的chacha20,适当的修改后再修改key,Nonce及position为js文件中的。(python实现chacha20的key是32字节,iv为8字节,position为0;js中的key同样为32字节但iv为12字节,position为1。这个从对比参数的填充很容易发现)

key:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31]

iv:[0, 0, 0, 0, 0, 0, 0, 74, 0, 0, 0, 0]

position:1

python生成chacha20异或序列代码:

import struct def yield_chacha20_xor_stream(key, iv, position=0): """Generate the xor stream with the ChaCha20 cipher.""" if not isinstance(position, int): raise TypeError if position & ~0xffffffff: raise ValueError('Position is not uint32.') if not isinstance(key, bytes): raise TypeError if not isinstance(iv, bytes): raise TypeError if len(key) != 32: raise ValueError if len(iv) != 12: raise ValueError def rotate(v, c): return ((v << c) & 0xffffffff) | v >> (32 - c) def quarter_round(x, a, b, c, d): x[a] = (x[a] x[b]) & 0xffffffff x[d] = rotate(x[d] ^ x[a], 16) x[c] = (x[c] x[d]) & 0xffffffff x[b] = rotate(x[b] ^ x[c], 12) x[a] = (x[a] x[b]) & 0xffffffff x[d] = rotate(x[d] ^ x[a], 8) x[c] = (x[c] x[d]) & 0xffffffff x[b] = rotate(x[b] ^ x[c], 7) ctx = [0] * 16 ctx[:4] = (1634760805, 857760878, 2036477234, 1797285236) ctx[4 : 12] = struct.unpack('<8L', key) ctx[12] = position ctx[13 : 16] = struct.unpack('<3L', iv) while 1: x = list(ctx) for i in range(10): quarter_round(x, 0, 4, 8, 12) quarter_round(x, 1, 5, 9, 13) quarter_round(x, 2, 6, 10, 14) quarter_round(x, 3, 7, 11, 15) quarter_round(x, 0, 5, 10, 15) quarter_round(x, 1, 6, 11, 12) quarter_round(x, 2, 7, 8, 13) quarter_round(x, 3, 4, 9, 14) for c in struct.pack('<16L', *( (x[i] ctx[i]) & 0xffffffff for i in range(16))): yield c ctx[12] = (ctx[12] 1) & 0xffffffff if ctx[12] == 0: ctx[13] = (ctx[13] 1) & 0xffffffff def chacha20_encrypt(data, key, iv=None, position=1): """Encrypt (or decrypt) with the ChaCha20 cipher.""" if not isinstance(data, bytes): raise TypeError if iv is None: iv = b'\0' * 8 if isinstance(key, bytes): if not key: raise ValueError('Key is empty.') if len(key) < 32: # TODO(pts): Do key derivation with PBKDF2 or something similar. key = (key * (32 // len(key) 1))[:32] if len(key) > 32: raise ValueError('Key too long.') return yield_chacha20_xor_stream(key, iv, position) def run_tests(): import binascii uh = lambda x: binascii.unhexlify(bytes(x, 'ascii')) key = bytes([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31]) iv = bytes([0, 0, 0, 0, 0, 0, 0, 74, 0, 0, 0, 0]) ans = chacha20_encrypt(b'\0' * 32, key, iv) for i, data in enumerate(ans): if i == 38: break print(data, end = ', ') if __name__ == "__main__": run_tests()

34, 79, 81, 243, 64, 27, 217, 225, 47, 222, 39, 111, 184, 99, 29, 237, 140, 19, 31, 130, 61, 44, 6, 226, 126, 79, 202, 236, 158, 243, 207, 120, 138, 59, 10, 163, 114, 96

异或密文与108后得到flag:

>>> p [34, 69, 86, 242, 93, 72, 134, 226, 42, 138, 112, 56, 189, 53, 77, 178, 223, 76, 78, 221, 63, 40, 86, 231, 121, 29, 154, 189, 204, 243, 205, 44, 141, 100, 13, 164, 35, 123] >>> a = [34, 79, 81, 243, 64, 27, 217, 225, 47, 222, 39, 111, 184, 99, 29, 237, 140, 19, 31, 130, 61, 44, 6, 226, 126, 79, 202, 236, 158, 243, 207, 120, 138, 59, 10, 163, 114, 96] >>> ans = [p[i]^a[i]^102 for i in range(38)] >>> bytes(ans) b'flag{59ec211c0695979db6ca4674fd2a9aa7}'

最后说一下如何直接使用给到的js代码生成chacha20的异或序列。对生成密钥序列的代码稍微改一下:

//2.js var r = function (t, e, n) { this._chacha = function () { var t = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], e = 0, n = 0 for (e = 0; e < 16; e ) t[e] = this._param[e] for (e = 0; e < this._rounds; e = 2) { this._quarterround(t, 0, 4, 8, 12), this._quarterround(t, 1, 5, 9, 13) this._quarterround(t, 2, 6, 10, 14) this._quarterround(t, 3, 7, 11, 15) this._quarterround(t, 0, 5, 10, 15) this._quarterround(t, 1, 6, 11, 12) this._quarterround(t, 2, 7, 8, 13) this._quarterround(t, 3, 4, 9, 14) } for (e = 0; e < 16; e ) { t[e] = this._param[e] this._keystream[n ] = 255 & t[e] this._keystream[n ] = (t[e] >>> 8) & 255 this._keystream[n ] = (t[e] >>> 16) & 255 this._keystream[n ] = (t[e] >>> 24) & 255 } } this._quarterround = function (t, e, n, r, o) { t[o] = this._rotl(t[o] ^ (t[e] = t[n]), 16) t[n] = this._rotl(t[n] ^ (t[r] = t[o]), 12) t[o] = this._rotl(t[o] ^ (t[e] = t[n]), 8) t[n] = this._rotl(t[n] ^ (t[r] = t[o]), 7) t[e] >>>= 0 t[n] >>>= 0 t[r] >>>= 0 t[o] >>>= 0 } this._get32 = function (t, e) { return t[e ] ^ (t[e ] << 8) ^ (t[e ] << 16) ^ (t[e] << 24) } this._rotl = function (t, e) { return (t << e) | (t >>> (32 - e)) } this.encrypt = function (t) { return this._update(t) } this.decrypt = function (t) { return this._update(t) } this._update = function (t) { if (!(t instanceof Uint8Array) || 0 === t.length) throw new Error( 'Data should be type of bytes (Uint8Array) and not empty!' ) for (var e = new Uint8Array(t.length), n = 0; n < t.length; n ) { ; (0 !== this._byteCounter && 64 !== this._byteCounter) || (this._chacha(), this._param[12] , (this._byteCounter = 0)) e[n] = this._keystream[this._byteCounter ] } return e } if ( ('undefined' === typeof n && (n = 0), !(t instanceof Uint8Array) || 32 !== t.length) ) throw new Error('Key should be 32 byte array!') if (!(e instanceof Uint8Array) || 12 !== e.length) throw new Error('Nonce should be 12 byte array!') this._rounds = 20 this._sigma = [1634760805, 857760878, 2036477234, 1797285236] this._param = [ this._sigma[0], this._sigma[1], this._sigma[2], this._sigma[3], this._get32(t, 0), this._get32(t, 4), this._get32(t, 8), this._get32(t, 12), this._get32(t, 16), this._get32(t, 20), this._get32(t, 24), this._get32(t, 28), n, this._get32(e, 0), this._get32(e, 4), this._get32(e, 8), ] this._keystream = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ] this._byteCounter = 0 var plain = new Uint8Array(38) //console.log(plain) console.log(this.encrypt(plain).toString()) } for (n = [], o = 0; o <= 31; o ) n[o] = o var t = new Uint8Array(n), e = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 74, 0, 0, 0, 0]), n = 1 r(t, e, n)

然后使用php来加载一下这个js文件:

<?php echo '<script src="2.js"></script>'; ?>

浏览器打开即可看到生成的异或序列:将结果输出toString()方便打印。

分形空间最后一关解析(技术干货l第五空间线上赛中的两道逆向题解)(13)

分形空间最后一关解析(技术干货l第五空间线上赛中的两道逆向题解)(14)

知白讲堂是启明星辰集团网络空间安全学院的在线教育培训平台。丰富的在线课程体系和专家直播讲堂为每一位学员授业解惑。知白讲堂,一直秉承“网络安全,人才当先”的理念,助力梦想,提升职业素养,打造网络安全的行业精英!

分形空间最后一关解析(技术干货l第五空间线上赛中的两道逆向题解)(15)

,

免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。文章投诉邮箱:anhduc.ph@yahoo.com

    分享
    投诉
    首页