直播抽奖
之前,我写了一篇文章介绍了公平抽奖的方法,python公平抽奖
其中,介绍到,对于公平没有黑幕的抽奖,一个较好的方法是,选择一个不可操控的内容如股票价格,或者彩票开奖号码作为随机数种子,以此来生成随机数,是较为公平的。因为,一旦这些内容被公布,那么结果也会随之产生,是公开可验证的。
有人看完这篇文章以后,提出了这样一个疑问,“这个方法虽然有道理,但是太慢了,例如,我们需要花时间去等待股票市场出结果,无法做到随时随地,立刻得到抽奖结果,所以是不方便的。因此,我认为,抽奖无需这么麻烦,只需要通过python生成随机数就可以了。只需要通过直播现场编程,然后当着所有人的面运行程序,确保不会重复运行,然后用直播中的结果,作为最终结果就可以了。不需要那么麻烦。”
总结来说就是,他认为,只需要通过直播的方式,在有观众监督的情况下,运行python代码,并得到结果,就是安全可靠,无“黑幕”的了,但是,真的是这样吗?
篡改程序文件
原理
python允许用户编写与标准库同名的模块,例如,如果在当前目录下存在一个名为random.py
的文件,就会优先加载这个文件,而不是标准库中的random
模块。
如果有人提前编写了一个伪造的random.py
,用于实现固定输出或其他不公平逻辑,即使抽奖的代码表面上使用了标准库的random
模块,实际上使用的却是伪造的模块。
并且,大多数情况下,抽奖的人公开展示了代码,并且当面进行了运行,就足以表明他们采用了“公平的方式”进行抽奖,通常不可能有人对其设备和运行环境进行详细而细致的检查,所以,这样的作弊行为是容易实现的。况且,由于绝大多数人对代码和运行环境的内在机制并不了解,看到结果是随机产生的,他们通常不会怀疑其中是否存在幕后操纵。
而在抽奖电脑上进行篡改代码是一种隐蔽且直接的操作,普通人很难察觉。即使检查相关代码,也看不出什么问题,因为伪造的模块可能隐藏在文件系统的其他地方。
创建同名文件
假设,我们在当前目录下创建一个文件,起名为random.py,此时,在该文件中定义一个randint
# 一个简单的模拟案例,已经事先选定了让42中奖
def randint(a, b):
if a <= 42 <= b:
return 42
else:
return a
# 假装接受一个随机数种子
# 但是什么都不做
def seed(a=None, version=2):
return
此时,如果按照平常的方式使用randint,那么
import random
import time
random.seed(time.time())
user = random.randint(1, 100)
print("中奖用户是:", user)
按照原本的python内置库,是可以选取1-100的任意用户的。但是引入了新的同名文件以后,就只会选取内定用户。
将伪造代码藏到解析路径中
由于此时random.py就在当前目录,看起来还是比较可疑,容易暴露的,因此,可以将其转移到其他位置。
通过sys.path
查看import的解析路径。
import sys
print(sys.path)
然后将random.py
文件,移动到解析路径的其他位置,此时,在当前目录就不会再显示该文件,但是依然可以被替换解析。
如何证明调用的是标准库
标准库哈希值
如果为了证明调用的是标准库,可以使用hashlib,对random进行校验,确保其没有被篡改。
import hashlib
import random
def verify_module(module):
current_hash = hashlib.md5(open(module.__file__, "rb").read()).hexdigest()
return current_hash
# 注意:实际上,对于不同版本的python,该值可能是不同的
print(verify_module(random))
打印模块加载路径
另一个直接的方法是,通过打印模块加载路径,表明模块使用的是正常路径下的标准库,而不是从其他非常规路径加载来的。
import random
print("random路径:", random.__file__)
新的问题
此时有人会提出疑问:既然已经篡改了random模块的内容,自然也可以篡改__file__
,这样的证明有什么用呢?那又要如何证明__file__
没被篡改呢?
当然,这种质疑是对的,确实__file__
自然也是可以被篡改的,因为,这样的检测方法,只能在作弊者没有想到的情况下,突然出击,才能发挥作用,如果作弊者已经考虑到了,那么是不可靠的。
总结
如果没有可靠的第三方作为监督,并且经过严密的预防措施,即使是通过直播的方式,当面编写代码,并且现场运行代码给所有人看,结果也未必是可靠的。
不过,因为担心存在“黑幕”,而不愿意参加抽奖也并不明智,因为,“抽了不一定中,但是不抽一定不中。”
P.S. 相比于普通的random.randint()
,并且设置时间作为随机数种子,直接使用系统随机数可能更好。
import random
secure_random = random.SystemRandom().randint(1, 2)
print(secure_random)
该方法不需要设置随机数种子,因为其并不依赖于python内部的随机数生成器,而是使用更可靠的操作系统提供的随机数生成器。