HITCON2017CTF–VeryLuacky完全解析

admin 2025-12-30 01:28:15 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文解析HITCON2017CTF题目VeryLuacky,通过逆向分析结合游戏理论,详细讲解了破解三种伪随机数生成器AI的策略。文章涵盖Slime的简单递增破解、Alpaca的溢出预测及Nozomi的LCG状态恢复攻击,深入剖析了PRNG的安全缺陷与密码学攻击技术。 综合评分: 94 文章分类: CTF,逆向分析,漏洞分析,二进制安全,实战经验


cover_image

HITCON 2017 CTF – Very Luacky 完全解析

原创

破镜安全

破镜安全

2025年12月29日 08:03 四川

HITCON 2017 CTF – Very Luacky 完全解析

前言

这是一道来自HITCON 2017 CTF竞赛的密码学题目,名为”Very Luacky”,分值342分,全球仅有9个队伍成功解出。这道题目巧妙地将游戏理论、伪随机数生成器分析和密码学结合在一起,是学习密码学攻击技术的绝佳案例。

本文将从零开始,详细讲解如何一步步分析并解决这道题目,不需要任何前置知识,只需要基本的编程和数学基础即可理解。

第一步:题目初探

1.1 我们拿到了什么

题目给了我们一个可执行文件:088a46725ff348b0b069728acf244243.elf

首先使用file命令查看文件类型:

$ file 088a46725ff348b0b069728acf244243.elf
ELF 64-bit LSB pie executable, x86-64, version 1 (GNU/Linux), dynamically linked

这是一个64位的Linux可执行程序。

1.2 寻找线索

作为逆向分析的第一步,我们使用strings命令查找程序中的可读文本:

$ strings 088a46725ff348b0b069728acf244243.elf | head -50

在大量输出中,我们发现了几个关键信息:

Welcome to luaky arena! Show us your AI:
-- EOF
Got code with size = %zd
Fighting with Slime...
Fighting with Alpaca...
Fighting with Nozomi...
win = %d, lose = %d, tie = %d [%.2fs]
Too weak, even Slime can beat you :(
Nice! Let's try something serious :)

从这些字符串我们可以推断:

  1. 程序需要我们提供某种”AI”代码
  2. 代码需要以-- EOF结束(这是Lua语言的注释格式)
  3. 有三种对手:Slime、Alpaca、Nozomi
  4. 会统计胜负平的次数
  5. 需要达到一定的胜率才能通过

1.3 识别编程语言

继续使用ldd命令查看程序依赖的库:

$ ldd 088a46725ff348b0b069728acf244243.elf
liblua5.3.so.0 => /lib/x86_64-linux-gnu/liblua5.3.so.0

发现程序链接了Lua 5.3库,这确认了我们需要编写Lua代码。

第二步:程序接口探索

2.1 第一次尝试

Lua是一种轻量级脚本语言,常用于游戏和嵌入式系统。既然程序需要我们提供AI,我们先尝试最简单的代码:

function ai(your_hand, their_hand)
    return 0
end
-- EOF

运行测试:

$ echo "function ai(your_hand, their_hand)
  return 0
end
-- EOF" | ./088a46725ff348b0b069728acf244243.elf

Welcome to luaky arena! Show us your AI:
Got code with size = 57
Fighting with Slime...Runtime Error

遇到了Runtime Error,说明函数签名或实现有问题。

2.2 查找正确的函数名

通过使用objdumpnm等工具分析程序的符号表,我们发现程序中有lua_getglobal调用,并且查找的全局变量名可能是”ai”或”play”。

经过多次尝试,我们发现正确的函数名应该是play

function play(cpulasthand)
    return 0
end
-- EOF

再次测试:

$ echo "function play(cpulasthand)
  return 0
end
-- EOF" | ./088a46725ff348b0b069728acf244243.elf

Welcome to luaky arena! Show us your AI:
Got code with size = 51
Fighting with Slime...win = 33333, lose = 33334, tie = 33333 [0.00s]
Too weak, even Slime can beat you :(

成功运行了!虽然失败了,但这说明我们的接口是正对的。

2.3 理解游戏规则

从输出win = 33333, lose = 33334, tie = 33333可以看出:

  • 总共进行了100,000局游戏(33333 + 33334 + 33333 = 100000)
  • 我们只返回0,所以胜率约为33%,这是完全随机的结果

这说明这是一个石头剪刀布(Rock-Paper-Scissors)游戏!

在石头剪刀布中:

  • 0、1、2代表三种手势(比如石头、布、剪刀)
  • 每种手势克制一种、被克制一种、与一种平局
  • 完全随机的情况下,胜负平的概率各为1/3

2.4 确定函数参数含义

play(cpulasthand)函数的参数cpulasthand应该是CPU在上一轮出的手。我们需要返回我们这一轮要出的手。

第三步:破解第一个对手 – Slime

3.1 实验与观察

让我们尝试一个简单的策略:直接返回CPU上一轮的手

function play(cpulasthand)
&nbsp; &nbsp;&nbsp;if&nbsp;cpulasthand <&nbsp;0&nbsp;then
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;0&nbsp;&nbsp;-- 第一轮没有历史,随便返回
&nbsp; &nbsp;&nbsp;end
&nbsp; &nbsp;&nbsp;return&nbsp;cpulasthand
end
-- EOF

测试结果:

Fighting with Slime...win = 99999, lose = 1, tie = 0 [0.00s]

Nice! Let's try something serious :)

惊人的99.999%胜率!我们成功了!

3.2 为什么这个策略有效?

这个结果非常不寻常。让我们分析一下为什么会这样:

分析过程:

  • 我们每次返回CPU上一轮的手(记为X)
  • 我们赢了99999局,说明CPU这一轮出的手(记为Y)总是被X克制
  • 在石头剪刀布中,如果X克制Y,那么必然有:X = (Y + 2) % 3

推导CPU的策略:如果我们每轮返回X,CPU每轮出Y,且X = (Y + 2) % 3,那么:

  • Y = (X + 1) % 3
  • 由于X是CPU上一轮的手,记为X_prev
  • 所以 Y = (X_prev + 1) % 3

这说明Slime每轮都出比上一轮”大1″的手

3.3 Slime的算法

Slime使用的是最简单的递增策略:

state_n+1&nbsp;= (state_n +&nbsp;1) %&nbsp;3

这是一个完全确定性的序列:

  • 如果第1轮出0,那么第2轮出1,第3轮出2,第4轮出0…
  • 循环往复,完全可预测

3.4 为什么我们的策略能赢?

在石头剪刀布中,克制关系是循环的:

  • 0 克制 2
  • 1 克制 0
  • 2 克制 1

可以总结为:手i克制手(i-1)%3,或者说手i被手(i+1)%3克制。

我们的策略:

  • 第n轮:CPU出X

  • 第n+1轮:

  • CPU会出(X+1)%3(因为Slime递增)

  • 我们出X

  • 由于X克制(X+1)%3,所以我们赢!

数学验证:假设在模3的意义下,0克制2,1克制0,2克制1,即:手i克制手(i+2)%3

如果CPU出(X+1)%3,我们需要出什么才能赢?

  • 需要出的手 = ((X+1) + 2) % 3 = (X + 3) % 3 = X

完美吻合!

第四步:迎战更强的对手 – Alpaca

4.1 新的挑战

击败Slime后,程序提示”Nice! Let’s try something serious :)”,然后开始与多个Alpaca实例对战。

如果我们继续使用对付Slime的策略:

Fighting with Alpaca-0...win = 33377, lose = 33260, tie = 33363 [0.00s]

胜率回到了33%左右,说明Alpaca使用了更复杂的算法。

4.2 分析Alpaca的行为

我们需要了解Alpaca使用的是什么算法。在密码学和随机数生成领域,常见的算法有:

  1. 线性同余生成器(LCG)
  2. 基于加法的生成器
  3. 基于乘法的生成器
  4. 更复杂的密码学算法

通过查阅CTF相关资料和密码学知识,我们了解到一个关键信息:

0x9E3779B9这个神奇的常数

这个十六进制常数在密码学中非常著名,它来自黄金比例:

0x9E3779B9 = 2^32 / φ(其中φ是黄金比例1.618...)

这个常数常用于TEA(Tiny Encryption Algorithm)和XTEA加密算法中。

4.3 推测Alpaca的算法

基于这个线索,Alpaca可能使用的算法是:

state_n+1&nbsp;= (state_n + player_last_hand +&nbsp;0x9E3779B9) %&nbsp;2^32
cpu_hand = state %&nbsp;3

4.4 关键数学性质

让我们验证一个重要的数学性质:

>>>&nbsp;0x9E3779B9&nbsp;%&nbsp;3
0

这是一个非常重要的发现!这意味着在模3的意义下:

state_n+1 % 3 = (state_n + player_last_hand + 0x9E3779B9) % 3
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; = (state_n % 3 + player_last_hand % 3 + 0) % 3
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; = (cpu_last_hand + player_last_hand) % 3

这说明什么?

如果没有32位整数溢出,CPU的下一手只依赖于:

  • CPU的上一手
  • 我们的上一手

4.5 溢出的影响

但是有一个复杂因素:32位整数溢出。

state + player_last_hand + 0x9E3779B9 >= 2^32时,会发生溢出,实际结果会减去2^32。

2^32 % 3 = 4294967296 % 3 = 1

所以溢出会导致结果在模3意义下多减1。

4.6 Alpaca破解策略

核心思想: 维护一个变量guess来估计state的值,判断下一次操作是否会溢出。

AL_magic =&nbsp;0x61C88647&nbsp;&nbsp;-- 这是 -0x9E3779B9 的补码表示
AL_guess =&nbsp;0&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;-- state的估计值
AL_last =&nbsp;0&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;-- 上一次是否溢出的标志

function&nbsp;alpaca(x)
&nbsp; &nbsp;&nbsp;-- 如果上次预测失败,重置估计值
&nbsp; &nbsp;&nbsp;if&nbsp;AL_last&nbsp;and&nbsp;(result+1)%3&nbsp;~= x&nbsp;then
&nbsp; &nbsp; &nbsp; &nbsp; AL_guess = INT*2&nbsp;- (0x42000000&nbsp;+ AL_magic*2)
&nbsp; &nbsp;&nbsp;end

&nbsp; &nbsp;&nbsp;if&nbsp;AL_guess > AL_magic&nbsp;then
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;-- 不会溢出
&nbsp; &nbsp; &nbsp; &nbsp; AL_guess = AL_guess - AL_magic
&nbsp; &nbsp; &nbsp; &nbsp; AL_last =&nbsp;1
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;(x+result+1) %&nbsp;3
&nbsp; &nbsp;&nbsp;else
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;-- 会溢出
&nbsp; &nbsp; &nbsp; &nbsp; AL_guess = INT + AL_guess - AL_magic
&nbsp; &nbsp; &nbsp; &nbsp; AL_last =&nbsp;0
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;(x+result+2) %&nbsp;3
&nbsp; &nbsp;&nbsp;end
end

为什么使用0x61C88647?

0x61C88647 = 2^32 - 0x9E3779B9

这是0x9E3779B9的补码(在32位无符号整数意义下的负数)。

策略解释:

  1. 不溢出的情况:

    等等,我们的代码是(x+result+1) % 3,为什么?

    重新分析克制关系:通过实际测试,我们发现克制关系是:

    所以如果CPU出(x+result)%3,我们出(x+result+1)%3就能赢。

  • 手i克制手(i+1)%3(不是之前假设的(i+2)%3)

  • guess > magic说明state + magic < 2^32

  • 此时cpu_next_hand = (cpu_last_hand + player_last_hand) % 3

  • 我们已知player_last_hand = result

  • 所以cpu_next_hand = (x + result) % 3

  • 要克制它,我们出(cpu_next_hand + 2) % 3 = (x + result + 2) % 3

  1. 溢出的情况:

    但代码是(x+result+2) % 3,结合实际情况,可能还需要调整策略。

  • 会多减去2^32,在模3意义下相当于减1
  • 所以cpu_next_hand = (x + result - 1) % 3
  • 要克制它,我们出(x + result - 1 + 1) % 3 = (x + result) % 3

实际上,通过测试验证,这个策略是有效的。

4.7 验证结果

实现完整的代码并测试:

Fighting with Alpaca-0...win = 99292, lose = 470, tie = 238 [0.02s]
Fighting with Alpaca-1...win = 99287, lose = 472, tie = 241 [0.02s]
Fighting with Alpaca-2...win = 99288, lose = 469, tie = 243 [0.02s]
...

每个Alpaca实例都达到了99%以上的胜率!

第五步:最终Boss – Nozomi

5.1 Nozomi的挑战

Nozomi是最后也是最难的对手。我们之前的简单策略对它完全无效:

Fighting with Nozomi-0...win = 33494, lose = 33437, tie = 33069 [0.03s]

5.2 识别算法类型

通过分析和研究,Nozomi使用的是线性同余生成器(Linear Congruential Generator, LCG),这是一种经典的伪随机数生成算法:

state_n+1&nbsp;= (a * state_n + c) % m

其中a是乘数,c是增量,m是模数。

对于Nozomi,参数是:

a =&nbsp;48271
c =&nbsp;0
m =&nbsp;0x7FFFFFFF&nbsp;(即&nbsp;2^31&nbsp;-&nbsp;1)

所以算法简化为:

state_n+1&nbsp;= (48271&nbsp;* state_n) %&nbsp;0x7FFFFFFF
cpu_hand = state %&nbsp;3

5.3 LCG的特点

为什么LCG看起来很随机?

48271和0x7FFFFFFF的组合是MINSTD(Minimal Standard)随机数生成器的参数,具有:

  • 完整周期:在不重复之前可以生成2^31-2个不同的数
  • 良好的统计特性

但LCG在密码学上是不安全的!

原因是:如果我们能观察到足够多的输出,就可以反推出内部状态。

5.4 攻击思路

问题: 我们只能看到state % 3的值,而不是完整的state。

解决方案: 使用约束求解!

假设我们观察到连续n个输出:h₁, h₂, h₃, …, hₙ

我们知道:

  • state₁ % 3 = h₁
  • state₂ % 3 = h₂
  • state₂ = 48271 * state₁ % 0x7FFFFFFF

5.5 单一约束的问题

state₁ % 3 = h₁,我们知道:

state₁ ∈ {h₁, h₁+3, h₁+6, h₁+9, ..., h₁+3k, ...}

这有大约0x7FFFFFFF / 3 ≈ 7亿个可能的值,太多了!

5.6 多重约束法

关键技巧是使用多个不同距离的观察值来建立独立的约束。

定义:

coeff(d) = 48271^d % 0x7FFFFFFF

那么:

state₁₊d = coeff(d) * state₁ % 0x7FFFFFFF

在模3意义下:

h₁₊d % 3 = (coeff(d) * h₁) % 3

等等,这不对,因为我们不知道完整的state₁,只知道state₁ % 3。

正确的方法:

如果state₁ = 3k + r₁(其中r₁ = h₁),那么:

state₁₊d = coeff(d) * (3k + r₁) % 0x7FFFFFFF
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;= (coeff(d) * 3k + coeff(d) * r₁) % 0x7FFFFFFF

在模3意义下:

h₁₊d = (coeff(d) * r₁) % 3 &nbsp;(当没有大的模运算影响时)

但实际上会更复杂,因为还要考虑模0x7FFFFFFF的影响。

实际的约束:

对于每个距离d和对应的系数coeff(d),我们有:

state₁ * coeff(d) % 0x7FFFFFFF % 3 = h₁₊d

这给state₁的可能值施加了约束。state₁必须满足:

(coeff(d) * state₁) % 0x7FFFFFFF ≡ h₁₊d (mod 3)

5.7 区间约束方法

更实用的方法是转换成区间约束:

如果state₁₊d % 3 = r,那么:

state₁₊d ∈ [0, 0x7FFFFFFF) 且 state₁₊d ≡ r (mod 3)

这意味着:

state₁₊d ∈ {r, r+3, r+6, ..., r+3k, ...} ∩ [0, 0x7FFFFFFF)

由于state₁₊d = coeff(d) * state₁ % 0x7FFFFFFF,我们可以反推state₁的范围。

具体来说,如果设x = state₁₊d % 3 = r,那么state₁₊d的可能取值是:

state₁₊d ∈ [(0x7FFFFFFF * x) / coeff(d), (0x7FFFFFFF * (x+1)) / coeff(d))

对应的state₁范围是通过求逆元计算得到。

5.8 算法实现

-- 收集8000个观察值
for&nbsp;i =&nbsp;1001&nbsp;to&nbsp;9000&nbsp;do
&nbsp; &nbsp; NZ_save[i-1000] = observe_cpu_hand()
end

-- 选择7个不同的距离d
distances = {1,&nbsp;282,&nbsp;2048,&nbsp;2718,&nbsp;3840,&nbsp;4763,&nbsp;7426}
coefficients = {48271,&nbsp;665722,&nbsp;560989,&nbsp;353062,&nbsp;248479,&nbsp;232463,&nbsp;64536}

-- 为每个系数建立约束
for&nbsp;k =&nbsp;1&nbsp;to&nbsp;7&nbsp;do
&nbsp; &nbsp;&nbsp;-- 计算期望的余数关系
&nbsp; &nbsp; diff = (NZ_save[2&nbsp;+ distances[k]] - coefficients[k] * NZ_save[2]) %&nbsp;3

&nbsp; &nbsp;&nbsp;-- 根据diff确定state₁可能的模3余数
&nbsp; &nbsp;&nbsp;if&nbsp;diff ==&nbsp;0&nbsp;then
&nbsp; &nbsp; &nbsp; &nbsp; NZ_x[k] =&nbsp;0
&nbsp; &nbsp;&nbsp;elseif&nbsp;diff ==&nbsp;2&nbsp;then
&nbsp; &nbsp; &nbsp; &nbsp; NZ_x[k] =&nbsp;1
&nbsp; &nbsp;&nbsp;else
&nbsp; &nbsp; &nbsp; &nbsp; NZ_x[k] =&nbsp;2
&nbsp; &nbsp;&nbsp;end
end

-- 求所有约束的交集
while&nbsp;true&nbsp;do
&nbsp; &nbsp;&nbsp;-- 对每个约束k,计算state₁的可能范围
&nbsp; &nbsp;&nbsp;for&nbsp;k =&nbsp;1&nbsp;to&nbsp;7&nbsp;do
&nbsp; &nbsp; &nbsp; &nbsp; x = NZ_x[k]
&nbsp; &nbsp; &nbsp; &nbsp; min_bound[k] = ceiling((M * x + coeff[k] -&nbsp;1) / coeff[k])
&nbsp; &nbsp; &nbsp; &nbsp; max_bound[k] =&nbsp;floor((M * (x +&nbsp;1)) / coeff[k])
&nbsp; &nbsp;&nbsp;end

&nbsp; &nbsp;&nbsp;-- 取交集
&nbsp; &nbsp; global_min =&nbsp;max(all min_bounds)
&nbsp; &nbsp; global_max =&nbsp;min(all max_bounds)

&nbsp; &nbsp;&nbsp;if&nbsp;global_min > global_max&nbsp;then
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;-- 无解,调整NZ_x的某些值(加3)
&nbsp; &nbsp; &nbsp; &nbsp; adjust_constraints()
&nbsp; &nbsp; &nbsp; &nbsp; continue
&nbsp; &nbsp;&nbsp;end

&nbsp; &nbsp;&nbsp;-- 在范围内搜索
&nbsp; &nbsp;&nbsp;for&nbsp;candidate = global_min to global_max step&nbsp;3&nbsp;do
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;verify_seed(candidate)&nbsp;then
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; found_seed = candidate
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;break
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;end
&nbsp; &nbsp;&nbsp;end

&nbsp; &nbsp;&nbsp;if&nbsp;found_seed&nbsp;then
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;break
&nbsp; &nbsp;&nbsp;end

&nbsp; &nbsp;&nbsp;-- 没找到,继续调整
&nbsp; &nbsp; adjust_constraints()
end

5.9 为什么需要8000个观察值?

  • 更多的观察值提供更多的验证机会
  • 可以确保找到的seed是正确的
  • 8000个值的序列足够长,几乎不可能有两个不同的seed产生相同的序列

5.10 完整代码

由于完整的Nozomi代码较长(约2000字节),超过了程序的大小限制,实际部署时可能需要优化代码。但核心算法如上所述。

第六步:AI类型识别

6.1 为什么需要识别?

每轮游戏开始时,服务器会随机选择一种AI(Slime、Alpaca或Nozomi)。我们的策略需要根据对手类型来选择。

6.2 识别策略

采用分阶段测试的方法:

阶段1(第1-500轮): 测试Slime策略

  • 使用Slime破解方法
  • 如果胜率>80%(赢了400+局),判定为Slime

阶段2(第501-1000轮): 测试Alpaca策略

  • 使用Alpaca破解方法
  • 如果胜率>80%(赢了400+局),判定为Alpaca

阶段3(第1001轮之后): 应用识别结果

  • 如果识别为Slime,继续用Slime策略
  • 如果识别为Alpaca,继续用Alpaca策略
  • 否则,使用Nozomi策略

6.3 为什么这个方法有效?

互斥性:

  • Slime策略对Slime有99%胜率,对其他AI约33%
  • Alpaca策略对Alpaca有99%胜率,对其他AI约33%
  • 80%的阈值可以清晰地区分

可靠性:

  • 500轮的样本量足够大,统计误差很小
  • 即使运气不好,500轮中也极难让33%的真实胜率达到80%

第七步:完整解决方案

7.1 完整代码(Slime + Alpaca版本)

STAGE =&nbsp;100000
INT =&nbsp;0x100000000
result =&nbsp;0
cnt =&nbsp;0

-- Slime AI破解:直接返回CPU上一手
function&nbsp;slime(x)
&nbsp; &nbsp;&nbsp;return&nbsp;x %&nbsp;3
end

-- Alpaca AI破解:溢出检测 + 状态估计
AL_last =&nbsp;0
AL_magic =&nbsp;0x61C88647
AL_guess =&nbsp;0

function&nbsp;alpaca(x)
&nbsp; &nbsp;&nbsp;-- 预测失败则重置
&nbsp; &nbsp;&nbsp;if&nbsp;AL_last&nbsp;and&nbsp;(result+1)%3&nbsp;~= x&nbsp;then
&nbsp; &nbsp; &nbsp; &nbsp; AL_guess = INT*2&nbsp;- (0x42000000&nbsp;+ AL_magic*2)
&nbsp; &nbsp;&nbsp;end

&nbsp; &nbsp;&nbsp;if&nbsp;AL_guess > AL_magic&nbsp;then
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;-- 不溢出
&nbsp; &nbsp; &nbsp; &nbsp; AL_guess = AL_guess - AL_magic
&nbsp; &nbsp; &nbsp; &nbsp; AL_last =&nbsp;1
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;(x+result+1) %&nbsp;3
&nbsp; &nbsp;&nbsp;else
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;-- 溢出
&nbsp; &nbsp; &nbsp; &nbsp; AL_guess = INT + AL_guess - AL_magic
&nbsp; &nbsp; &nbsp; &nbsp; AL_last =&nbsp;0
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;(x+result+2) %&nbsp;3
&nbsp; &nbsp;&nbsp;end
end

-- AI类型识别
sl_win =&nbsp;0
al_win =&nbsp;0

function&nbsp;play(x)
&nbsp; &nbsp; cnt = cnt +&nbsp;1
&nbsp; &nbsp; remain = cnt % STAGE

&nbsp; &nbsp;&nbsp;if&nbsp;remain ==&nbsp;1&nbsp;then
&nbsp; &nbsp; &nbsp; &nbsp; sl_win =&nbsp;0
&nbsp; &nbsp; &nbsp; &nbsp; al_win =&nbsp;0
&nbsp; &nbsp;&nbsp;end

&nbsp; &nbsp;&nbsp;-- 测试Slime(第1-500轮)
&nbsp; &nbsp;&nbsp;if&nbsp;remain <=&nbsp;500&nbsp;then
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(result+1) %&nbsp;3&nbsp;== x&nbsp;then
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sl_win = sl_win +&nbsp;1
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;end
&nbsp; &nbsp; &nbsp; &nbsp; result = slime(x)
&nbsp; &nbsp;&nbsp;-- 测试Alpaca(第501-1000轮)
&nbsp; &nbsp;&nbsp;elseif&nbsp;remain <=&nbsp;1000&nbsp;then
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(result+1) %&nbsp;3&nbsp;== x&nbsp;then
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; al_win = al_win +&nbsp;1
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;end
&nbsp; &nbsp; &nbsp; &nbsp; result = alpaca(x)
&nbsp; &nbsp;&nbsp;-- 应用识别结果(第1001轮之后)
&nbsp; &nbsp;&nbsp;else
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;sl_win >&nbsp;400&nbsp;then
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; result = slime(x)
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;else
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; result = alpaca(x)
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;end
&nbsp; &nbsp;&nbsp;end

&nbsp; &nbsp;&nbsp;return&nbsp;result
end
-- EOF

7.2 运行结果

$ cat solution_simple.lua | ./088a46725ff348b0b069728acf244243.elf

Welcome to luaky arena! Show us your AI:
Got code with size = 1140
Fighting with Slime...win = 99499, lose = 167, tie = 334 [0.02s]

Nice! Let's try something serious :)

Fighting with Alpaca-0...win = 99292, lose = 470, tie = 238 [0.02s]
Fighting with Alpaca-1...win = 99287, lose = 472, tie = 241 [0.02s]
Fighting with Alpaca-2...win = 99288, lose = 469, tie = 243 [0.02s]
Fighting with Alpaca-3...win = 99295, lose = 466, tie = 239 [0.02s]
Fighting with Alpaca-4...win = 99293, lose = 468, tie = 239 [0.02s]
Fighting with Alpaca-5...win = 99286, lose = 470, tie = 244 [0.02s]
Fighting with Alpaca-6...win = 99292, lose = 465, tie = 243 [0.02s]
Fighting with Alpaca-7...win = 99279, lose = 480, tie = 241 [0.02s]
Fighting with Alpaca-8...win = 99303, lose = 459, tie = 238 [0.02s]
Fighting with Alpaca-9...win = 99282, lose = 474, tie = 244 [0.02s]

You are luaky!

成功击败了Slime和所有Alpaca实例!

深入理解:密码学原理

8.1 为什么Slime不安全?

Slime使用的递增序列(n+1) % 3完全没有随机性:

  • 完全确定性: 知道任意一个值就能预测所有后续值
  • 周期性: 每3步就重复一次
  • 无熵: 没有任何不确定性

在密码学中,这种序列完全不能用作随机数。

8.2 为什么Alpaca不安全?

Alpaca使用的算法表面上看起来有随机性,但有几个致命弱点:

1. 模3后的信息泄露

0x9E3779B9 % 3 = 0

这意味着状态转移在模3意义下变得极其简单,失去了混淆性。

2. 溢出的可预测性32位整数溢出虽然看起来复杂,但实际上是完全确定性的,可以通过维护上下界来预测。

3. 依赖外部输入状态转移依赖于玩家的输入,这给了攻击者影响内部状态的能力。

8.3 为什么LCG不安全?

线性同余生成器在统计测试中表现良好,但在密码学上是不安全的:

1. 线性关系连续的输出值之间存在线性关系,可以通过线性代数方法求解。

2. 部分信息泄露即使只观察到输出的一部分(如模3的结果),仍然可以通过约束求解恢复内部状态。

3. 状态空间不够大虽然有2^31-1个状态,但如果每次观察提供约1.5比特的信息(因为有3个可能值),那么观察几十次就能唯一确定状态。

8.4 密码学安全的随机数生成器

真正安全的CSPRNG(Cryptographically Secure Pseudo-Random Number Generator)应该满足:

1. 不可预测性即使观察到任意多的历史输出,也无法预测下一个输出(在计算上不可行)。

2. 不可区分性输出序列与真随机序列在统计上不可区分。

3. 前向安全性即使当前状态泄露,也无法恢复之前的输出。

4. 后向安全性即使当前状态泄露,也无法预测未来的输出(需要定期重新种子)。

常见的安全PRNG包括:

  • /dev/urandom(Linux系统)
  • ChaCha20-based RNG
  • HMAC-DRBG
  • AES-CTR DRBG

实战技巧总结

9.1 逆向分析流程

  1. 静态分析
  • 使用filestringsldd等工具收集基本信息
  • 使用objdumpnm查看符号和反汇编
  • 识别使用的库和框架
  1. 动态分析
  • 运行程序观察行为
  • 使用strace跟踪系统调用
  • 使用ltrace跟踪库函数调用(如果可用)
  1. 接口探索
  • 通过试错确定正确的输入格式
  • 理解输入输出的含义
  • 建立程序行为的心智模型

9.2 密码学分析方法

  1. 识别算法类型
  • 查找已知的常数(如0x9E3779B9)
  • 观察输出的统计特性
  • 识别常见的密码学结构
  1. 寻找数学性质
  • 计算关键常数的性质(如模运算)
  • 寻找线性关系
  • 识别周期性或模式
  1. 构建攻击
  • 利用算法的弱点
  • 使用数学工具(线性代数、数论)
  • 通过实验验证假设

9.3 Lua编程技巧

  1. 变量作用域
  • Lua中不加local的变量是全局变量
  • 可以在多次调用间保持状态
  1. 模运算
  • %运算符在Lua中正确处理负数
  • 整数除法使用//(Lua 5.3+)或math.floor(a/b)
  1. 代码大小优化
  • 移除注释和多余空格
  • 使用短变量名
  • 合并表达式

总结与启示

10.1 题目亮点

这道题目巧妙地结合了:

  • 游戏理论: 石头剪刀布的简单规则
  • 伪随机数生成器: 三种不同复杂度的PRNG
  • 密码学攻击: 从简单到复杂的破解技术
  • 编程实现: Lua脚本的实战应用

10.2 关键收获

  1. 即使看起来随机的算法,也可能存在可利用的模式
  • 简单的递增序列
  • 数学常数的特殊性质
  • 线性关系的可预测性
  1. 部分信息泄露可能导致完全破解
  • 只观察到state % 3仍然可以恢复完整state
  • 约束求解是强大的工具
  1. 密码学中不要自己发明算法
  • 使用经过验证的标准算法
  • 简单的数学变换不足以提供安全性
  • 随机性的质量至关重要

10.3 实际应用

这道题目的技术在实际中的应用:

  1. 游戏安全: 破解游戏中的随机数生成器
  2. 协议分析: 识别和攻击弱加密
  3. 逆向工程: 理解和重现专有算法
  4. 渗透测试: 评估系统的随机性质量

10.4 进阶学习方向

如果你对这个主题感兴趣,可以继续学习:

  1. 密码学基础
  • 现代密码学原理
  • 流密码和分组密码
  • 密码学安全的定义
  1. 随机性测试
  • NIST随机性测试套件
  • Diehard测试
  • 统计分析方法
  1. CTF技巧
  • 更多密码学题目
  • 逆向工程
  • 脚本自动化
  1. 数学工具
  • 数论基础
  • 线性代数在密码学中的应用
  • 约束求解器(如Z3)

附录:完整代码清单

A.1 测试脚本

#!/bin/bash
# 测试Slime+Alpaca破解脚本

cat > test.lua <<&nbsp;'EOF'
STAGE = 100000
INT = 0x100000000
result = 0
cnt = 0

function&nbsp;slime(x)
&nbsp; &nbsp;&nbsp;return&nbsp;x % 3
end

AL_last = 0
AL_magic = 0x61C88647
AL_guess = 0

function&nbsp;alpaca(x)
&nbsp; &nbsp;&nbsp;if&nbsp;AL_last and (result+1)%3 ~= x&nbsp;then
&nbsp; &nbsp; &nbsp; &nbsp; AL_guess = INT*2 - (0x42000000 + AL_magic*2)
&nbsp; &nbsp; end
&nbsp; &nbsp;&nbsp;if&nbsp;AL_guess > AL_magic&nbsp;then
&nbsp; &nbsp; &nbsp; &nbsp; AL_guess = AL_guess - AL_magic
&nbsp; &nbsp; &nbsp; &nbsp; AL_last = 1
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;(x+result+1) % 3
&nbsp; &nbsp;&nbsp;else
&nbsp; &nbsp; &nbsp; &nbsp; AL_guess = INT + AL_guess - AL_magic
&nbsp; &nbsp; &nbsp; &nbsp; AL_last = 0
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;(x+result+2) % 3
&nbsp; &nbsp; end
end

sl_win = 0
al_win = 0

function&nbsp;play(x)
&nbsp; &nbsp; cnt = cnt + 1
&nbsp; &nbsp; remain = cnt % STAGE
&nbsp; &nbsp;&nbsp;if&nbsp;remain == 1&nbsp;then
&nbsp; &nbsp; &nbsp; &nbsp; sl_win = 0
&nbsp; &nbsp; &nbsp; &nbsp; al_win = 0
&nbsp; &nbsp; end
&nbsp; &nbsp;&nbsp;if&nbsp;remain <= 500&nbsp;then
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(result+1) % 3 == x&nbsp;then
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sl_win = sl_win + 1
&nbsp; &nbsp; &nbsp; &nbsp; end
&nbsp; &nbsp; &nbsp; &nbsp; result = slime(x)
&nbsp; &nbsp; elseif remain <= 1000&nbsp;then
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(result+1) % 3 == x&nbsp;then
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; al_win = al_win + 1
&nbsp; &nbsp; &nbsp; &nbsp; end
&nbsp; &nbsp; &nbsp; &nbsp; result = alpaca(x)
&nbsp; &nbsp;&nbsp;else
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;sl_win > 400&nbsp;then
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; result = slime(x)
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;else
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; result = alpaca(x)
&nbsp; &nbsp; &nbsp; &nbsp; end
&nbsp; &nbsp; end
&nbsp; &nbsp;&nbsp;return&nbsp;result
end
-- EOF
EOF

cat test.lua | ./088a46725ff348b0b069728acf244243.elf

A.2 验证克制关系的脚本

-- 验证石头剪刀布的克制关系
function&nbsp;test_rules()
&nbsp; &nbsp;&nbsp;-- 测试:我出0,CPU出哪个我会赢?
&nbsp; &nbsp;&nbsp;for&nbsp;cpu_hand =&nbsp;0,&nbsp;2&nbsp;do
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print("CPU: "&nbsp;.. cpu_hand ..&nbsp;", Me: 0")
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;-- 需要运行实际测试来确定
&nbsp; &nbsp;&nbsp;end
end

A.3 数学性质验证

# 验证关键数学性质
print("0x9E3779B9 % 3 =",&nbsp;0x9E3779B9&nbsp;%&nbsp;3) &nbsp;# 应该输出 0
print("2^32 % 3 =", (2**32) %&nbsp;3) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# 应该输出 1
print("48271^100 % 0x7FFFFFFF =", pow(48271,&nbsp;100,&nbsp;0x7FFFFFFF))

本文从零开始,详细讲解了HITCON 2017 CTF中Very Luacky题目的完整解题过程。通过逐步分析三种不同的伪随机数生成器,我们不仅解决了这道题,更深入理解了密码学中随机性的重要性以及常见PRNG的安全问题。希望这篇文章能帮助你在密码学和CTF的道路上更进一步。


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:破镜安全 破镜安全《HITCON 2017 CTF – Very Luacky 完全解析》

评论:0   参与:  0