跳至主要内容

0xGame 2025 Week2 Reverse WP

引言

  • 我是一名CTF萌新啊,做这么详细的WP,也是希望能够帮助像我一样的新手,如果大佬发现WP有问题可以评论,我会改进!

16 bit

  • 这个官方文档写的比较详细,那我就仔细的讲一下过程
  • 运行这个exe会发现运行不了,据官方WP所说,这个是16位的应⽤程序,是由汇编语⾔直接编写再编译的,现代操作系统⼀般都无法直接运行,但它还是⼆进制⽂件,ida可以分析,不过F5⼀键出伪代码就没法⽤了

方法1:观看反汇编

  • 可以把汇编代码复制给ai,然后问一下它这段代码写的啥,这里就结合ai进行解释了

  • IDA不会用的可以看我的Week1

  • 第1行:mov ax, seg dseg

    • seg dseg
      • dseg 是程序中定义的一个数据段的名称。
      • seg 是一个操作符,它获取 dseg 这个段的段地址(段基址)。
      • 在实模式下,内存地址由 段地址:偏移地址 组成。
    • mov ax, seg dseg
      • 将数据段 dseg 的段地址加载到 AX 寄存器中。
      • AX 在这里作为临时中转寄存器。
  • 第2行:mov ds, ax

    • mov ds, ax
      • 将 AX 寄存器中的值(即 dseg 的段地址)移动到 DS 寄存器
      • DS 是数据段寄存器,它定义了程序中数据访问的默认段。
  • 为什么不能直接 mov ds, seg dseg

    • 在 x86 架构中,大多数指令不允许将立即数直接移动到段寄存器。
    • 必须通过通用寄存器(如 AX)进行中转。
  • 第3行:assume ds:dseg

    • assume
      • 这是一个汇编器伪指令,不是 CPU 指令。
      • 不生成任何机器代码,只是给汇编器提供信息。
  • 其实这一段不用看,因为下面又给覆盖了 :(

  • 只是把sub和xor的位置颠倒了,所以这段和后面那段就一块讲了

  • sub是减法,xor是异或

  • cx是循环计数器,mov是赋值语法,0x17 = 23,也就是循环23次

  • loc_1000E:后面就是循环结构

1
2
3
4
5
6
7
8
loc_1000E:
mov al, [si+0Ah] ; 从 [SI+0x0A] 加载一个字节到 AL
sub al, 9 ; AL = AL - 9
xor al, 0Eh ; AL = AL XOR 0x0E
mov [di+38h], al ; 将结果存储到 [DI+0x38]
inc si ; SI++
inc di ; DI++
loop loc_1000E ; CX--, 如果CX≠0则继续循环
  • 再往下看
1
2
3
4
mov     byte_100A6, 24h ; '$'  ; 在地址 66h 写入 '$' 终止符
mov dx, 67h ; 设置字符串起始地址为 67h
mov ah, 9 ; 设置功能号:输出字符串
int 21h ; 调用 DOS 输出
  • 下面那段也类似就不解释了,什么,你问我这个地址在哪看?把鼠标滚轮往下滚,你会得知答案
  • 然后你已经看懂了汇编,就可以试着写一写python脚本来推算出flag,我不是很会,就用ai生成脚本了
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
# decode_flag.py
# 从汇编逻辑还原 flag

data = [
0x47,0x7F,0x52,0x78,0x6C,0x74,0x7E,0x72,0x47,0x47,0x73,0x5A,0x84,0x5A,0x43,0x85,0x46,0x5A,0x83,0x6F,0x46,0x5A,0x6C,
0x33,0x30,0x73,0x32,0x75,0x66,0x37,0x61,0x66,0x33,0x30,0x78,0x66,0x40,0x35,0x61,0x4E,0x64,0x34,0x65,0x32,0x33,0x88
]

out = []
# 第一个循环(前 23 字节):先 sub 9,再 xor 0x0E
for i in range(23):
al = data[i]
al = (al - 9) & 0xFF
al = al ^ 0x0E
out.append(al)

# 第二个循环(后 23 字节):先 xor 0x0E,再 sub 9
for i in range(23, 46):
al = data[i]
al = al ^ 0x0E
al = (al - 9) & 0xFF
out.append(al)

flag = bytes(out).decode('latin1')
print(flag)
  • 另外提一嘴,国内的ai真的不太好用,写脚本都是错的:p

方法2:配置Dosbox运行

  • 不用看其他人配置Dosbox了,太麻烦,看我的精简版
  • DOSBox, an x86 emulator with DOS下载
  • 进去之后啥也不用点,等几秒就行,别点那老鼻子广告都是假的下载按钮
  • 下载好之后,就随便安装个地方,我就安装在z盘了
  • 然后打开Dosbox,你会发现不管你安装在哪个磁盘,默认进去就是z:/,没有关系,那只是他内部的磁盘罢了,利用mount c 你放exe文件夹的路径,来挂起这个文件夹当做内部磁盘c盘,如果你嫌麻烦,可以点击这个
  • 在最下面填上
  • 然后进入Dosbox,输入16bit.exe就行了

BabyJar

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import base64  

key = 0x14

def decrypt(base64_text: str) -> str:
decoded_bytes = base64.b64decode(base64_text)
decrypted = bytearray()
for c in decoded_bytes:
temp = ((c & 0xF0) >> 4) | ((c & 0x0F) << 4)
decrypted.append(temp ^ key)
return decrypted.decode("latin1")

ciphertext = "QsY1V5cX9jJyF2JSAgdikwfCEneTAgICUpNnd1Iyk8IXUkJ3QhcyZ8J3YpY="
print(decrypt(ciphertext))

TELF

  • 依旧查壳,可以看Week1,由于大部分说过,所以省略些部分
  • 可以看到有个UPX壳,我们去脱,发现脱不了壳
  • 官方WP写的很清楚啊,我们去下个Winhex,吾爱破解就有,自行下载
  • upx执行脱壳逻辑的时候,会先识别⼀下⽂件内部的upx标识,也就是”upx”这个字符串,upx只有识别到了它才能认识这个upx程序,从而执行脱壳。
  • 反之,如果⼈为地修改这个upx标识,让upx⽆法识别出来,那么脱壳就会失败,这道题就是这样。只要把被修改的字符串改回去就⾏了
  • 改一下
  • 就成功了
  • 丢到IDA,查看c语言伪代码
  • 可以注意到,密钥是动态生成的,若需要得到密钥则需要动态调试,但本题是elf文件,动态调试需要远程挂linux(具体操作可以自行搜索学习)
  • 有的师傅可能做过相关题⽬,觉得动态调试有点麻烦,因此直接在本地windows的编译器⾥⾯设置相同的种⼦,然后模拟出随机数列得到密钥,最后发现密钥不对
  • 这是因为windows和linux两个系统对于随机数的⽣成逻辑不太⼀样,这个程序是在linux中运⾏的,所以在windows系统⽆法模拟出来正确的密钥顺序,必须要在linux中运⾏才可以得到正确的密钥
  • 上面是我引用的官方的WP,很高深,学习到了:>
  • 这里引用官方WP所给的脚本以及源代码
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#include <stdio.h>  
#include <stdint.h>
#include <string.h>
#define DELTA 0x9e3779b9

void decrypt(uint32_t *v, uint32_t *k) {
uint32_t v0 = v[0], v1 = v[1];

uint32_t sum = DELTA * 32;

for (int i = 0; i < 32; i++) {
v1 -= ((v0 << 4) + k[2]) ^ (v0 + sum) ^ ((v0 >> 5) + k[3]);

v0 -= ((v1 << 4) + k[0]) ^ (v1 + sum) ^ ((v1 >> 5) + k[1]);

sum -= DELTA;
}

v[0] = v0;

v[1] = v1;
}

int main() {
uint32_t key[4] = {0x7E4D087B, 0x7A4DB733, 0x70FE9DF0, 0x595607F7};

uint8_t enc[56] = {

0xAD, 0xDA, 0x01, 0xDC, 0xAE, 0x5B, 0x8A, 0x08,

0x4E, 0xF5, 0x4F, 0x8F, 0x6E, 0x5F, 0x9D, 0x9E,

0x0A, 0x4E, 0xA9, 0x08, 0x25, 0xAB, 0x45, 0xC2,

0x4B, 0xC9, 0x8F, 0x43, 0x3D, 0x51, 0xD6, 0x28,

0xF6, 0x72, 0xCD, 0xF4, 0x2B, 0xB4, 0x4A, 0x3B,

0xFB, 0x36, 0x66, 0xEF, 0xD6, 0x8A, 0x8C, 0xB2,

0xEB, 0x1A, 0x9C, 0x1B, 0x0A, 0x9C, 0x1F, 0x53

};

for (int i = 0; i < 56; i += 8) {
uint32_t v[2];

memcpy(&v, enc + i, 8);

decrypt(v, key);

memcpy(enc + i, &v, 8);
}

for (int i = 0; i < 56; i++) {
printf("%c", enc[i]);
}

return 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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#include <stdio.h>  
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#define DELTA 0x9e3779b9
void encrypt(uint32_t* v, uint32_t* k) {
uint32_t v0 = v[0], v1 = v[1];
uint32_t sum = 0;
for (int i = 0; i < 32; i++) {
sum += DELTA;
v0 += ((v1 << 4) + k[0]) ^ (v1 + sum) ^ ((v1 >> 5) + k[1]);
v1 += ((v0 << 4) + k[2]) ^ (v0 + sum) ^ ((v0 >> 5) + k[3]);
}
v[0] = v0;
v[1] = v1;
}
uint8_t enc[56] = {
0xAD, 0xDA, 0x01, 0xDC, 0xAE, 0x5B, 0x8A, 0x08,
0x4E, 0xF5, 0x4F, 0x8F, 0x6E, 0x5F, 0x9D, 0x9E,
0x0A, 0x4E, 0xA9, 0x08, 0x25, 0xAB, 0x45, 0xC2,
0x4B, 0xC9, 0x8F, 0x43, 0x3D, 0x51, 0xD6, 0x28,
0xF6, 0x72, 0xCD, 0xF4, 0x2B, 0xB4, 0x4A, 0x3B,
0xFB, 0x36, 0x66, 0xEF, 0xD6, 0x8A, 0x8C, 0xB2,
0xEB, 0x1A, 0x9C, 0x1B, 0x0A, 0x9C, 0x1F, 0x53
};
int main() {
srand(1010000);
uint32_t key[4];
for (int i = 0; i < 4; i++) {
key[i] = (uint32_t)rand();
}
char input[57];
printf("Please input your flag: ");
if (scanf("%56s", input) != 1) {
fprintf(stderr, "Input error or EOF\n");
return 1;
}
if (strlen(input) != 56) {
printf("Length Error!\n");
return 1;
}
uint8_t input_buf[56] = {0};
memcpy(input_buf, input, 56);
for (int i = 0; i < 56; i += 8) {
uint32_t v[2];
memcpy(&v, input_buf + i, 8);
encrypt(v, key);
memcpy(input_buf + i, &v, 8);
}
for (int i = 0; i < 56; i++)
{
if (enc[i]!=input_buf[i])
{
printf("Try Again!");
return 1;
}
}
printf("Congratulation!\n");
return 0;
}
  • 最后结果为

算术高手

  • 把它丢进DIE中,可以看到它是pyinstaller打包的exe
  • PyInstaller 是⼀个把 Python 脚本及其依赖打包成独立可执行文件(如 .exe),方便分发和运行的工具
  • 也就是说这个exe文件里是一堆python代码和依赖文件,不是传统的二进制文件,所以说ida无法分析这个exe文件
  • https://github.com/extremecoders-re/pyinstxtractor
  • 下载这个工具,分离那些文件
  • 下载uncompyle6
  • pip install uncompyle6
  • uncompyle6 -o . yourfile.pyc
  • 这个.是生成文件在根目录
  • 在生成的文件夹下打开cmd
  • 然后就能找到.py
  • 看到flag,并且这道题认真计算做的话也是得不到flag的,可以看看源代码

Shuffle!Shuffle!

  • 直接丢进IDA,看下c语言伪代码
  • 这两个地方可以双击点开,esc返回,看下函数
  • 当你把那些信息喂给ai,它给你的脚本是几乎得不到答案的
  • 看看这个官方WP的脚本吧
  • ⽤了随机数打乱,随机种⼦固定,所以可以写代码模拟乱序的序列,然后恢复字符串的正确顺序,但实际上更推荐另⼀种方法:动态调试⼀个固定长度字符全不同的字符串,得到其加密结果,就能还原任意密文。
  • 简单来说,就是我们可以先随便输入⼀个没有重复字符且长度和flag相等的字符串,得到它的乱序结果,然后和被乱序的flag进行对比,就能得到正确的flag
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
32
33
enc='23-64bed6}-xm5300-{faGa34-0e04c2e7c2a78f39a4' # 待恢复的密⽂字符串

test='kL9f2hEwR0xB8YpQvNjOtCz1Dg5sV3UaH4MbrX7iAqS+' # 明⽂字符串

test2='Nbgz45vH3+UL2wMj8tE0x97DCQphksVAa1XqfiSRYBrO'

# 被乱序的明⽂字符串,将明⽂字符串输⼊程序运⾏后的结果,可以动态调试抓取

swap=[[0]*44,

[0]*44]

for i in range(len(test)):

for j in range(len(test2)):

if test[i]==test2[j]:

swap[0][i]=i

swap[1][i]=j

break

flag=""

print(swap)

for i in range(len(enc)):

flag+=enc[swap[1][i]]

print(flag)

总结

  • 这Reverse的题,我如果不看WP可能也只会做出来BabyJar那道题,有些东西确实是看不懂,只能慢慢学了,很多思路和脚本也不是很理解
  • 本WP是基于官方WP更为详细的版本,只是我复现的过程,希望能帮助到你
  • 题目需要到https://www.ctfplus.cn/自取

关于本文

由 GuQing 撰写,采用 CC BY-NC 4.0 许可协议。

#Reverse_Writeup