跳过正文
  1. Writeups/

0xGame2023 Reverse

·591 字·3 分钟·
CTF Reverse
目录

Week1
#

数字筑基
#

在IDA中打开文件,一眼flag。

代码金丹
#

IDA打开即送 +1

image-20231005181146607

网络元婴
#

IDA打开即送 +2。 从上到下输入即可。

虚拟化神
#

反编译程序,在main函数中找到关键变量v13。它的值没有明确给出,而且后续一系列的xor操作都和它有关系,所以通过动态调试找到内存中的值。实际上就是flag。

另一方面,如果不调试。按照官方wp的提示,这个程序会生成一个文件config.txt。之后根据其内容判断是否输出v13的内容。具体的逻辑是提取config.txt的前两个字符,转换为整数并与1比较。这里直接修改该文件,把0改成1,之后运行程序就能拿到flag。

赛博天尊
#

分析程序逻辑:do...while部分判断输入Buffer的长度,要求长度为44。其中最后一个字符为}(ascii 125),接下来以-为分隔符将flag内部32位十六进制数拆分成五部分,各部分满足方程组。

赛博天尊

核心在于解方程组,这里尝试了两种方法。

  1. sympy 库求解
#python
from sympy import *
v7,v8,v9,v10,v11=symbols('v7 v8 v9 v10 v11')

equations=[Eq(7 * v9 + 5 * (v8 + v11) + 2 * (v10 + 4 * v7) , 0x12021DE669FC2),
    Eq( v8 + 3 * (v9 + v10 + 2 * v10 + 2 * (v11 + v7))  , 0x159BFFC17D045),
    Eq(v10 + 3 * (v11 + 3 * v9) + 2 * (v8 + 4 * v7) , 0xACE320D12501),
    Eq(v8 + 2 * (v7 + v11 + v9 + 2 * v10) , 0x733FFEB3A4FA),
    Eq(v8 + 7 * v11 + 8 * (v9 + v10) + 5 * v7 , 0x1935EBA54EB28)]

solution = solve(equations)
solution = sorted(solution.items(), key=lambda item:int(str(item[0])[1:]))
hexs=[]
for value in solution:
    hexs.append(hex(value[1])[2:])
flag='0xGame{'+'{}-{}-{}-{}-{}'.format(*hexs)+'}'
print(flag)
  1. z3 模块求解

    z3求解出来的结果是z3.RatNumRef类型的,不是内置类型。需要转换为int型。此外要先用Solver.check()判断解的情况,才能调用 Solver.model() 输出解。

#python
from z3 import *
v7,v8,v9,v10,v11=Reals('v7 v8 v9 v10 v11')
s=Solver()
s.add(7 * v9 + 5 * (v8 + v11) + 2 * (v10 + 4 * v7) == 0x12021DE669FC2)
s.add(v8 + 3 * (v9 + v10 + 2 * v10 + 2 * (v11 + v7)) == 0x159BFFC17D045)
s.add(v10 + 3 * (v11 + 3 * v9) + 2 * (v8 + 4 * v7) == 0xACE320D12501)
s.add(v8 + 2 * (v7 + v11 + v9 + 2 * v10) == 0x733FFEB3A4FA)
s.add(v8 + 7 * v11 + 8 * (v9 + v10) + 5 * v7 == 0x1935EBA54EB28)

hexs=[]
if isinstance(s.check(), CheckSatResult):
    result=s.model()
    for var in (v7,v8,v9,v10,v11):
        num = result[var].as_long()
        hexs.append(hex(num)[2:])
flag='0xGame{'+'{}-{}-{}-{}-{}'.format(*hexs)+'}'
print(flag)

Week2
#

符文解密师
#

32位程序,IDA打开,在hex窗口找到flag

image-20231008221922973

编译逆旅者
#

文件后缀为.pyc可知是编译过的python程序。鉴于IDA反编译后的结果是C伪代码,并不能得到源python代码。于是考虑在线python反编译。

在main()函数中找到flag的十六进制形式。粘贴(ctrl+shift+v)到十六进制编辑器010Editor中得到flag。

image-20231008220256210

码海舵师
#

32位IDA打开,F5反编译代码如下:

在21行位置发现可疑的一串乱码,末尾‘=’盲猜是base64,在线解密。

image-20231008231113802

image-20231008230909210

注册侦探
#

反编译main函数,发现是c++程序,而且调用了很多API函数。分析程序逻辑后发现flag是在一系列判断之后生成并且输出的,所以尝试动态调试。刚开始不知道RegOpenKeyExA,RegQueryValueExA这两个函数的功能,也没有去查。当时思路是修改寄存器的返回值,绕过这两个函数,但在动调过程中发现无法改data的值。所以最后flag没有生成。

换一种思路,在输出时数据经过了0x33的异或,那么数据是怎么来的?一点点往回找发现和Src变量有关,而且函数中有Src数组的全部值。尝试了一下把Src[0]逐字节异或0x33,结果是0xGa,和flag正好对应。说明就是单字节异或,写脚本解密就得到flag。

注册侦探1

#python
'''
Src[0] = 1383353091;
Src[1] = 189290078;
Src[2] = 38864395;
Src[3] = 503515984;
Src[4] = 1364350722;
Src[5] = 1448105758;
Src[6] = 89136641;
Src[7] = 85852241;
Src[8] = 72812293;
Src[9] = 50464516;
Src[10] = 1314325334;
'''
Src=[1383353091, 189290078, 38864395, 503515984, 1364350722,
     1448105758, 89136641, 85852241, 72812293, 50464516, 1314325334]
flag=bytes()
for num in Src:
    byte=num.to_bytes(4,'little')
    for i in byte:
        flag+=(i^0x33).to_bytes(1)

print(flag.decode())
#0xGame{885b1c80-1dab-dce2-c6b3-664d77410e0d}

看了官方wp,还是从RegOpenKeyExA,RegQueryValueExA入手,创建注册表值,之后运行程序得到flag

注册2

壳艺大师
#

在DIE里查壳发现有upx壳,先进行脱壳。

壳艺大师1

程序反编译后整体看上去有点复杂,涉及到很多未知函数。跟踪这些函数,发现大多数没有什么实际用处,感觉像是用来混淆的,暂且忽略它们。核心的判断部分也比较抽象,v7v8等变量都对应寄存器的值,并不在内存中。缺少关键值,仅凭静态调试梳理不出完整的逻辑。但能看出和异或有关。

壳艺大师3

动调也显得繁琐,涉及寄存器的变量还是很多的。(不知道是不是脱壳的原因)

既然分析不出明确的逻辑,那就猜一下吧。main函数中依然有一系列的数组赋值语句,和上一题非常相似。推测还是对这些数据异或求flag。最开始分析string的时候发现的'The0xGameKey'应该是密钥,循环异或。

#python
from pwn import *
src=[0x64, 0x10, 0x22, 0x51, 0x15, 0x22, 0x1A, 0x0F, 0x06, 0x7C, 
  0x01, 0x18, 0x6C, 0x0A, 0x56, 0x1D, 0x4B, 0x7E, 0x57, 0x08, 
  0x48, 0x28, 0x51, 0x4C, 0x60, 0x45, 0x07, 0x53, 0x1E, 0x77, 
  0x4C, 0x5E, 0x5D, 0x7B, 0x53, 0x4F, 0x61, 0x59, 0x07, 0x52, 
  0x1C, 0x74, 0x07, 0x10]
flag=xor(bytes(src),b'The0xGameKey')
print(flag)
#b'0xGame{bc7da8b3-396e-c454-bcf0-3806651bbd3f}'

Week3
#

代码启示录
#

打开发现是.jar文件,在本地安装JDK来配置Java环境。用反编译器GDA打开文件,一眼flag。

image-20231017192850046

旋转密码城
#

image-20231017194036788

同上GDA打开,Main类中定义了CaesarPlus() 函数,不难发现是变种的凯撒密码。把密码移位的范围从字母扩大到了ASCII可打印字符的范围。密文在main()主函数中给出。编写python脚本解密:

image-20231017194011037

enc="_Iv2>6L424c_4c2\\f__5\\7fec\\da32\\3ef2`cgd4b46N"
flag=''
for char in enc:
    index=ord(char) 
    result=index-33-47
    if result<0:
        result+=94
    flag+=chr(result+33)

print(flag)

变量迷城
#

对Java,android逆向都不太熟悉,拿着反编译代码问了几次GPT。勉强理清了主要的逻辑。

程序只有一个Main类,直接给了decryptFlag解密函数,能看出来还是循环异或。下面的lambda$static$0函数给出了关于xy的方程组,可以解出xybrand变量也已知是'0xGame'

#python
from z3 import *
x,y = Reals('x y')
s=Solver()
s.add(x**2+2*y**2+3*x+4*y == 7384462351178)
s.add(5*x**2+6*y**2+7*x+8*y == 22179606057658)
print(s.model())
#[y = 1919810, x = 114514]

main()的末尾调用解密函数,能看出密钥是xbrand拼接的字符串。但是整个程序并没有给出原始数据,所以不能写脚本解密了。试一下运行程序,但是另一个问题是,程序从头到尾没有任何输入,我们只能等它自己生成flag然后输出。

结合上网搜索和wp,System.getenvSystem.getProperty这两个函数比较关键,提示要创建环境变量和 JVM变量。在命令行中进行设置即可,最后运行jar程序得到flag。

System.getenv()读取的是当前系统环境的环境变量。可以通过 System.getEnv(key) 获取对应环境变量的值。

通过 System.getProperty(key)获取单个变量值,通过System.getPropertys() 获取所有 JVM 环境变量值。

启动JVM时,可以通过 -D 设置JVM 环境变量值

之前一直认为逆向题的flag都是输入到程序的正确字符串,其实是陷入思维定式了。这几周的一些题不是简单地判断输入,而是检查系统的文件或者是变量,还是很打开眼界的。最后还踩了坑:参考wp在cmd敲了命令,结果报错。发现是多了不必要的空格。用powershell试了试发现这段命令不能正常运行。powershell里面set命令不能修改环境变量,要用set-item

变量迷城1

(cmd.exe)
set x=114514
set y=1919810
java -Dbrand=0xGame -jar "C:\Users\LENOVO\Desktop\变量迷城.jar"

数字幽灵城
#

反编译程序,看到有一个Base58类,说明这道题用到Base58算法。之前对Base64接触比较多,对于Base58不是很了解。于是上网查了一下原理。

base58加密与其他base系列不同的是: base64是每6bit位一个映射,base32是每5bit位一个映射 base58不是根据bit位而来,而是直接模58而映射的

其算法很简单 就是将字符流转成256进制的一个超大数 然后不断的模58 最后得到的结果逆序即是结果

分析一下decode函数,就是模58的逆运算,能确定是正常的Base58,没有魔改。接下来分析MainActivity类,内部对编码后的flag,也就是变量C0587R.string.encodedFlag进行解码,再和用户输入的flag比对。我们将经过编码的flag提取出来手动解码即可。查找encodedFlag,应该在资源文件里。

数字幽灵城1

RmC442S4tDMzc3CvzoCx8toKodL8SE8GRQSmz8M84k6g9jG1vVrf3c5TECZR

复制下来拿工具解码即可。大部分在线工具的Base58编码表都是按照比特币的标准,数字在前,之后是大写字母和小写字母。和本题程序的编码表不一样,所以在cyberchef换一下表,进行解码。

数字幽灵城2

虚构核心
#

用jadx反编译程序,核心的函数都在MainActivity类里面。onClick函数调用checkFlag函数进行判断。继续分析checkFlag函数,发现还需要调用FlagCheker类的checkFlag方法。在反编译的结果里面没找到这个类,推测应该和encrypted.dex有关,但是需要解密出decrypted.dex。程序最后给出了解密函数,和密钥The0xGameKey逐字节异或。

虚构核心3

内置的加密文件不能在jadx里直接打开,也没找到直接导出的方法。将整个.apk文件重命名为.zip后缀的压缩包,解压后在 assets\目录下找到了encrypted.dex。按照decrypt函数的逻辑手动解密。

#include<stdio.h>
#include<string.h>
int main()
{
    unsigned char key[] = "The0xGameKey";
    FILE *encdex = fopen("C:\\Users\\LENOVO\\Desktop\\encrypted.dex","rb+");
    FILE *decdex = fopen("C:\\Users\\LENOVO\\Desktop\\decrypted.dex","wb+");
    unsigned char buf[16];
    while (!feof(encdex))
    {
        memset(buf,0,16); 
        fread(buf,sizeof(char),12,encdex);
        for(int i=0; i<12; i++) 
        {
            buf[i] ^= key[i];
        }
        fwrite(buf,sizeof(char),12,decdex);
    }
    fclose(encdex);
    fclose(decdex);
    return 0;
}

虚构核心1

虚构核心2

在010Editor中把解密文件最后面多出的一段删掉,之后用jadx反编译即可。在官方wp给的cmd5网站上查出3段md5的明文,最后各部分拼在一起得到flag

Week4
#

二进制学徒
#

在线反编译.pyc文件,直接看到flag

image-20231029203042866

代码悟道者
#

程序是一个.jar文件。反编译后发现main函数比较清晰,调用加密函数并将结果与内置的密文比较。而且给出了加密算法的具体信息即custom Base64,考虑到换表,用自定义解密工具进行解密得到flag。(这道题挺直白的没什么坑)

代码悟道1

代码悟道2

回过头来分析(学习)一下程序中的Base64算法:先把输入的明文字符串转换为字节,每三个字节以二进制的形式拼接,再按6bit拆成4个字节。其中对二进制数据的处理没有采用数组,而是直接进行位运算,通过移位后或运算/移位后与运算实现了拼接和截取,很简洁高效。(和NSSRound18 的那道Base64对比了一下,但那题还要交换特定的bit)

这里移位时右操作数为负,网上搜索发现结果未定义,根据整体功能猜测符号正负不影响移位结果。

(摘自oi-wiki) 移位运算中如果出现如下情况,则其行为未定义:

  1. 右操作数(即移位数)为负值;
  2. 右操作数大于等于左操作数的位数;

内存星旅者
#

首先静态分析,main函数不算复杂。可以看到比较核心的部分,其中包含两个函数 sub_7FF630D51510sub_7FF630D51600。二者还有同样的int型参数v6

进一步分析,第二个函数可能与生成flag有关。跟踪该函数,发现其内部有点乱,调用了一大堆未知的函数,但是有两个确定的API函数(GetTempPathADeleteFileA,可以上网查到功能)。大概猜测flag被写入一个临时文件,而且最后会删除这个文件。

第一个函数也比较乱,我们的任务是让它的返回值不为0。最好同时能推出v6的值。这个函数需要命令行参数,但是很难从程序反编译的代码中推断该输入什么。复现的时候卡在这里了很久。最后也不去管这些参数了,采用动态调试直接修改寄存器的值,让rdi的最后一位不是零即可。v6的值还不能完全确定,根据官方wp发现其实是函数后面的1897488这个数(其实挺明显的,当时没想到

同样按照动态调试修改寄存器的思路,F7进入第二个函数内部,把传入参数的ecx寄存器的值改为18974880x001CF410)。

内存星3+4
DeleteFileA之前打一个断点,运行后就直接去找临时文件。在 C:\Users\LENOVO\AppData\Local\Temp里找到了刚生成的文件,名字就叫flag。用记事本打开即可。

内存星1

指令神使
#

反编译main函数,发现整体逻辑还算清晰。先把unk_一类的数据恢复为字符串,然后分析程序。输入的flag保存在Str中,再对Str1分别调用sub_140001154sub_140001118函数,最后与Str2比较。

跟踪这两个函数,第一个就是简单地判断flag的格式。第二个函数进行加密,是题目的关键。不难看出是针对小写字母的rot13。(看到9726就猜测和字母表有关系,可能是移位密码,比如摩斯之类的,再根据84推偏移量)最后用CyberChef解一下。把前缀补全。

指令神使
指令神使1

还注意到题目里的一个细节,反编译后的代码没有Str1的赋值语句,但是直接就对它调用函数。而且储存flag的Str数组长度至少是44,但声明部分 char Str[7];,显然会溢出。于是在栈窗口中查找这两个变量,发现它们是相连的,地址上刚好相差0x7,也就是说Str1储存的是Str溢出的内容。或者说Str1是指向Str+7的指针,这样也能解释为什么Str1能与Str2字符串的格式相对应。

指令神使2

算法祭司
#

程序放到DIE里面查看是.NET32位程序,用dnSpy进行反编译,分析Main类。最开始对encryptedKey进行逐字节异或解密。之后读取用户输入,利用异或后的key做密钥,encryptedKey作为初始向量对其进行DES加密。

Main类中直接给出了DES加密后的Base64编码结果。在资源文件中可以找到encryptedKey,进行异或得到密钥。最后写脚本解密DES即可。

#python
import base64
from Crypto.Cipher import DES
encryptedkey="STV>!'+#"
key=''
result='s7/e+JnJbGEdE9j2g3XHxgym+G6Fu/PjJuW80NeMKgemdqaWG9KVM8Tfcc0eRfaA'
for c in encryptedkey:
    key+=chr(ord(c)^ord('f'))

des=DES.new(key.encode(),DES.MODE_CBC,iv=encryptedkey.encode())
enc=base64.b64decode(result)
flag=des.decrypt(enc)
print(flag)
#b'0xGame{8edf2e65-1cb3-2e1a-b2d1-b54d3d4bddc5}\x04\x04\x04\x04

相关文章

0xGame2023 Crypto
·1338 字·7 分钟
CTF Crypto
crypto writeup of 0xgame2023