ctfshow reverse record

REVERSE

萌新赛

数学不及格

re3

考察点:斐波那契数列、elf逆向还原函数参数、十六进制转可读字符串

丢到ida反编译后,首先我们要了解main函数尤其是括号内各参数的基本含义: 2024-09-19-00-01-02

  • int argc: argc 是一个整数,表示命令行参数的数量,包括程序名称本身。例如,如果命令行输入是 ./program arg1 arg2,则 argc 为 3。
  • const char **argv: argv 是一个指向字符串的指针数组,包含所有命令行参数。argv[0] 是程序的名称,argv[1] 是第一个参数,依此类推。
  • const char **envp: envp 是一个可选参数,指向环境变量的指针数组。每个环境变量都是一个字符串,格式为 “KEY=VALUE”。

注意:不是所有编译器都支持 envp 参数。

另外,这里的__cdecl是调用约定,指定如何调用函数(如参数如何传递和栈如何清理),它是 C 语言的默认调用约定,支持可变参数列表。

注意到这里用到了argv[],也能看出来这个main函数实际上是接收了4个参数,且都是十六进制数: 2024-09-18-23-18-04

还有个较陌生的函数strtol,来自于c标准库,用于将字符串转换为长整型(long),分析其原型: 2024-09-18-23-51-15

  • nptr:要转换的字符串。
  • endptr:用于指向未转换部分的指针。如果转换成功,指向字符串中第一个非数字字符的位置。
  • base:指定基数,比如值为16意味着输入字符串被视为十六进制数。

main函数整体逻辑: 首先判断argc是否为5个,不是则输出错误信息并退出程序,然后分别将四个参数使用strtol函数转换为整数,分别存储到v10、v11、v12和v4中。接着将v4减去25923传入函数f,并将返回值存储到v9中。然后通过一系列运算条件做判断,如果不满足则输出相应错误信息并退出程序。最后如果满足求和运算,则输出成功信息。

因为逆向中一般和算法有关,上面出现的都是一些正常的简单判断逻辑,这里的函数f是关键,分析可知f包含着斐波那契数列算法: 2024-09-19-00-20-40 关于斐波那契数列,其中每个数都是前两个数的和。数列的定义如下:

  • 初始条件:
    • ( F(0) = 0 )
    • ( F(1) = 1 )
  • 递推关系:
    • ( F(n) = F(n-1) + F(n-2) ) (对于 ( n \geq 2 ))

伪代码中,首先将从main接收过来的整型参数a1(v4)作为需要计算的斐波那契数列中的第几个数,首先判断参数是否在规定范围(1,200]内,然后用malloc动态分配内存空间存放斐波那契数列的值,接着用for循环根据斐波那契数列的递推关系,直到计算出第a1个数的值停止循环,最后释放内存并把返回的计算结果赋给main的v9。f中我们无法算出项数a1(v4)和对应的数列值v3,回到main中结合所有判断条件看看有无新发现,即使不明白题意,先抽丝剥茧,发现合并同类项后最终可以得到只含v4和v9的表达式,如下: 2024-09-19-10-06-42 到这一步,可以借助在线的斐波那契数列计算网站,生成指定数量的数列,然后看哪一项的值和这里v9的粗略值591286729879(因为v4/3几乎可以忽略不计,即使v4项数大)最为接近,从而我们就可以大致推出来v4的值是多少,我们生成60项,最终在58项时看到了一样的值,因此此时v458,v9591286729879 2024-09-19-10-11-05 发现还可以推算出来其他参数的粗略值: 2024-09-19-10-31-04 这里要注意根据ida的伪代码,v4还要进行处理。

所以到这一步就知道了题目的用意,是想让我们通过斐波那契数列算法,结合已知的表达式,逆向还原出函数的各个传参值,这题的核心就是数学中的解方程和估算。那么此时我们只要将还原出的参数分别按照定义好的参数位置摆放,执行程序时传参,就可以输出最后的成功语句,注意都要是十六进制,这样才能满足表达式中的计算: 2024-09-19-10-35-22 根据成功语句提示,把这些参数组合在一起,解码成字符串就可以得到flag,而要把十六进制转换成字符串,显然要借助编码来完成转义,比如ascii码,每两个十六进制数字代表一个字符(字节),可以将其分组再转换,首先要将十六进制字符串转换为字节流,再将字节流解码为 UTF-8 字符串,可以得到可读的文本格式,因为字节流才是计算机处理数据的基本单位,能够准确表示原始数据: 2024-09-19-10-44-16 这里的编码用ascii解码也能得到同样结果: 2024-09-19-10-45-18

flag白给

flag.exe

考察点:upx脱壳、ollydbg基本使用、windows PE基本逆向

运行程序,初步观察意图: 2024-09-19-11-07-18 2024-09-19-11-07-36 也就是要输入正确的序列号才能给flag,这也看起来像逆向中基本的软件破解类型。 但是当丢到ida后发现找不到main函数,并且反编译后发现逻辑也很奇怪,不像是程序的正常逻辑,此时猜测可能是做了加密混淆被加壳了,既然静态分析遇到阻碍,尝试丢到动态调试器ollydbg中,弹出的警告窗也告知我们这个程序很有可能被处理过: 2024-09-19-11-16-16 丢到查壳工具如Exeinfo PE: 2024-09-19-11-16-55 分析出是upx壳,并且告知签名像是来自于UPX packer,还提供解该upx壳的方式和链接: unpack "upx.exe -d" from http://upx.github.io or any UPX/Generic unpacker 尝试脱upx壳: 2024-09-19-11-28-44 重新丢到ida后发现函数和各个显示项就正常了,也能反编译出正常逻辑的伪代码,shift+F12后没有发现和flag相关的字符串,我们现在逆向破解的目的就是找到输入序列号弹出提示弹窗对应的逻辑代码,看看能不能修改其逻辑达到逆向破解的效果,而提示符是中文的,要运行程序后才能显示,显然此时用动态调试器更合适,丢到OD,根据弹窗逻辑搜索字符串“错误”,发现附近有其对立的词“成功”: 2024-09-19-11-43-34 2024-09-19-11-44-26 双击该位置,就会跳转到对应反汇编代码处: 2024-09-19-11-46-23 由该片段的反汇编代码分析及推测,显然这里的HackAv很可能就是正确的序列号,即flag。 2024-09-19-11-48-47

(未完待续)签退

re3.pyc

考察点:pyc to py

关于.pyc文件:.pyc文件是Python程序编译后的字节码文件,通常在模块导入时自动生成。它们的存在主要是为了提高程序的加载速度,因为解释器可以直接执行这些字节码而不需要再次编译源代码(.py文件)。.pyc文件是优化Python程序执行效率的一种方式,通过减少重复编译的需求来加快程序启动速度。然而,它们并非必需,缺少.pyc文件时,Python解释器会直接从.py源文件运行程序。

如果是windows使用pycdc和pycdas,可以用visual studio来编译安装: image.png image.png 然后再切换编译安装pycdas: image.png 快捷键用ctrl+F5

然后尝试用安装好的pycdc直接反编译,看是否能直接生成py源码: 这里反编译成功了。如果不能,可能还需要配合pycdas反汇编来进一步分析字节码。 观察源代码似乎是一个加密算法,其中这里的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
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
61
62
63
64
65
66
67
68
69
70
# Source Generated with Decompyle++
# File: re3.pyc (Python 2.7)

import string

# 自定义的 Base64 字符表:总共64个字符
c_charset = string.ascii_uppercase + string.ascii_lowercase + string.digits + '()'

# 加密后的 flag(用于校验)
flag = 'BozjB3vlZ3ThBn9bZ2jhOH93ZaH9'

def encode(origin_bytes):
# 将输入的每个字节转为8位二进制字符串
c_bytes = [ '{:0>8}'.format(str(bin(b)).replace('0b', '')) for b in origin_bytes ]

resp = '' # 编码后的结果
nums = len(c_bytes) // 3 # 能整除3的块数
remain = len(c_bytes) % 3 # 剩下不足3个字节的数量

# 取出可整除部分的所有字节(3的倍数部分)
integral_part = c_bytes[0:3 * nums]

# ❌ 以下部分是反编译错误,不合法:
# - tmp_unit 是空列表 [],无法索引
# - 逻辑本应是:将3字节拼接为24位,分4个6位,再映射为字符
for x in [
0,
6,
12,
18]:
tmp_unit = [][int(tmp_unit[x:x + 6], 2)] # ❌ 无意义,非法访问空列表
resp += ''.join([ c_charset[i] for i in tmp_unit ])
integral_part = integral_part[3:]

# 如果还有剩余的字节(1或2个),进行编码补齐
if remain:
# 将剩余的字节拼接起来,并补足到3字节(24位),补0
remain_part = ''.join(c_bytes[3 * nums:]) + (3 - remain) * '0' * 8
# 将24位分成4段,每段6位,然后只取前 remain+1 个字符
tmp_unit = [ int(remain_part[x:x + 6], 2) for x in [
0,
6,
12,
18] ][:remain + 1]
# 使用 c_charset 映射出字符,并在后面补 '.'(表示补齐)
resp += ''.join([ c_charset[i] for i in tmp_unit ]) + (3 - remain) * '.'

# 对编码结果再进行字符级加密(Caesar 加密)
return rend(resp)

# 字符级加密函数:将字母右移2位(Caesar Cipher)
def rend(s):

def encodeCh(ch):
# Caesar 偏移函数:将 ch 转换为右移2位的字母
f = lambda x: chr(((ord(ch) - x) + 2) % 26 + x)

# 如果是小写字母,则从 'a' 偏移
if ch.islower():
return f(97)

# ❌ 错误:这一句反编译错了,本意是 if ch.isupper()
if (None,).isupper(): # ❌ 错误语法:元组对象没有 isupper() 方法
return f(65)

# ❌ 以下这句是非法语法,反编译错误
# - (''.join,) 是一个函数元组
# - (lambda .0: pass)(s) 是完全非法的表达式
return (''.join,)((lambda .0: pass)(s)) # ❌ 无法执行

加密算法分析

逆向思路

逆向解密编写

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
import string  

# 自定义 Base64 字符表:共 64 个字符(标准 Base64 是 A-Z a-z 0-9 + /,这里用 () 替代了 +/)
c_charset = string.ascii_uppercase + string.ascii_lowercase + string.digits + '()'
# 即:'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789()'

# 🔓 第一步:还原 Caesar Cipher 加密(字母左移 2 位)
def unshift(s):
def shift_back(ch):
# 如果是小写字母
if ch.islower():
# 'a' 左移 2 应该变成 'y',所以用 (ord(ch) - ord('a') - 2) % 26 做循环偏移
return chr((ord(ch) - ord('a') - 2) % 26 + ord('a'))
# 如果是大写字母
elif ch.isupper():
return chr((ord(ch) - ord('A') - 2) % 26 + ord('A'))
# 如果不是字母(如数字、括号等),原样返回
else:
return ch
# 把整个字符串的字符逐个还原 Caesar Cipher 加密
return ''.join(shift_back(c) for c in s)

# 🔓 第二步:自定义 Base64 解码
def custom_b64_decode(encoded):
# 去掉结尾可能的补位符号 '.'(源码中编码时为了对齐长度补了这个字符)
encoded = encoded.replace('.', '')

bin_str = ''

# 把每个字符变为其在 c_charset 中的索引,再转成 6 位的二进制字符串
for c in encoded:
index = c_charset.index(c)
bin_str += '{:06b}'.format(index)

# 每 8 位为一组,还原成原始的字节(byte)
bytes_out = []
for i in range(0, len(bin_str), 8):
byte = bin_str[i:i+8]
# 只有当长度足够 8 位,才转换(防止末尾被补零)
if len(byte) == 8:
bytes_out.append(int(byte, 2)) # 转换成十进制整数

# 返回字节数组组成的字节串(再 decode 还原为字符串)
return bytes(bytes_out)

# 📦 已加密的 flag(由 encode() 函数生成)
flag = 'BozjB3vlZ3ThBn9bZ2jhOH93ZaH9'

# 第一步:对 Caesar 加密进行逆运算(右移2变成左移2)
shifted = unshift(flag)
print('[*] Caesar还原:', shifted)

# 第二步:执行自定义 Base64 解码,还原原始明文 flagdecoded = custom_b64_decode(shifted)
print('[+] 解密结果:', decoded)

image.png

内部赛

(未完待续)真的是签到

描述:flag 格式 ctfshow{XX} zip

考察点:

拿到的文件名是zip,通过010editor检查其文件头,确实是zip压缩包文件,将其重命名为xx.zip,然后解压,提示需要密码,先看看是否是伪加密,找到压缩源文件目录区: 2024-09-20-10-44-33 这里的全局方式位标记是奇数,说明可能是伪加密,尝试改成0000,覆盖原来的zip文件,成功解压: 2024-09-20-10-48-35 分析该exe,存在asp压缩壳: 2024-09-20-10-50-29 用ask脱壳工具脱壳并检查: 2024-09-20-11-03-38 2024-09-20-11-05-08 未知保护但检测出了upx packer的签名,说明可能还嵌套了一层upx壳,尝试upx脱壳: 2024-09-20-11-08-30 直接用官方自带命令脱壳失败,说明它可能是个变种壳,即除了常规upx壳外还可能被UPXRUPXSCREAMBLE等做了其他处理,可以先尝试用upxfix解决这些干扰后再重新用官方命令脱壳,然而和官方命令的输出一样提示该文件不是upx壳: 2024-09-20-11-14-51 不要盲目绝对相信输出,也有可能只是程序做了防护措施或加了干扰,此时可以尝试搜索报错中的输出,并没有找到什么很有价值的信息,但部分文章提示到可以尝试看看文件中是否包含UPX的魔术字符,显然上面的检测工具能检测到签名也是和魔术字符有关。把解完asp壳的exe丢到010 editor: 2024-09-20-12-10-22 确实有,但由于经验尚浅,暂时看不出其他更多额外隐藏特征。 最后的办法只能用动态调试器手动尝试脱壳了:

批量生产的伪劣产品

描述:flag 格式ctfshow{XXX} checkme.apk - 蓝奏云

考察点:AndroidManifest.xml文件和<activity>的基本认识

用jadx反编译后,查看AndroidManifest.xml文件,关注主要的组件: image.png 不同组件对应不同的类,可以分别定位过去查看一下: 其中在类a中直接能搜索到flag: image.png

AndroidManifest.xmlAndroid 应用的核心配置文件,它告诉操作系统:这个 App 包含什么组件、具备哪些权限、对外暴露什么功能,以及 启动逻辑和入口 是什么。可以看作是每个 Android App 的“身份证”和“功能说明书”,不写或写错它,App 根本无法运行或安装。 AndroidManifest.xml 中的 <activity> 标签用于声明应用中的一个 Activity 组件,即用户界面中的一个屏幕。系统通过该标签了解应用中有哪些活动页面,以及每个页面的属性和行为。

来一个派森

描述:flag格式ctfshow{} checkme.exe - 蓝奏云

考察点:exe to py、base58编码

派森的谐音显然是python,可以推测这是用python写完然后打包成exe的程序。 可用pyinstxtractor/pyinstxtractor-ng来提取: image.png 默认生成在工具同目录下: image.png 接着pyc转py: image.png

image.png

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
def b58encode(tmp=None):
# 将输入的字符串 tmp 转换为 ASCII 码列表
tmp = list(map(ord, tmp))
# 初始化 temp 为第一个字符的 ASCII 码
temp = tmp[0]
# Base58 字符集(去掉了容易混淆的 0, O, I, l 等字符)
base58 = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'

# 将 tmp 视为大端序的字节序列,转换为一个大整数
for i in range(len(tmp) - 1):
temp = temp * 256 + tmp[i + 1]

# 将大整数 temp 转换为 Base58 编码
tmp = []
while None: # 反编译错误,应为 while temp > 0:
temp = temp // 58
if temp == 0:
break
# 反编译错误,应为 tmp.append(temp % 58)
temp = ''
for i in tmp:
temp += base58[i]

# 对 Base58 编码后的字符串逐字符进行异或操作(每个字符与其索引异或)
tmp = []
for i in range(len(temp)):
tmp.append(chr(ord(temp[i]) ^ i))

# 预定义的检查列表
check = [
'A', '5', 'q', 'O', 'g', 'q', 'd', '\x7f', '[', '\x7f', 's', '{',
'G', 'A', 'x', '`', 'D', '@', 'K', 'c', '-', 'c', ' ', 'G', '+',
'+', '|', 'x', '}', 'J', 'h', '\\', 'l']

# 如果异或后的结果与 check 列表一致,返回 1
if tmp == check:
return 1

# 用户输入 flag
flag = input('输入flag:')
# 检查 flag 是否正确
if b58encode(flag):
print('you win')
else:
print('try again')

加密算法分析

首先对tmp(即flag转换后的ASCII 码列表)中的每个字符temp,将原字节序列转换为大整数并拼接,因为需要先将普通字符串转换为字节序列(ASCII 码),再视为大整数后,才能进行 Base58 编码。

这里实现拼接用到了temp = temp * 256 + tmp[i + 1],即左移8位(256=2^8)后,把序列中的下一个temp字符拼接上来构成大整数。例如,字符串 "abc" 的 ASCII 码是 [97, 98, 99],转换为大整数的过程:

  • temp = 97'a'
  • temp = 97 * 256 + 98 = 24930'a' 左移 8 位 + 'b'
  • temp = 24930 * 256 + 99 = 6382179'ab' 左移 8 位 + 'c'
  • 最终大整数是 6382179(即 0x616263,对应 "abc" 的十六进制表示)。 可以发现按这个顺序是大端序(高位字节在前),符合人类阅读书写习惯,且大端序是网络传输和常见编码的标准顺序。

同理,假设用十进制模拟(类比字节是 0~9 的数字): 拼接 序列[1, 2, 3] 成数字 123: 正确方法(左移):1 * 10 + 2 = 12,再 12 * 10 + 3 = 123,也就是以进制为基准进行计算,最后实现拼接。 错误方法(右移):1 / 10 + 2 = 0 + 2 = 2,再 2 / 10 + 3 = 3,得到 3(这相当于提取数字了而不是拼接)。 所以:

  • 左移(* 256)​​:是​​拼接字节​​的正确操作,保留高位腾出空间用于低位扩展,可以发现左移实际上就相当于让其变成更高进制数(比如原先在个位的1左移后就变成10)
  • ​右移(/ 256)​​:是​​分解字节​​的操作(如解码时),不能用于拼接。

计算机存储数字时,高位在左,低位在右,拼接需要向左扩展空间。

Base58 编码通常用于将大整数(如比特币地址、哈希值)转换为可读字符串,注意只有大整数才能作为Base58 编码的输入。

对于:

1
2
3
tmp = []
for i in range(len(temp)):
tmp.append(chr(ord(temp[i]) ^ i))

temp是前面已经进行base58编码后的字符串,ord(temp[i]) ^ i,对字符 temp[i]ASCII 码与索引 i 按位做异或运算,例如,假设 temp[0] = 'A'(ASCII 码 65),索引 i = 065 ^ 0 = 65,结果仍为A;temp[1] = '4'(ASCII 码 52),索引 i = 1:52 的二进制:00110100,1 的二进制: 00000001,异或结果: 00110101(十进制 53,对应字符 '5')。接着通过append()将异或后的结果拼接,然后存放于tmp列表。最后,将tmp列表与预定义好的check做比对,如果都匹配,则b58encode返回1,说明flag正确。

逆向思路

显然根据上述加密算法,对check列表逆着推导还原就行,由于异或本身就是可逆的,所以将check异或后再base58解码就能得到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
34
35
# Base58 解码函数(将 Base58 字符串还原为原始字节序列)  
def b58decode(encoded):
base58 = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
temp = 0
# 将 Base58 字符串转换为一个整数
for c in encoded:
temp = temp * 58 + base58.index(c)

result = []
# 将整数逐个转为字节(8位)
while temp > 0:
result.insert(0, temp % 256) # 插入到前面,保持原始顺序
temp //= 256

# 将字节列表转成字节串,再解码为字符串(假设原始输入是 ASCII/UTF-8 编码)
return bytes(result).decode()


# 从原程序中的混淆字符列表提取出来
check = [
'A', '5', 'q', 'O', 'g', 'q', 'd', '\x7f', '[', '\x7f', 's', '{', 'G', 'A',
'x', '`', 'D', '@', 'K', 'c', '-', 'c', ' ', 'G', '+', '+', '|', 'x', '}',
'J', 'h', '\\', 'l'
]

# 第一步:逆向异或操作,还原被混淆之前的 Base58 编码字符串
# 原逻辑是:chr(ord(base58_char) ^ index)
# 所以我们要反过来:base58_char = chr(ord(check_char) ^ index)
encoded = ''.join([chr(ord(c) ^ i) for i, c in enumerate(check)])

print("[*] 恢复出的 Base58 编码字符串:", encoded)

# 第二步:Base58 解码,还原最初的用户输入 flagflag = b58decode(encoded)

print("[+] 恢复出的原始 flag:", flag)

image.png

(未完待续)屏幕裂开了

描述:你的手机屏幕能承受住那么多次点击吗? ClickStorm.apk

考察点:

image.png