最近在攻防世界的CoolCat挑战中遇到一个有趣的MISC题目,题目给了一张被自定义"ACM"算法加密的图片和一段残缺的Python代码。这个挑战完美结合了图片处理和RSA加密的知识点,让我来分享一下解题的全过程。
首先拿到题目时,解压后主要看到两部分内容:一张看起来像是被处理过的图片,以及一段明显不完整的Python代码。代码中定义了一个名为ACM的函数,从参数名就能猜到这应该和RSA加密有关,但关键的参数p、q和m都丢失了。这种题目类型在CTF比赛中很常见,考察的就是如何从有限的信息中逆向推导出关键参数。
先来看这段Python代码的核心部分 - ACM函数。这个函数接收四个参数:img(图片对象)、p、q和m。函数内部的主要逻辑是一个循环,循环次数由m控制。每次循环都会对图片像素进行重新排列。
具体来说,函数首先检查图片模式,如果是调色板模式("P")就转换为RGB模式。然后确保图片是正方形(宽高相等)。核心的像素置换逻辑在嵌套的for循环中:
python复制nx = (x + y * p) % width
ny = (x * q + y * (p * q + 1)) % height
这两行代码定义了像素位置(x,y)会被移动到新位置(nx,ny)。这种置换方式看起来像是某种线性变换,而且明显和参数p、q相关。
仔细分析这个置换公式,可以发现它实际上是一个线性变换矩阵的应用:
[nx] = [1 p][x]
[ny] [q pq+1][y]
这个矩阵的行列式值为1pq + 1 - pq = 1,说明这个变换是可逆的。这一点很重要,意味着如果我们能找到正确的p、q值,就能逆向这个变换。
在实际操作中,我尝试用Pillow库加载图片,然后手动尝试一些小的p、q值(比如1到10之间),观察图片的变化。这种方法虽然原始,但对于小范围的参数猜测很有效。
题目提示说图片是用ACM加密的,但丢失了p、q和m参数。而ACM这个名称和RSA的参数命名(p,q)高度相似,这提示我们可能需要从RSA的角度来思考。
在RSA加密中,p和q是两个大素数,n=pq。而题目中的置换矩阵恰好包含了pq这一项。这不是巧合,而是出题人精心设计的提示。
m参数控制着置换循环的次数。在RSA中,加密过程通常只需要一次运算,但这里为什么要多次循环呢?我猜测这可能是为了增加逆向难度,或者模拟RSA中模重复平方法的概念。
实际操作中发现,m值过大时图片会变得完全混乱,而m值过小时可能无法完全打乱图片。因此m值需要适中,通常在题目设计中会选择10-100之间的值。
由于我们有一张加密后的图片,可以假设原始图片有某些可识别的特征(比如特定颜色分布、边缘特征等)。我们可以编写脚本尝试不同的p、q组合,观察解密后的图片是否恢复这些特征。
python复制from PIL import Image
def reverse_ACM(img, p, q, m):
for _ in range(m):
dim = width, height = img.size
with Image.new(img.mode, dim) as canvas:
for x in range(width):
for y in range(height):
# 逆向计算原始位置
orig_x = (x*(p*q+1) - y*p) % width
orig_y = (-x*q + y) % height
canvas.putpixel((x,y), img.getpixel((orig_x, orig_y)))
img = canvas
return img
由于p和q在RSA中都是素数,我们可以将搜索范围限制在素数集合内。此外,通常CTF题目中的参数不会太大,可能在1000以内。
另一个技巧是观察图片尺寸。因为置换操作使用了模width/height运算,所以p和q的值可能与图片尺寸有数学关系(比如是尺寸的因数或互质数)。
为了提高效率,我编写了一个自动化测试脚本,可以批量尝试不同的p、q组合:
python复制from itertools import permutations
from math import isqrt
def is_prime(n):
if n < 2:
return False
for i in range(2, isqrt(n)+1):
if n % i == 0:
return False
return True
# 假设图片尺寸为512x512
size = 512
primes = [p for p in range(2, 100) if is_prime(p)]
for p, q in permutations(primes, 2):
try:
decrypted = reverse_ACM(enc_img, p, q, 10) # 假设m=10
if is_recognizable(decrypted):
print(f"Found params: p={p}, q={q}")
break
except:
continue
如何自动判断解密是否成功是个挑战。我采用了以下几种方法:
在实际操作中,我发现结合视觉检查最可靠,所以让脚本保存每个可能的解密结果,然后人工快速浏览。
在解决这类问题时,经常会遇到一些坑。比如有一次我花了半天时间调试,最后发现是因为图片模式问题 - 有些操作需要图片是RGB模式而非RGBA。另一个常见问题是循环次数m的选择,有时候需要尝试不同的m值才能得到正确结果。
调试这类问题时,我建议:
这个CoolCat挑战很好地展示了如何将不同领域的知识结合起来解决问题。图片处理、数论知识和编程技巧缺一不可。在实际比赛中,时间有限,所以快速识别题目类型和潜在解法很关键。
我发现这类题目通常有一些共同特点:
最后成功解密时,看到flag清晰呈现的瞬间,所有的调试痛苦都值得了。这种从混乱中找出规律、一步步接近答案的过程,正是CTF比赛最吸引人的地方。