第一届“数信杯” 初赛WriteUp

2024/4/14 Sunday

drinktea

下载题目附件out.txt,打开发现是python字节码。

  6           0 LOAD_GLOBAL              0 (c_uint32)
              2 LOAD_FAST                0 (v)
              4 LOAD_CONST               1 (0)
              6 BINARY_SUBSCR
              8 CALL_FUNCTION            1
             10 LOAD_GLOBAL              0 (c_uint32)
             12 LOAD_FAST                0 (v)
             14 LOAD_CONST               2 (1)
             16 BINARY_SUBSCR
             18 CALL_FUNCTION            1
             20 ROT_TWO
             22 STORE_FAST               2 (v0)
             24 STORE_FAST               3 (v1)

 ...
 ...

 33     >>  202 LOAD_GLOBAL              8 (print)
            204 LOAD_CONST              17 ('no~')
            206 CALL_FUNCTION            1
            208 POP_TOP
            210 LOAD_CONST               0 (None)
            212 RETURN_VALUE

可以使用dis 模块,可以将python代码转化成字节码,所以对于题目给到的out.txt,可以推出源代码:

以上代码顺序有一定调整。

可以观察到,题目给出了 arrkey 的值,根据加密函数 encrypt() ,可以知道:

  • arr - 加密后的密文序列

  • key - 密钥

整个加密步骤为:

  • 将明文 flag 以每8个字符为一组,unpack 后并入到 encry 列表中。

  • 将 `encry` 列表以每两个元素为一组,调用 encrypt 函数进行加密。

  • `encrypt` 函数进行了32轮操作,其中要注意的是 total 值累加了32轮。

  • 最后将输出的结果并入到 encrypted 列表中。

  • 判断 encrypted 是否与 arr 相等,相等则输出 “yes~”,不相等则输出“no~”。

针对以上加密步骤,构造解密函数:

  • 将每组参数(2个)和 key 作为参数传入解密函数,解密函数先计算 total * 32, 因为total在加密时累加了32轮。

  • 接着进行32轮解密,用 “-=”来匹配加密的时候的“+=”。

  • 将输出的每组结果 pack后拼接并输出。

下面演示执行结果:

Justsoso

打开后直接乱输密码登录,提示只能本地IP登录

加上xff头绕过

Username加上单引号发现无返回内容,可能存在sql注入

使用#闭合发现正常返回,确定存在sql注入

尝试使用万能密码登录,失败

可能存在过滤,经过尝试可使用admin'oorr'1'='1'%23绕过,and同理可替换成anandd,但还是无法登录

构造盲注语句

编写脚本过程中发现可能存在过滤,导致表名读取失败,经过以下测试发现select被过滤,同样,使用双写绕过

编写脚本如下:

查出密码90440ad8ff884788ed99747acb0872c0

可能是md5,使用somd5解出为yingyingying

设置好请求头后登录

给了个链接

尝试目录穿越发现被检测

Dirsearch目录扫描结果

直接访问flag.php,无法访问,加xff头也无法绕过

尝试使用该链接读取

后面拼接了.jpg,使用?绕过,没有报错

抓包查看,进行base64解码得到flag

MagicAudio

这道题比赛的时候中途放弃了没做,当时尝试了一般的音频解法,没有解出来,于是有了可能是 sstv 的猜测。赛后补做了一下,做出来了。

下载看到 ctf.wav ,应该就是音频题目了。打开听声音是很刺耳的滴滴声,并且声音长短不明显,大概率也不是摩斯电码。

把这个文件丢进 HxD (任何一款十六进制编辑工具都行),可以看到是该文件其实是两个文件拼接的结果,并且能看到是一个 wav 音频文件 和 一个 ZIP 文件进行了拼接。

可以在HxD中看到两个文件拼接

在 Ubuntu 中,使用 foremost 进行分离(用十六进制编辑工具手动分离也可以)。

可以看到输出文件夹 output 中,存在两个被分离的文件夹:

wav和zip被分离了

打开 zip,里面有个 flag.txt ,但是 ZIP 包被真实加密了,需要解密密码。

这里使用 rx-sstv 工具:

用MMSSTV也可以:

还需要下载一个虚拟声卡驱动,让电脑的声音能直接作为输入。

播放源
接收源

然后播放wav文件,看到以下图片:

菜就多练

既然只有这个提示,就作为压缩包密码去打开 flag.txt,成功:

flag.txt的内容

easyjava

题目附件是一个 .hprof 文件,丢进 VisualVM 里看,找 Object;

选择Objects后,可以看到一个Test

点 Fields 看一下它的内容:

题目提示了百家姓,在这里找到了confuse后的码表和密文

搜索百家姓加解密工具;

把码表丢进去,解密出来:

看到少了A和Z,进行补全:

这里补全的原因是:如果不进行补全,密文最后一个字符“鲍”将没办法对应确切的字符。

然后对应密文解密:

接着用 CipherChef ,拿去进行base64解码,但是要注意的是:码表已经变换了,不是原本的码表了。

原本的 Alphabet 是 A-Za-z0-9+/=

rrrrcccc

这道题也没做出来,太菜了。没怎么碰过 Reverse,上来就开逆,连壳也不查了。正常步骤应该先查一下壳:

看看能不能直接脱壳。

提示 "file is modified/hacked/protected" ,手动改。

把 EXE 文件丢进 HxD(或者随便一款 HEX 编辑器),看到 UPX 被改成了小写,搜索都改成大写(共四处),再尝试脱壳。

脱壳结束后,直接丢到 IDA 里分析。

首先来看 main 函数:

先不管变量声明,来看看涉及的主要步骤:

  1. v3 = alloca(16); 这里在栈上动态分配 0x10 个字节的空间,未初始化;

  2. 调用 __main() 函数;

  3. VirtualProtect(LPVOID lpAddress, SIZE_T dwSize, DWORD flNewProtect, DWORD flOldProtect) 用于更改调用进程的虚拟地址空间中已提交页面区域的保护方式。如果没有设置成功就会 exit(1)

我们可以通过 Microsoft 提供的 win32 API 文档:https://learn.microsoft.com/zh-cn/windows/win32/api/memoryapi/nf-memoryapi-virtualprotectarrow-up-right

  • LPVOID lpAddress - 目标内存起始位置

  • SIZE_T dwSize - 从起始位置开始需要变更保护方式的内存分页的区域大小

  • DWORD flNewProtect - 设置新的保护方式

  • DWORD flOldProtect - 保存旧的保护方式

如果该函数成功,则返回值为非零值。

如果函数失败,则返回值为零。

  • 因此,对于第一句 VirtualProtect() 而言:

    • 它将对 &loc_401485 这个代码段上的地址起始的 0x46A 个字节进行保护方式的变更;

    • 我们可以看到其新的保护方式是 0x4u (也就是常量 4 )。通过 MS 的文档查看内存保护常量都有哪些:https://learn.microsoft.com/zh-cn/windows/win32/Memory/memory-protection-constantsarrow-up-right ,对应的保护方式是 PAGE_READWRITE ,对其描述是:启用对已提交页面区域的只读或读/写访问。 如果启用了 数据执行保护arrow-up-right (DEP),则尝试在提交的区域中执行代码会导致访问冲突。

    • 然后将旧的保护方式存放到变量 flOldProtect 中;

  • 第二句同理,对 byte_401301 起始的 0x14E 个字节进行保护方式的变更,变更为 PAGE_READWRITE ,并将旧的保护方式保存到变量 v5 中;

  1. 对于两个 for 循环而言,我们逐步分析。第一个 for 循环,共执行了768次,对于 *((_BYTE *)&loc_401485 + i) ^= 0x20u; ,其对loc_401485 上的 768 个字节各做了一次异或操作,异或的值为 0x20 ;

&loc_401485 就是 0x401485 这个地址,(_BYTE *)&loc_401485 获取了一个指向改地址的 byte 类型的指针,(_BYTE *)&loc_401485 + i 获取了该地址后的第 i 个字节的地址,最外层的*(...) 获取地址上的值。

  1. 同理,对于 byte_401301 而言,该数组对 0x401301作为起始地址的后 334 个字节(包括初始地址)进行了异或操作;

  2. 调用 request_input() 函数(这个是我自己 rename 的);

  3. ((void (*)(void))loc_401485)(); 这条指令将地址 0x401485 转换为指向一个无参数且没有返回值的函数的指针,并且对这个函数进行调用;

void (*)(void) 表示一个无参数且无返回值的函数指针,这里的前一个 void 表示函数没有返回值,后一个 (void) 表示该函数没有参数。使用(void (*)(void)) 对地址 0x401485 进行类型转换,并在最后使用括号 (...)() 对处于该地址的函数进行调用;

  1. 调用 print_str() 函数(这个是我自己rename 的);

针对上述过程,不难发现:

  • 主函数的两个 iffor 的内容,是一种对局部代码段进行解密的操作,这种操作是被称为 SMC (Software-based Memory Encryption) 的一部分。使用VirtualProtect() 更改需要解密的内存区间的保护方式为读写模式,然后进行异或解密。

基于软件的内存加密通常通过以下步骤实现:

  1. 加密算法:选择合适的对称加密算法(如AES)来加密和解密内存中的数据。

  2. 密钥管理:安全地生成、存储和管理加密密钥。

  3. 运行时加密:在数据被写入内存之前,动态地加密数据;在数据从内存被读取时,动态地解密。

  4. 性能优化:由于加密和解密操作会增加额外的处理负担,因此需要优化算法和实现,以减少性能损失。

  • request_input() 中存在一个反调试函数anti_debug() (也是我自己命名的)。这个函数会校验NtGlobalFlag 是否等于 0x70,在调试的时候进行 opcode 绕过就行(当然直接force jump强制跳,反正不要执行 mov [ebp+var_8], 1指令)。

jmp 绕过或者改cmp的值
  • 在执行((void (*)(void))loc_401485)(); 前,先到该地址 (0x401485) ,执行 U (undefine) -> C (code) -> P (Make Procedure) 来把解密后的代码定义为一个函数。

这里可以看到RC4的核心代码。

其实分析到这里就知道应该怎么做了:观察到两个for循环,Str 进行了两次异或,最后异或的结果和字符串 Whatareyourencryption&decryptionbasics 进行 compare。不难想到这个进行两次异或的就是明文的flag 了。

不过这里更想说的是流程:

  • 在第一个 for 循环里,(前面的代码就省略了)v4 是输入数据的长度,iv10 用于生成伪随机置换的值,而 v12 则是关键的 S-Box 数组,提供置换用的密钥流。所以大致流程就是:① 初始化生成伪随机值 -> ② swap置换 -> ③ 密钥流与数据进行异或;

  • 第二个 for 循环就更加只管了,直接数组元素和数据进行异或;

所以我们接着调试,拿到两个for循环的参与异或的数据,然后把字符串异或回去就能拿到flag了。

Last updated