eatsuki5/Write-up-Cinderbound-Reverse-Challenge-The-Salt-Crown
GitHub: eatsuki5/Write-up-Cinderbound-Reverse-Challenge-The-Salt-Crown
该仓库是一篇 CTF 逆向挑战的详细解题报告,展示了如何反汇编并分析 MicroPython 字节码文件以还原验证逻辑。
Stars: 0 | Forks: 0
# Write-up-Cinderbound-Reverse-Challenge-The-Salt-Crown
本仓库旨在解析 The Salt Crown CTF 中 Cinderbound 逆向挑战的解题过程
# Write-up-Cinderbound-Reverse-Challenge-The-Salt-Crown
本仓库旨在解析 The Salt Crown CTF 中 Cinderbound 逆向挑战的解题过程
| 字段 | 详细信息 |
| ---------- | ------------------------------------- |
| 赛事 | Cyber Apocalypse 2026: The Salt Crown |
| 挑战名称 | Cinderbound |
| 类别 | 逆向 |
| 作者 | danou_78 |
| 战队 | ShadowNet Community |
| 最终 Flag | HTB{c1nd3rbound_v0w5} |
你好,这里是关于逆向分类中名为 Cinderbound 的非常简单的挑战的 write up。我想为我的语法错误道歉,因为英语不是我的母语。这篇 write up 是以对新手友好的方式编写的。
## 1 理解真正的问题
当我们下载 cinderbound zip 文件时,我们会遇到一个问题。我们找到了一个 .mpy 文件。什么是 mpy 文件?mpy 文件代表 micropython,是 python 的一种派生形式。`.mpy` 是 MicroPython 使用的编译字节码格式。
如果我们尝试用 python 执行 .mpy 文件,我们会得到一个错误,这是因为 mpy 文件只能在 micropython 环境中执行。那么我们该如何分析它的代码呢?我们需要安装 mpy-tools,可以从 micropython 的 git 仓库中获取它。
```
git clone https://github.com/micropython/micropython.git
```
之后我们需要 cd 进入 micropython 目录,然后再进入 tools 目录。
接着我们需要执行它。
```
./mpy_tool.py -xd cinderbound.mpy
```
(-x 和 -d 分别代表 hexdump 和 --disassemble,用于查看指令)
然后我们会发现一大块指令,我将尝试用两个代码块,一个用来解释低级指令,另一个用来展示其对应的 Python 代码。
第一行我们会看到魔术数以及其他你可能会觉得熟悉的内容,比如函数。
`
```
00000000: 4d06 001f 0801 186a 7564 6765 5f73 7263 M......judge_src judge_src.py
00000010: 2e70 7900 0f0a 6a75 6467 6500 7910 7379 .py...judge.y.sy judge append syllable
00000020: 6c6c 6162 6c65 0081 5781 6f81 590a 1007 llable..W.o.Y... len ord list
00000030: 0235 3707 0331 3239 0703 3135 3407 0233 .57..129..154..3 57 129 154 31
00000040: 3107 0331 3939 0703 3139 3207 0237 3307 1..199..192..73. 199 192 73
00000050: 0332 3433 0702 3433 0703 3137 3607 0332 .243..43..176..2 243 43 176 255
00000060: 3535 0703 3137 3307 0235 3407 0332 3033 55..173..54..203 173 54 203
00000070: 0702 3637 0702 3135 4c00 0201 3200 1602 ..67..15L...2... 67 15
00000080: 5163 0185 4059 1402 0420 2324 232a 322e Qc..@Y... #$#*2. judge
00000090: 3023 00c1 2280 5ac2 2b00 c312 05b0 3401 0#..".Z.+.....4. `
```
之后,在代码被混淆前的指令中,我们会发现它的真实名称,源文件被称为 `judge_src.py`。(.mpy 扩展名是在 python 中指示 mpy 模块后创建的)。
## 2 从指令翻译为真实代码
然后我们会看到如下内容
```
obj_table: [(57, 129, 154, 31, 199, 192, 73, 243, 43, 176, 255, 173, 54, 203, 67, 15)]
simple_name:
raw bytecode: 9 00:02:01:32:00:16:02:51:63
prelude: (1, 0, 0, 0, 0, 0)
args: []
line info:
32:00 MAKE_FUNCTION 0
16:02 STORE_NAME judge
51 LOAD_CONST_NONE
63 RETURN_VALUE
children: [judge]
```
据我理解,module 是我们在 mpy 环境中所谓的父函数,而 children [judge] 简单来说就是我们要处理的“main”函数。看到 simple_name 上面的 obj_table 了吗?它会派上用场的(如果你问为什么 obj_const 在一个列表里,那是因为 mpy 环境将所有 const 对象放在一个列表中以便于访问;如果有第二个对象,它可能会写成这样 `[(list),(another_list)]`)。
然后我们终于看到了名为 judge 的 main 函数
```
simple_name: judge
raw bytecode: 88 59:14:02:04:20:23:24:23:2a:32:2e:30:23:00:c1:22:80:5a:c2:2b:00:c3:12:05:b0:34:01:80:42:6b:57:c4:12:06:b0:b4:55:34:01:b2:ee:b4:8d:f4:22:81:7f:ef:ee:c5:b2:12:06:b0:b4:55:34:01:f2:22:81:7f:ef:c2:b3:14:03:b5:36:01:59:81:e5:58:5a:d7:43:10:59:59:b3:12:07:b1:34:01:d9:63
prelude: (12, 0, 0, 1, 0, 0)
args: ['syllable']
```
它接收 `syllable` 作为参数,我们现在可以开始将这些指令翻译为真实代码了。
```
def judge(syllable):
```
我将尝试每次展示至少 10 条指令:
```
LOAD_CONST_OBJ (57, 129, 154, 31, 199, 192, 73, 243, 43, 176, 255, 173, 54, 203, 67, 15)
c1 STORE_FAST 1
22:80:5a LOAD_CONST_SMALL_INT 90
c2 STORE_FAST 2
2b:00 BUILD_LIST 0
c3 STORE_FAST 3
12:05 LOAD_GLOBAL len
b0 LOAD_FAST 0
34:01 CALL_FUNCTION 1
80 LOAD_CONST_SMALL_INT 0
```
这里我们有很多信息,让我们试着找出它们的含义。
`LOAD_CONST_OBJ` 加载存储在常量表中的对象。在这里,它是稍后用于验证的 tuple。
`LOAD_FAST` 可以理解为变量,所以我将其命名为 FAST X(X 代表数字)。
然后我们有作为 const 对象的 FAST 1,之后创建了一个存储在 FAST 2 中的整数,一个存储在 FAST 3 中的空列表,以及一个用于整个函数的 len 函数,`LOAD_FAST 0` 意味着我们将尝试访问第一个变量 0,也就是参数 `syllable`。
为了更清楚地说,我们有:
FAST 0 : syllable
FAST 1 : object tuple
FAST 2 : 90(因为这是一个整数)
FAST 3 : 设为 0 的空列表 []
在 Python 中我们将得到:
```
def judge(syllable):
const = (57, 129, 154, 31, 199, 192, 73, 243, 43, 176, 255, 173, 54, 203, 67, 15)
fast_2 = 90
fast_3 = []
length = len(syllable)
```
让我们跳过接下来的 10 条指令。
```
LOAD_CONST_SMALL_INT 0
42:6b JUMP 43
57 DUP_TOP
c4 STORE_FAST 4
12:06 LOAD_GLOBAL ord
b0 LOAD_FAST 0
b4 LOAD_FAST 4
55 LOAD_SUBSCR
34:01 CALL_FUNCTION 1
```
这里我们设置了一个值为整数 0 的变量,我们称之为:
`x = 0`
它被存储在栈顶(我们需要跳转到偏移量 43 处,稍后再完善我们的代码)。
JUMP 主要用于循环指令,这意味着例如在一个 while 循环中,代码首先会检查条件是否为真,如果不为真,则什么也不做。
`DUP_TOP` 意味着复制栈顶元素。
之后我们有了 `LOAD_FAST 4` 指令,这会在我们的变量库中添加另一个变量 FAST = 0,并且设置了另一个函数,即 `ord()`。
所以 FAST 4 = 0。
这里有一条新指令 `LOAD_FAST`,意味着程序将从上面解释过的地址 FAST 1 和 FAST 0 中获取值或数据。
```
def judge(syllable):
const = (57, 129, 154, 31, 199, 192, 73, 243, 43, 176, 255, 173, 54, 203, 67, 15)
fast_2 = 90
fast_3 = []
length = len(syllable)
x = 0
while #we don't know at this moment
```
`LOAD_SUBSCR` 用于索引序列,比如访问 list 或 tuple 的元素。
`LOAD_FAST 2` 意味着我们将“调用”或访问变量 FAST 2 的值,在这里也就是 `fast_2`。
```
b2 LOAD_FAST 2
ee BINARY_OP 23 __xor__
b4 LOAD_FAST 4
8d LOAD_CONST_SMALL_INT 13
f4 BINARY_OP 29 __mul__
22:81:7f LOAD_CONST_SMALL_INT 255
ef BINARY_OP 24 __and__
ee BINARY_OP 23 __xor__
c5 STORE_FAST 5
```
这里开始变得有点复杂了,这里的 `__xor__` 意味着将进行一次 xor 运算,我们访问值为 0 的 `fast_4` 变量,加载设为 90 的变量 FAST 2,与值为 0 的 `LOAD_FAST 4` 进行 xor 运算,接着我们有两个新值 13 和 255,以及新的乘法运算和 and 运算,还有一个用于存储结果的变量 FAST 5。因此:
FAST 5 = 某个结果
然后我们开始将这些混合起来。
我们可以得到类似这样的代码:
`fast_5 = (ord(syllable[fast_4]) ^ fast_2) ^ ((fast_4 * 13) & 255)`
我们这里得到了什么?`fast_5` 是结果变量,我们对 `judge` 参数 `syllable` 应用了 `ord` 函数(索引为 `fast_4`),因此 `fast_4` 是我们 while 循环的条件之一。我们对它应用值为 90 的 `fast_2`,然后再与索引乘以 13 的结果进行一次 xor 运算,最后与 255 进行 and 运算。
为了让它更容易理解,我们可以这样写(抱歉,我觉得这可能有点不太对):
`result = (ord(syllable[x])^90)^ ((x * 13)&255)`
我们的 Python 翻译开始变得越来越清晰了:
```
def judge(syllable):
const = (57, 129, 154, 31, 199, 192, 73, 243, 43, 176, 255, 173, 54, 203, 67, 15)
fast_2 = 90
fast_3 = []
length = len(syllable)
x = 0
while x # Always unclear at this stage:
result=(ord(syllable[x])^90)^((x * 13)&255)
```
接下来的 10 条指令会更简单,因为我们已经见过相同的指令了。
```
b2 LOAD_FAST 2
12:06 LOAD_GLOBAL ord
b0 LOAD_FAST 0
b4 LOAD_FAST 4
55 LOAD_SUBSCR
34:01 CALL_FUNCTION 1
f2 BINARY_OP 27 __add__
22:81:7f LOAD_CONST_SMALL_INT 255
ef BINARY_OP 24 __and__
c2 STORE_FAST 2
```
代码加载了 `FAST_2` 和 `ord` 函数,以及其他变量 `FAST_0` 和 `FAST_4`,还有同样的 `SUBSCR` 指令。我们对 `fast_2` 和包含 `fast_0` 及 `fast_4` 的 `ord` 函数应用加法运算(`+`),然后对结果与 255 应用 and 操作,并将结果存回 `FAST_2` 本身。有了这些信息,我们可以写出这个:
`fast_2 = (fast_2 + ord(syllable[fast_4])) & 255`
或者像这样:
`fast_2 = (fast_2 + ord(syllable[x]))& 255`。
然后在 Python 中:
```
def judge(syllable):
const = (57, 129, 154, 31, 199, 192, 73, 243, 43, 176, 255, 173, 54, 203, 67, 15)
fast_2 = 90
fast_3 = []
length = len(syllable)
x = 0
while x # Always unclear at this stage:
result=(ord(syllable[x])^90)^((x * 13)&255)
fast_2 = (fast_2 + ord(syllable[x]))& 255
```
这表明 `fast_2` 会随着循环次数而改变,因此 90 在每次迭代中都会发生变化。
接下来我们有两个新指令。
```
b3 LOAD_FAST 3
14:03 LOAD_METHOD append
b5 LOAD_FAST 5
36:01 CALL_METHOD 1
59 POP_TOP
81 LOAD_CONST_SMALL_INT 1
e5 BINARY_OP 14 __iadd__
58 DUP_TOP_TWO
5a ROT_TWO
d7 BINARY_OP 0 __lt__
```
这里我们有 2 个变量和一个 `append` 函数,我们可以直接补全:
```
fast_3.append(result)
```
这条指令与 `CALL_METHOD` 一起会将值推入栈顶,其结果为 `None`(这就是为什么我在 write up 开头纠正了一个由于误解而产生的错误)。
我们需要跟踪栈的变化以便更清楚:
在 `call_method` 处,`append` 函数返回了 `None`,随后将被 `POP_TOP` 清理掉。
| 常量为 0 时的初始 TOP | CALL_METHOD | 在 POP_TOP 之后 | 加载常量 1 | iaad 运算符 | TOP_DUP_TWO | ROT_TWO |
| ------------------------------------ | ----------- | ----------------- | --------------- | ------------- | ----------- | ---------- |
| x | None | x | 1 | x1 | x1 | len(const) |
| len(const) | x | len(const) | x | len(const) | len(const) | x1 |
| | len(const) | | len(const) | | x1 | x1 |
| | | | | | len(const) | len(const) |
所以在这里我们终于可以找到 while 循环的两个条件,因为 `__lt__` 代表小于,它会尝试执行 while 循环中的 `x1 < len(const)`。
现在我们的 Python 代码可以这样写:
```
def judge(syllable):
const = (57, 129, 154, 31, 199, 192, 73, 243, 43, 176, 255, 173, 54, 203, 67, 15)
fast_2 = 90
fast_3 = []
length = len(syllable)
x = 0
while x < len(const)
result=(ord(syllable[x])^90)^((x * 13)&255)
fast_2 = (fast_2 + ord(syllable[x]))& 255
# Here the fast_3 and fast_5
fast_3.append(result)
x+=1
```
| TOP_DUP_TWO | ROT_TWO | POP_JUMP_IF_TRUE |
| ----------- | ---------- | ---------------- |
| x1 | len(const) | True |
| len(const) | x1 | x1 |
| x1 | x1 | len(const) |
| len(const) | len(const) | |
在这里 `x1` 和 `len(const)` 变成了一个布尔值,`pop_jump_if_true` 会将布尔值移出栈,栈中只剩下 `x1` 和 `len(const)`,最后栈将随着接下来的 2 条 `POP` 指令被完全清空。
```
43:10 POP_JUMP_IF_TRUE -48
59 POP_TOP
59 POP_TOP
b3 LOAD_FAST 3
12:07 LOAD_GLOBAL list
b1 LOAD_FAST 1
34:01 CALL_FUNCTION 1
d9 BINARY_OP 2 __eq__
63 RETURN_VALUE
```
之后我们加载 `fast_3`(包含所有的 result 值)和作为 list 的 `fast_1`,以及代表等于的 `__eq__` 运算符,我们使用它来返回 `fast_3` 和 `fast_1` 之间的相等性比较结果。
```
def judge(syllable):
const = (57, 129, 154, 31, 199, 192, 73, 243, 43, 176, 255, 173, 54, 203, 67, 15)
fast_2 = 90
fast_3 = []
length = len(syllable)
x = 0
while x < len(const)
result=(ord(syllable[x])^90)^((x * 13)&255)
fast_2 = (fast_2 + ord(syllable[x]))& 255
# Here the fast_3 and fast_5
fast_3.append(result)
x+=1
return fast_3 == list(fast_1)
```
至此我们终于完成了翻译。但是我们该如何尝试找到 flag 呢?虽然我们现在还不能直接得到 flag,但既然我们已经理解了使用 `chr()` 函数(作为 `ord()` 的逆操作,能将十进制转换为 ASCII 字符)的代码,接下来的工作就会简单得多。
我们将从创建常量对象,也就是我们的第一个 tuple 开始。
```
const = (
57, 129, 154, 31,
199, 192, 73, 243,
43, 176, 255, 173,
54, 203, 67, 15
)
def decryption():
const1 = 90
flag = []
# We will use enumerate function
for x, value in enumerate(const):
# for each index for example 0 *13 = 0 and 255 will also make 0 and this is for each index of the const tuple
result1 = (x * 13)&255
# Here we try to exchange the xored values of const by applying one of the xor rule a ^b = c soo c^b=a for example in our first code result was each index of syllable xored by 90 and xored with index multiply with 13 and "and" operand
result2 = value ^ result1 ^ const1
#Here we change the decimal integer into ASCII
result_final = chr(result2)
flag.append(result_final)
#Here same as for judge
const1 = (const1 + result2) & 255
return "".join(flag)
print(decryption())
```
最终我们得到了 flag `{c1nd3rbound_v0w5}`。我还想补充一点,通过展示最终转换为 ASCII 的十进制数值的演变过程。
首先我们将用 `result1` 进行尝试。
```
const = (
57, 129, 154, 31,
199, 192, 73, 243,
43, 176, 255, 173,
54, 203, 67, 15
)
const1 = 90
flag = []
result1_value = []
for x, value in enumerate(const):
result1 = (x * 13)&255
result1_value.append(result1)
result2 = value ^ result1 ^ const1
result_final = chr(result2)
flag.append(result_final)
const1 = (const1 + result2) & 255
print(result1_value)
```
在这里,通过将 `result1` 放入一个新列表,我们得到:
`[0, 13, 26, 39, 52, 65, 78, 91, 104, 117, 130, 143, 156, 169, 182, 195]`
将 `result1` 替换为 `result2`,我们得到:
`[99, 49, 110, 100, 51, 114, 98, 111, 117, 110, 100, 95, 118, 48, 119, 53]`
最后使用 `result_final`:
`['c', '1', 'n', 'd', '3', 'r', 'b', 'o', 'u', 'n', 'd', '_', 'v', '0', 'w', '5']`
感谢您阅读这篇 write up,它非常长,我尽力做到尽可能清晰。如果您有任何建议,请随时在 Discord 上私信我:danou_78
标签:MicroPython, Writeup, 云资产清单, 逆向工具, 逆向工程