跳过正文
  1. Writeups/

0xGame2024 Reverse Official

·1156 字·6 分钟·
CTF Reverse
目录

Week1
#

SignSign
#

打开程序,直接送上flag,但只输出了flag的后一半??

W1-signin00

为了找到完整的flag,我们用ida打开程序,进行静态分析。

正常情况下,一开始能看到一个写满汇编指令的窗口。这个就是程序对应主函数的反汇编结果。

W1-signin0

但是主函数中并没有另一半flag。

这个时候我们的思路应该是去搜集一下这个程序的相关信息,尤其是字符串信息很重要。

打开字符串窗口(快捷键Shift+F12),查看程序的所有字符串。

能看到两段flag都在其中,拼接一下即可提交。

W1-signin1

BinaryMaster
#

有两种思路:

  • 按照题目提示,八进制04242424转成十六进制为0x114514。把这个结果输入程序后拿到flag。
  • IDA反编译主函数,可以直接看到flag明文。

对于第二种思路:在反汇编结果页面,按F5(或者Tab)就可以生成反编译的伪C代码。这一功能大大降低了逆向分析的难度。反编译成功后,程序的主要逻辑就很清晰明白了。

W1-BinaryMaster1

BabyBase
#

按照上面两题的方法反编译程序主函数。

W1-babybase01

发现调用了两个可能和flag相关的函数:encode()check_flag(),后续要着重分析。

如果继续收集信息,查看字符串,能够发现一个比较特殊的字符串:(下图蓝色高亮)

W1-babybase0

有经验的师傅一眼就能看出是base64标准索引表,然后直接base64decode一把梭了。不过没看出也不要紧。

按照常规思路,逆向分析上面提到的两个关键函数:

  • check_flag:检查编码后的结果是否正确,其实就是给出了编码结果(在上面字符串窗口也能看到)

    • W1-babybase22
  • encode:Base64编码的具体实现,如果要彻底看懂这个函数,最好提前了解一下Base64的原理。

    • 不过看到一些关键特征也能大概推测出来,比如3字节输入变成4字节输出、以及添加到末尾的等号。

    • W1-babybase02

最后拿着结果找个在线网站解码就可以,非常推荐: CyberChef

W1-babybase1

Xor-Beginning
#

反编译后,可以暂时忽略主函数中常见的IO函数和数据赋值等部分,以便快速找到核心逻辑。

可以发现关键代码是下图的一段循环。涉及到运算符^,对应的运算是异或。

W1-xorbegin1

异或(xor)是逆向中很常见的一种位运算,它的逆运算就是自身,也就是 (A ^ B) ^ B == A

由此推导出解密方法和加密方法相同。

回到前面提取出赋值给数组 v5 的密文,可以写出如下解密脚本:

data=[0x7E, 0x35, 0x0B, 0x2A, 0x27, 0x2C, 0x33, 0x1F, 0x76, 0x37, 
      0x1B, 0x72, 0x31, 0x1E, 0x36, 0x0C, 0x4C, 0x44, 0x63, 0x72, 
      0x57, 0x49, 0x08, 0x45, 0x42, 0x01, 0x5A, 0x04, 0x13, 0x4C]
for i in range(len(data)):
    data[i] = data[i]^(78-i)
print(bytes(data).decode("utf-8"))

Xor-Endian
#

c语言中int类型是32位数据,即4字节。

由于数据在内存中以字节为单位存储,int类型也可以看作长度为4的char数组。

多字节类型的数据,各个字节按照 小端序(Little Endian)存储,这一顺序与我们习惯的阅读顺序相反。

本题的文件格式是ELF,也就是Linux下的可执行程序。

可以在Linux系统下用 file 命令查一下:

W1-endian1

ELF在Windows系统下不能直接运行,但并不妨碍用IDA进行静态分析。

和上一题类似,程序同样使用了xor进行加密,但是密文是比较大的整数,有点奇怪。

W1-xoren1

出题时这里其实是char数组强制类型转换成了int数组,每一个数字实际上对应着四个字符。

在提取密文进行解密的时候,要把这些数字重新转换成字符串或者char数组。过程中一定注意小端序的处理,才能让最后得到的字符串顺序正确。例如下面python脚本使用的函数 to_bytes(4,"little")

data = [1363025275,253370901,1448151638,1415391232,91507463,139743552,
      1450318164,1985283101,1465125718,1934953223,84430593]
source = b''.join([element.to_bytes(4,"little") for element in data])
key = b'Key0xGame2024'

for i in range(len(source)):
    flag = chr(source[i] ^ key[i % len(key)])
    print(flag,end="")

当然也可以巧妙利用IDA规避这一问题。IDA自动按照小端序解析字符串,但是注意伪代码中字符串用双引号标注,有的师傅遇到下图的情况:

W1-xoren2

仔细观察能发现是用的单引号,这个应该理解为字节序列,依然按照大端的顺序从左到右解析的。

这个时候按一下F5重新反编译就能看到正确顺序的字符串。最后解密思路和上面脚本相同。

W1-xoren3

Week2
#

BabyUPX
#

根据题目的提示,可以猜到本题加了UPX壳。UPX是一种常见的压缩壳,可以使用查壳工具进行检测。常用的有以下两种:

W2-upx5

  • Detect-It-Easy (比较推荐,Github:DIE

W2-upx1

压缩壳对exe文件进行压缩,在执行原程序代码前率先取得控制权,自动对原始程序解压还原,以达到保护程序不被静态反编译的目的。如果直接把程序扔进IDA,就会看到警告和一个明显不正常的start函数,而我们想要的主函数是无法找到的。

W2-upx00

W2-upx01

这里就要考虑如何脱壳。UPX能够压缩的同时自带解压功能,所以先去下载 UPX- the Ultimate Packer for eXecutables(命令行工具)。然后一把抓住程序,输入脱壳命令,顷刻炼化。

upx -d <filename>

W2-upx2

脱壳后能用IDA正常打开。分析encode函数,可以看出每个字节高4位和低4位交换,实际上就是16进制逆序。

W2-upx4

FirstSight-Pyc
#

.pyc是Python源代码经过编译生成的字节码文件。可以选择pycdc或者uncompyle6 进行反编译。

要注意pyc文件对应的python版本可能会影响反编译的效果,本题使用的是较低的3.8版本,经过测试上述两个反编译引擎都能还原代码。

uncompyle6为例进行分析,反编译结果如下:

W2-pyc1

加密逻辑是先对输入取md5得到十六进制摘要,之后仅对数字部分进行rot5,最后套上0xGame{}输出。

直接运行pyc程序即可得到flag,但是需要用3.8版本的python解释器才能正常运行。

W2-pyc2

当然也可以把反编译的代码copy下来运行。

FirstSight-Jar
#

Java源文件编译后生成.class文件,之后打包成可执行的jar包。

针对jar的反编译工具有很多,例如Procyon、CFR、jd、jadx等,一抓一大把,找一款适合的就可以。

以jadx为例:

W2-jar2

encrypt实现了一个简单的仿射密码,只不过字母表改成了十六进制字符。

加密函数的数学形式为 \(E(x) = 5x+3(mod\ 16)\),求解同余式就可以反解出x。注意解密时不能用除法,在模运算下要改用乘法逆元。

import gmpy2
Alphabat = "0123456789abcdef"
enc = "ab50e920-4a97-70d1-b646-cdac5c873376"
inverse = int(gmpy2.invert(5,16))
for i in enc:
    if i not in Alphabat:
        print(i,end="")
    else:
        index = ((Alphabat.index(i) - 3)*inverse)%16
        print(Alphabat[index],end="")

另外值得一提,仿射密码作为单表代换密码,明文和密文之间存在一一映射关系。有不少师傅先计算出了明文字符与密文字符的对应关系,然后逐一替换来解密。这个思路也是可行的。

Xor::Random
#

c++程序,反编译后看起来有一点丑陋,涉及到大量使用的模板,类,对象,还有构造析构函数。

函数名比较长,不过都是标准库的函数,不难根据名字推测函数功能。

W2-random1

主函数大概可以划分成三部分:先检查了flag的格式和长度,之后生成随机数v21,最后一边用v21异或加密一边检查。注意密钥从v21v21+3中交替选择。

重点说一下中间随机数的部分。有两种解法:

  • 解法一:常规静态分析

结合密码学的相关知识,可以知道种子能够控制伪随机数生成。程序首先在init_random函数中设置了种子0x77。后面if语句中又设置了种子0x1919810。到底用了哪个种子呢?

回过头跟踪v6的赋值,查看check(),不难发现这个函数的返回值一定为0,那么v6的值也为0。上图绿色方框内的srand()压根不会执行。由此可以确定正确的种子,进而得到正确的随机数值。等效的简化代码如下:

srand(0x77);
v22 = rand();
v21 = rand();

不同编程语言的伪随机数生成算法存在差异,甚至c标准库在windows和linux系统下的rand实现也不一样。本题的随机数应在windows下调用stdlib.hrand

#include<stdio.h>
#include<stdlib.h>

int main()
{
    unsigned char num,key;
    unsigned char *array;
    unsigned long long v13[4];
    v13[0] = 0x1221164E1F104F0C;
    v13[1] = 0x171F240A4B10244B;
    v13[2] = 0x1A2C5C2108074F09;
    v13[3] = 99338668810000;
    srand(0x77);
    rand();
    num = rand() & 0xff;
    array = (unsigned char*)v13;
    for(int i = 0; i <= 29; i++)
    {
        key = (i % 2 !=0? num: num+3);
        array[i] ^= key;
    }
    printf("0xGame{%s}",array);
    return 0;
}
  • 解法二:动态调试直接拿密钥

由于check()只检查长度不检查内容,而且随机数种子只可能二选一,所以直接在v21最后一次赋值处设置断点。当程序运行到此处时种子一定已经被确定下来,不用关心到底用了哪个种子,我们只需要拿v21这个随机数的具体值即可。

W2-random2

调试可以选择IDA自带的Local Windows debugger调试器,断点处的语句运行后,查看内存中的随机数值。

脚本参考解法一。

ZzZ
#

本题是Visual Studio 2022生成的Debug程序,vs生成程序的一大特征就是把函数名、变量名等符号全部拿出来放到pdb文件中。如果没有符号文件,在静态分析时就看不到这些信息。

无符号的情况下,IDA会自动把函数命名为其起始地址。start代表是整个程序的入口点,与main略有区别。

W2-zzz1

首先要想办法找到main()。不难发现程序运行时会输出enter flag的提示,而main()一定会调用io函数来输出这一字符串。所以可以从字符串入手,通过字符串窗口找到相关的字符串,进行交叉引用,定位到主函数。

W2-zzz2

W2-zzz20

交叉引用快捷键:X 或 ctrl+X

W2-zzz3

之后就成功来到了主函数,反编译后分析加密逻辑:

W2-zzz4

flag内的uuid共有5段,其中首尾两段直接给出,中间三段从字符数组转unsinged int后代入方程组验证。

接下来用Z3 Solver解方程,因为涉及位运算,所以用BitVec这种类型,长度至少为32bit。

有的师傅在解出方程后发现结果有问题,基本上都是在数据类型上出了问题。举例来说:%4s(const char *)v5这个地址写入连续的4个字节,但v5本身是unsigned int数组,所以这4个字节会被按小端序解析成数组的第一个unsigned int元素,之后赋值给v10进行方程的运算。

W2-zzz5

所以解出来的值应该是一个4字节也就是32bit的整数,之后按小端转成字节再转字符串。

from z3 import *
v13 = 3846448765
v14 = 0xD085A85201A4
v10,v11,v12 = BitVecs("v10 v11 v12",32)

s = Solver()
s.add(11 * v11 + 14 * v10 - v12 == 0x48FB41DDD)
s.add(9 * v10 - 3 * v11 + 4 * v12 == 0x2BA692AD7)
s.add(((v12 - v11) >> 1) + (v10 ^ 0x87654321) == 3451779756)
if s.check()==sat:
    result = s.model()
    form = "0xGame{%8x-%4s-%4s-%4s-%12x}"
    args = [result[var].as_long().to_bytes(4,"little").decode() for var in (v10,v11,v12)]
    print(form % (v13,args[0],args[1],args[2],v14))

Week3
#

BabyASM
#

一段比较精简的Intel x86汇编指令,除去数据之外大概50多行,主要考察一些常见汇编指令的功能。

同时也是提醒下不要太过于依赖IDA反编译的伪码,F5虽好但也可能出现反编译错误甚至完全用不了的情况,所以还是有必要掌握汇编的知识。

W3-asm1

jle向回跳转表示循环,程序的两段循环分别在L4L5部分,对应两段加密。

data = [20, 92, 43, 69, 81, 73, 95, 23, 72, 22, 24, 69, 25, 27, 22, 17, 23, 29, 24, 73, 17,
    24, 85, 27, 112, 76, 15, 92, 24, 1, 73, 84, 13, 81, 12, 0, 84, 73, 82, 8, 82, 81, 76, 125]
for i in range(22):
    data[i] += 28
for j in range(22,43,1):
    data[j] ^= data[j-22]
print(bytes(data).decode())

非预期:把文件.txt改成.s后缀,用Linux直接gcc编译链接成32位程序,运行出flag

W3-asm0

LittlePuzzle
#

和上周差不多的java逆向。反编译后看到一个 \(9\times 9\) 的二维数组board,之后分析下面的check函数可以确定是数独的逻辑。把board提取出来整理成数独的格式,如下图:

W3-puz1

数独本身难度不大,可以自行推理求解,也可以找数独求解器一类的工具去解。

最后应该能求出唯一的一个解(下图白色部分)

W3-puz2

通过主函数的代码,能够分析出只需要输入(白色部分的)解即可,程序调用flag函数输出flag。

Tea
#

程序没有符号信息,但是结合上一周的经验,可以通过字符串来定位主函数。

代码量并不多,比较难受的是不能根据函数名推测功能,有耐心的话挨个函数点进去看也是可以的。

好一点的方法是,关注对明文(传入的flag)和密文进行操作的函数,从这些函数入手分析。

W3-Tea1

传入的明文是Buf1,根据memcmp推测绿色框中的数据是密文,中间for循环调用的可以猜到是加密函数,红色框的数据对应密钥。

细心观察可以发现密文被用了两次,除去最后的check,前面还有一次对密文进行了逆序(sub_1400113B6)

接下来分析加密函数,是经典的TEA加密算法,题目中也有提示。算法的具体原理可以自行搜索了解(这里贴一个b站视频【动画密码学】TEA ), 一个很显著的识别特征是0x9E3779B9,也就是题目中的v5 -= 0x61C88647

W3-Tea2

W3-Tea3

最后有个小坑还需要说一下,Buf1unsigned int数组而不是char数组,每两次加密的数据间隔了8*4 = 0x20字节。实际上只加密了flag的最前面8个字节和最后面8个字节。中间的部分没有变化,能够看出来还是可读的十六进制明文。解密只需调用两次tea解密。

(至于为什么for循环中j<5进行了5次加密,其实算是一个干扰项。但是Buf1数组开的空间不够大,导致加密时会越界把栈上的一些无关数据也加密了,可能影响到一些师傅的分析思路,在这里给各位师傅们磕一个)

W3-Tea4

#include<stdio.h>

void tea_decry(unsigned int data[2], unsigned int key[4])
{
    unsigned int d1 = data[0], d2 = data[1];
    unsigned int delta = 0x9e3779b9, number = delta * 32;
    for (int i = 0; i < 32; i++)
    {
        d2 -= ((d1<<4) + key[2]) ^ ((d1>>5) + key[3]) ^ (d1 + number);
        d1 -= ((d2<<4) + key[0]) ^ ((d2>>5) + key[1]) ^ (d2 + number);
        number -= delta;
    }
    data[0] = d1;
    data[1] = d2;
}

void reverse(unsigned char *data,unsigned int len)
{
    unsigned char *pchar = data;
    unsigned char *bchar = data + len - 1;
    unsigned char bufchar;
    while(pchar<bchar)
    {
        bufchar = *pchar;
        *pchar = *bchar;
        *bchar = bufchar;
        pchar++;
        bchar--;
    }
}

int main()
{
    unsigned char data[] = {
        0xC9, 0xB6, 0x5C, 0xCE, 0xF8, 0xEE, 0x8E, 0xA2, 0x33, 0x36, 
        0x34, 0x63, 0x37, 0x32, 0x36, 0x64, 0x38, 0x37, 0x65, 0x33, 
        0x62, 0x33, 0x63, 0x64, 0x36, 0x39, 0x64, 0x34, 0x64, 0x30, 
        0x62, 0x38, 0x2A, 0x7A, 0x7C, 0x3B, 0x85, 0x33, 0x6D, 0xD3};
    reverse(data,40);
    unsigned int key[] = {2,0,2,4};
    unsigned int *u32data = (unsigned int*)data
    tea_decry(&u32data, key);
    tea_decry(&u32data[8], key);
    printf("%s",data);
    return 0;
}

The Matrix
#

本题的矩阵使用了结构体进行存储,由于IDA不能识别自定义的结构体,所以会看到有大量形式相似的赋值语句,其实是结构体元素的初始化。 由于内存对齐的原则,结构体大小为0x10字节,其中element_array的指针占用最后8字节。

struct Matrix_m_n{
    unsigned char m
    unsigned char n,
    unsigned int* element_array
}

程序混淆了几个关键的函数名,不过好在大多数函数的名称是正常的,只需要对这几个函数单独分析即可。

调用了4次hello(data,Matrix)初始化4个key矩阵元素的值。要留意一下这几个key在栈上的顺序,后面加密时会用到。

w3-Mat3

Morpheus(Matrix,length,flag)检查输入flag长度,之后初始化矩阵元素。

w3-Mat1

之后分析下面的加密逻辑,guess(keyMatrix,sourceMatrix,outputMatrix)实现矩阵乘法,也就是key矩阵右乘flag明文的矩阵。

w3-Mat2

根据线性代数知识,\(KA =B\) 这一矩阵方程的解为 \(A = K^{-1}B\)

利用numpy可以比较方便的求出逆矩阵(但是求出来的有点精度误差,需要手动修正下,题目给的key矩阵求逆后元素依然都是整数,所以四舍五入一下),进而还原加密前的矩阵,得到flag。

import numpy as np
k1=[1,3,1,0,2,1,1,2,0]
k2=[1,2,2,3,5,6,0,2,1]
k3=[0,4,3,1,2,1,2,3,1]
k4=[1,2,3,3,5,6,1,4,10]
data = [
    0x1e8,0x1c0,0x181,0x557,0x4d3,0x41e,0x13d,0x111,0x102,
    0x26c,0x140,0x145,0x5b7,0x2ec,0x2f3,0x5e9,0x31d,0x336,
    0x14d,0x10a,0x192,0xbd,0x9f,0xf5,0xbd,0xa1,0x101,
    0x162,0x147,0x223,0xfb,0xc0,0x126,0x191,0x123,0x1b7,
    0xf0,0xfd,0x10d,0x29e,0x2c0,0x2f1,0x91,0x9f,0xa4,
    0x229,0x13b,0x12e,0x4e4,0x2d8,0x2c7,0x5bd,0x325,0x2e4,
    0x1c7,0x151,0xd5,0xfe,0xe5,0x6e,0x12c,0xa0,0x9e]
# 注意key矩阵的顺序
keyMat = [np.array(var).reshape(3,3) for var in (k2,k4,k1,k3)]
dataMat = [np.array(data[i*9:(i+1)*9]).reshape(3,3) for i in range(len(data)//9)]

flag = []
for i in range(7):
    inv = np.linalg.inv(keyMat[i%4])
    arr = np.matmul(inv,dataMat[i]).flatten().tolist()
    if i < 6:
        arr = arr[:6]   # 相邻矩阵的元素重叠
    flag.extend([round(num) for num in arr])
print(bytes(flag).decode())

FirstSight-Android (Mobile)
#

安卓apk反编译,比较好用的工具是GDA和jadx,更推荐后者。

程序位置在com.example.ezandroid,不难发现有一个Base62编码类,主活动调用了其中的编码函数,然后和secret字符串进行比较。

Android程序的各种资源都被保存到xml文件中,字符串也不例外,所以secret字符串的具体内容要去value/string.xml查看。可以在jadx中搜索资源中的文本。

W3-android1

之后拿去解码即可。

W3-android2

Week4
#

PyPro
#

根据题目提示推测本题的程序由python打包而来,先 pyinstaller 解包(这一步也可以参考后面Misc)

解包结果可以看到pyc版本:3.12,很不幸 uncompyle6 不支持反编译3.9版本以上的 pyc,这里可以用 pycdc。( https://github.com/zrax/pycdc,官方版本是源代码,需要手动cmake一下,pycdc可能遇到反编译了一半之后报错罢工的情况,参照 Issue #307 改一下pycdc源码之后重新编译即可解决,当然也可以用pycdas看字节码)

PyLingual这个网站也支持反编译3.12版本,效果还不错。

贴个源码:

import base64
from Crypto.Cipher import AES
from Crypto.Util.number import long_to_bytes

key = 0x554b134a029de539438bd18604bf114

def PKCS5_pad(data):
    if len(data)<48:
        length = 48 - len(data)
        return data.ljust(48,length.to_bytes())
    else:
        return None

def main():
    enc = input("在这里输入你的flag:\n").encode("utf-8")
    if len(enc)!=44:
        print("length error!")
        for i in range(len(enc)):
            exit(123)
    elif enc[6] != ord("{") or enc[-1] != ord("}"):
        print("format error")
        exit(1)
    chiper = AES.new(long_to_bytes(key),AES.MODE_ECB)
    enc = chiper.encrypt(PKCS5_pad(enc))
    result = base64.b64encode(enc)
    data = '2e8Ugcv8lKVhL3gkv3grJGNE3UqkjlvKqCgJSGRNHHEk98Kd0wv6s60GpAUsU+8Q'
    if result.decode() == data:
        print("flag正确")
    else:
        print("错误")
        return None

if __name__ == "__main__":
    main()

先AES之后Base64,解密就不难了。从反编译结果直接抄点代码下来,或者Cyberchef一把梭。

import base64
from Crypto.Cipher import AES
from Crypto.Util.number import long_to_bytes

key = 0x554b134a029de539438bd18604bf114
data = '2e8Ugcv8lKVhL3gkv3grJGNE3UqkjlvKqCgJSGRNHHEk98Kd0wv6s60GpAUsU+8Q'
enc = base64.b64decode(data)
chiper = AES.new(long_to_bytes(key),AES.MODE_ECB)
flag = chiper.decrypt(enc)
print(flag)

MineSweeper
#

Unity游戏题,选择C#反编译工具DnSpy或者ILSpy。一般情况下(没有IL2CPP),游戏代码编译后保存在Assembly-CSharp.dll文件中,预期是对这个文件进行逆向。

题目解法比较开放,有师傅直接硬扫70个雷通关的,但是难度太大不推荐硬玩。

先说常规思路:静态分析

  1. 找到Game类,分析Update函数,发现通关后会调用crypt函数解密出flag然后输出。
  2. 继续分析crypt,函数导入了Resources/enc.bytes的前44字节作为密文。这个文件用AssetStudio可以导出,之后用十六进制编辑器查看。
  3. 接下来的解密部分有点类似RC4,Game.key先打乱传入的参数Key也就是Game.haha,得到的结果作为真正的密钥进行异或。
  4. 仿照crypt函数搓个解密脚本。

更推荐的方法:patch修改游戏逻辑。DnSpy中可以编辑反编译的代码,只要想办法触发通关条件就能拿flag,修改后记得重新编译运行。

  • 最直接的改法,把开始时的雷数改成0,点两下就通关。或者this.gamewin直接改成true

W4-mine1

  • wincheck函数中修改语句win = true
  • Update函数对this.gamewin的判断,对条件取反。

当然改法还有很多,甚至可以重写整个函数,在刚进入游戏的时候就弹出flag。

W4-mine2

W4-mine3

Register
#

一个简易的crackme,需要逆向破解序列号保护。

打开运行,发现要输入用户名和对应的序列号。用户名只能是题目描述中提到的0xGameUser,否则警告用户名错误,这也是为了避免多解。

使用DIE进一步查信息,32位GUI

W4-register1

运行时注意观察字符串信息,之后在IDA通过字符串定位关键函数

  • 如果交叉引用主窗口标题 “0xCrackMe”,定位到 WinMain,之后找回调函数 lpfnWndProc
  • 交叉引用子窗口的相关字符串直接定位到回调函数 sub_411B10

在整个逆向过程中会碰到很多windows API函数,可以去Microsoft官方文档搜索查询。

W4-register3

进入sub_4112DA继续分析,v9这里是对时间戳右移28位,结合动态调试也能发现v9是一个固定值。之后先对用户名异或,再调用sub_1011041生成SHA1哈希值。

W4-register5

SHA1的十六进制值套上0xGame{}就是flag

W4-register6

W4-register7

Tea2.0
#

同样是32位程序,反编译后定位主函数。代码只有短短几行,逻辑还是比较清晰的。

W4-tea1

sub_41116D是加密函数,分析一下发现和上周的TEA很像,通过网络搜索或者问AI能够分析出是XTEA加密。不过要注意轮数改成了64轮。

之后按常规思路就是拿unk_41A020密文进行解密,不出意外会得到一串乱码)

第四周的题目当然不会白送分。为了找到问题出在哪里,借助动态调试对加密过程和密文密钥进行检查,最后发现密文和静态分析时看到的硬编码的数据不同。把这段新的密文dump下来拿去解密才会得到flag。

如果纯静态做要麻烦的多。交叉引用unk_41A020,可以看到TlsCallback_1_0函数修改了密文(注意Tlscallback部分的代码先于主函数执行),具体原理是对硬编码的密文进行了一次TEA加密,其密钥又经过TlsCallback_0_0修改。理清这些后可以写出如下脚本:

#include<stdio.h>

void tea_encry(unsigned int* data, unsigned int* key)
{
    unsigned int d1 = data[0], d2 = data[1];
    unsigned int delta = 0x9e3779b9, sum = 0;
    for (int i = 0; i < 32; i++)
    {
        sum += delta;
        d1 += ((d2 << 4) + key[0]) ^ ((d2 >> 5) + key[1]) ^ (d2 + sum);
        d2 += ((d1 << 4) + key[2]) ^ ((d1 >> 5) + key[3]) ^ (d1 + sum);
    }
    data[0] = d1;
    data[1] = d2;
}

void xtea_decry(unsigned int *data, unsigned int *key)
{
    int round = 64;
    unsigned int d1 = data[0], d2 = data[1];
    unsigned int delta = 0x9e3779b9, number = delta * round;
    for (int i = 0; i < round; i++)
    {
        d2 -= ( ((d1<<4) ^ (d1>>5)) + d1) ^ (number + key[(number>>11) & 3]);
        number -= delta;
        d1 -= ( ((d2<<4) ^ (d2>>5)) + d2) ^ (number + key[number & 3]);
    }
    data[0] = d1;
    data[1] = d2;
}

int main()
{
    unsigned int data[] ={
        0x18dc360,0xd5835457,0x8bee2dcb,0x92bb2dee,0xfdf4ad54,0x43f8c2d,
        0x61a232a9,0xf15f4d1,0x16ea4979,0x7c2bf6da,0xdcd5fa32,0x76450819 };
    unsigned int TEAkey[] = { 0x1245,0x3298,0x4756,0x1463 };
    unsigned int XTEAkey[] = { 0x4512,0x9832,0x5647,0x6314 };
    for (int i = 0; i < 4; i++) {
        TEAkey[i] ^= 0xABCD;
    }
    for(int j = 0; j < 6; j++)
    {
        tea_encry(data+2*j, TEAkey);
    }
    for (int k = 0; k < 6; k++)
    {
        xtea_decry(data+2*k, XTEAkey);
    }
    printf("%s",(char*)data);
    return 0;
}

JustSoSo (Mobile)
#

jadx反编译后能看到Java层有一个ReversC4类,简单分析下可以看出是修改过的RC4加密。只是在生成密钥流(KSA)的过程中把初始的S-box倒序排列了一下。

W4-so3

之后来到MainActivity,发现加载了外部库secret,并且声明了native层的方法getKey()

W4-so1

后面调用了ReversC4.encrypt(flagInput.getBytes(), getKey()),由此可以推测RC4的密钥要从getKey()得到。这个函数的实现并不在java层,而是之前提到的secret库中。也就是lib 目录下的 libsecret.so 文件。

W4-so2

分析so动态库,armx86/64架构任选其一分析即可,后者相对简单一些。IDA打开后一眼看到要找的函数Java_com_ctf_justsoso_MainActivity_getKey

W4-so4

这里的xmm是Intel SSE指令集的寄存器,可以存放128bit的数据类型__m128i,事实上这个数据类型是由若干个基本类型打包而来。比如本题的_mm_cvtepu8_epi32函数先把4个uint8拓展为4个uint32(高位用0填充),再把这4个uint32打包成一个m128ixmmword也是一样的道理,做题时完全可以把它当成长度为4的uint数组。

理解了这一点再去看伪代码就比较清楚了,source中每个字符乘2再异或0x7F得到密钥,之后回到java层RC4解密即可。

相关文章

0xGame2023 Reverse
·591 字·3 分钟
CTF Reverse
reverse writeup of 0xgame2023
HGAME2024 Revserse
·1327 字·7 分钟
CTF Reverse
reverse writeup of Hgame2024
0xGame2023 Crypto
·1338 字·7 分钟
CTF Crypto
crypto writeup of 0xgame2023