2015 Challenge 3
0x00 查壳与环境识别
使用 DIE (Detect It Easy) 进行初步分析:

分析结果显示程序是由 PyInstaller 打包的。
0x01 解包与反编译
1. PyInstaller 解包
使用 pyinstxtractor.py 对目标文件 elfie.exe (或 elfie) 进行解包:
1
|
python pyinstxtractor.py elfie
|
解包后得到 elfie_extracted 文件夹。
2. 字节码分析
进入解包目录,发现存在 .pyc 文件。
在尝试使用反编译工具失败后,发现可以直接查看 .pyc 文件内容。观察到文件头魔数 03 f3 0d 0a 以及目录下的 python27.dll,确认这是 Python 2.7 环境。
直接用文本编辑器查看 .pyc 文件,可以清晰看到 base64、b64decode 等关键字及大量混淆变量。这是 Python 2.7 字节码的一个特征:字符串(包括变量名)以未压缩的 ASCII 形式存储。
反编译后的源码(或直接观察)显示程序使用了典型的 exec(base64.b64decode(...)) 混淆方式:
1
2
3
4
5
|
O0OO0OO00000OOOO0OOOOO0O00O0O0O0 = 'IRGppV0FJM3BRRlNwWGhNNG'
# ... 大量混淆变量定义 ...
O00OO00OOO0OOOO0OOOO0OO00000OOO0 += 'nNocTB'
import base64
exec(base64.b64decode(OOO0OOOOOOOO0000O000O00O0OOOO00O + ...))
|
0x02 去混淆
为了获取被执行的真实代码,我们不需要手动解密,只需“借力打力”。编写脚本模拟 Loader 的变量拼接过程,但将最后的 exec(...) 替换为文件写入操作。
解包脚本逻辑:
1
2
3
4
5
6
7
8
9
10
11
|
# ... (复制原始文件中的所有变量定义) ...
import base64
# 原始代码: exec(base64.b64decode(...))
# 修改为写入文件:
decoded_code = base64.b64decode(OOO0OOOOOOOO0000O000O00O0OOOO00O + ...)
with open(r'elfie.py', 'wb') as f:
f.write(decoded_code)
print('Decoded stage 2 saved.')
|
提取出的代码部分如图所示:

0x03 逻辑分析
提取出的 elfie.py 是一个基于 PySide (Qt) 的图形界面应用程序。
运行效果:

代码中使用了大量的 getattr 和字符串切片(如 'txeTnialPot'[::-1] 即 toPlainText)来隐藏 API 调用,增加了静态分析的难度。
核心验证逻辑:
程序定义了一个继承自 QtGui.QLineEdit 的类,并在 keyPressEvent 中监听回车键。当用户按下回车时,触发验证函数 O000OOOOOO0OOOO00000OO0O0O000OO0。
1
2
3
4
5
6
7
8
|
def O000OOOOOO0OOOO00000OO0O0O000OO0(self):
# 获取用户输入: getattr(self, 'toPlainText')()
O0O0O0000OOO000O00000OOO000OO000 = getattr(self, 'txeTnialPot'[::-1])()
# 核心比对逻辑
if (O0O0O0000OOO000O00000OOO000OO000 == ''.join((OO00O00OOOO00OO000O00OO0OOOO0000 for OO00O00OOOO00OO000O00OO0OOOO0000 in reversed('moc.no-eralf@OOOOY.sev0000L.eiflE')))):
self.OO0O0O0O0OO0OO00000OO00O0O0000O0.setWindowTitle('!sseccus taerg'[::-1])
# ... 显示成功状态 ...
|
0x04 Flag 获取
分析上述比对逻辑:
reversed('moc.no-eralf@OOOOY.sev0000L.eiflE') 会将字符串反转。
直接反转硬编码字符串即可得到 Flag:
1
|
Elfie.L0000ves.YOOOO@flare-on.com
|
0x05 附录:现代环境运行兼容性
直接提取的代码依赖于老旧的 PySide (Qt4) 库。在现代 Python 3 环境中,通常使用 PySide2 (Qt5) 或 PySide6 (Qt6)。
为了让提取的代码在现代环境中运行,可以使用以下垫片代码(Shim)进行兼容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
try:
from PySide import QtGui, QtCore
except ImportError:
try:
from PySide2 import QtGui, QtCore, QtWidgets
# 将 QtWidgets 中的属性映射回 QtGui,以兼容旧代码
for name in dir(QtWidgets):
if not hasattr(QtGui, name):
setattr(QtGui, name, getattr(QtWidgets, name))
except ImportError:
from PySide6 import QtGui, QtCore, QtWidgets
for name in dir(QtWidgets):
if not hasattr(QtGui, name):
setattr(QtGui, name, getattr(QtWidgets, name))
|
此外,代码中还包含两段 Base64 编码的字符串,分别对应验证成功和失败时显示的图片,感兴趣可以解码还原。
2020 Challenge 1: Fidler
挑战概述
挑战以一个简单的点击游戏形式呈现,玩家需要通过点击猫咪赚取硬币,最终达到约1000亿硬币来赢得游戏并揭示flag。游戏使用Pygame编写,并提供了源代码(fidler.py)和可执行文件(fidler.exe)。
密码保护
游戏启动时会显示密码输入界面,标题为“This program is protected by Flare-On TURBO Nuke v55.7”。

密码检查函数 password_check(input) 的逻辑如下:
1
2
3
4
|
def password_check(input):
altered_key = 'hiptu'
key = ''.join([chr(ord(x) - 1) for x in altered_key])
return input == key
|
altered_key = 'hiptu'
- 对每个字符的ASCII值减1:h(104)-1=g(103), i(105)-1=h(104), p(112)-1=o(111), t(116)-1=s(115), u(117)-1=t(116)
- 因此,正确密码为
'ghost'
输入错误密码会显示“FBI警告”界面,包含幽默的威胁文本。

游戏机制
- 界面:640x480的窗口,显示猫咪图像(kittyelaine.png)、硬币计数、自动点击器计数。

- 玩法:
- 点击猫咪按钮,每次+1硬币。
- 购买自动点击器(每10硬币一个),每秒自动增加相应数量的硬币。
- 目标:赚取约1000亿硬币(100 Billion coins)。
胜利条件与Flag解码
在 game_screen() 函数中,检查胜利条件:
1
2
3
4
5
6
|
target_amount = (2**36) + (2**35) # 2^36 + 2^35 = 68719476736 + 34359738368 = 103079215104
if current_coins > (target_amount - 2**20): # 2^20 = 1048576
while current_coins >= (target_amount + 2**20):
current_coins -= 2**20
victory_screen(int(current_coins / 10**8)) # token = current_coins // 100000000
return
|
- 当硬币超过
target_amount - 2**20 时触发胜利。
- 实际胜利硬币数需 >=
target_amount + 2**20 ≈ 103080263680。
- token = 103080263680 // 100000000 = 1030(向下取整)。
Flag通过 decode_flag(token) 解码:
1
2
3
4
5
6
7
8
9
10
11
|
def decode_flag(frob):
last_value = frob # 初始为1030
encoded_flag = [1135, 1038, 1126, 1028, 1117, 1071, 1094, 1077, 1121, 1087, 1110, 1092, 1072, 1095, 1090, 1027,
1127, 1040, 1137, 1030, 1127, 1099, 1062, 1101, 1123, 1027, 1136, 1054]
decoded_flag = []
for i in range(len(encoded_flag)):
c = encoded_flag[i]
val = (c - ((i%2)*1 + (i%3)*2)) ^ last_value
decoded_flag.append(val)
last_value = c
return ''.join([chr(x) for x in decoded_flag])
|
- 解码公式:
val = (c - ((i%2)*1 + (i%3)*2)) ^ last_value
- last_value 初始为frob(1030),每次循环后更新为c。
运行解码脚本得到flag:idle_with_kitty@flare-on.com
2020 Challenge 2: garbage
首先打开显示

查DIE发现是upx壳,但是直接upx -d会报错
1
2
3
4
5
6
7
8
9
10
|
fanshanng@Ubuntu:/mnt/d/Desktop/challenge/attachments_383/2 - garbage2/2 - garbage$ upx -d garbage.exe
Ultimate Packer for eXecutables
Copyright (C) 1996 - 2025
UPX 5.0.2 Markus Oberhumer, Laszlo Molnar & John Reiser Jul 20th 2025
File size Ratio Format Name
-------------------- ------ ----------- -----------
upx: garbage.exe: OverlayException: invalid overlay size; file is possibly corrupt
Unpacked 1 file: 0 ok, 1 error.
|
invalid overlay size; file is possibly corrupt没找到是什么原因,于是去翻看源代码
1
2
3
4
5
6
7
8
9
10
|
void Packer::checkOverlay(unsigned overlay)
{
if ((int)overlay < 0 || (off_t)overlay > file_size)
throw OverlayException("invalid overlay size; file is possibly corrupt");
if (overlay == 0)
return;
info("Found overlay: %d bytes", overlay);
if (opt->overlay == opt->SKIP_OVERLAY)
throw OverlayException("file has overlay -- skipped; try '--overlay=copy'");
}
|
会发现错误原因overlay > file_size是文件头信息显示文件比实际大小大。这一点在 CFF Explorer 中也能看到:

文件中缺少 41472−40740 = 732 字节。
于是我打开了010editor,在结尾补全了732个字节

补全后就可以顺利脱壳了
1
2
3
4
5
6
7
8
9
10
|
fanshanng@Ubuntu:/mnt/d/Desktop/challenge/attachments_383/2 - garbage2/2 - garbage$ upx -d garbage.exe
Ultimate Packer for eXecutables
Copyright (C) 1996 - 2025
UPX 5.0.2 Markus Oberhumer, Laszlo Molnar & John Reiser Jul 20th 2025
File size Ratio Format Name
-------------------- ------ ----------- -----------
80470 <- 42582 52.92% win32/pe garbage.exe
Unpacked 1 file.
|
但是脱完壳后的程序还是不能直接运行

这里应该是产生了无效的资源数据,在Resource Hacker可以清晰看到

右键点击资源将其删除

但是又会遇到另一个错误

在 CFF Explorer 中查看最新文件的导入目录,可以看到模块名称缺失:

点击其中任何一个都会显示这些 DLL 中的函数:

看到这些函数名,我就可以确定第一个函数是kernel32.dll,第二个函数是shell32.dll。

然后就可以看到输出的flag了

参考文献: