前言

协会应学校要求搞了个线上CTF,虽然都是基础题,但是目的是为了让大家疫情期间在宿舍不要太无聊,找点事情做,同时也是寓教于乐的一次欢乐CTF,重在参与就行

题目确实挺欢乐的,就是要做大作业,看论文啥的,脑子都是糊的,只能吃饭的时候看看题

不过,辛苦0xb14cb12d同学了,疫情期间还要这么折腾

做个核酸签个到

题目描述:核酸检测 是直接找到病毒存在的证据,人如果感染了病毒,在咽部的呼吸道里面会有病毒的残骸,若是经过涂抹采集出的样本里或者痰液里分离出了病毒残骸,也就是说检测结果呈阳性,则证明该人已经感染了病毒。因此,它作为诊断新冠肺炎的一个标准,具有非常重要的意义。

疫情期间,做核酸是一件很 重要 的事情!要 对自己负责 !也要 对别人负责
因此,0xb14cb12d同学作为核酸检测的志愿者,接下来要引导你去做核酸

做完核酸以后 不要忘了 西安加油!!!!

非常鼓舞人心的题目描述,直接看文件吧

flag = bytes_to_long(flag)

tmp = getRandomNBitInteger(142)
a = flag | tmp
b = flag & tmp
print(tmp)
print(a)
print(b)

'''OUTPUT
4475588893486760807434877361949655702156202
10403436853134845129656953606837707260539903
2994485173442830774576326061629704337957160
'''

一个按位或,一个按位与

简单分析一下

如果flag是1,tmp是1,则a和b都是1

如果flag是1,tmp是0,则a是1,b是0

如果flag是0,tmp是1,则a是1,b是0

如果flag是0,tmp是0,则a是0,b也是0

不难看出,把flag和tmp和a和b全部异或起来,结果是0,因此flag就是把tmp和a和b按字节异或一下就可以了

写脚本跑即可

tmp = long_to_bytes(4475588893486760807434877361949655702156202)
a = long_to_bytes(10403436853134845129656953606837707260539903)
b=long_to_bytes(2994485173442830774576326061629704337957160)
s=''
for i in range(len(tmp)):
   s+=(chr(tmp[i]^a[i]^b[i]))

print(s)

Random?Secure?Algorithm?

题目描述:0xb14cb12d最近学习了随机数,他想要使用自己构造的随机数生成方式去使用一种 非对称加密算法 ,素数只要足够大就能安全,认为该方法 相当的 安全。

但是你看了他的算法之后,你在思考通过随机数生成的算法会是真的 安全 吗?

如果没有什么思路的话,你可以试着百度一下这道题题目的 大写内容 & 素数

题目的已经提示的很明显了,RSA,直接看代码吧

def gen_prime():
    bits = randrange(200,500)
    tmp = 1 << bits
    while 1:
        if isPrime(tmp - 1):
            return tmp - 1 
        tmp <<= 1

p = gen_prime()
q = gen_prime()
r = gen_prime()
s = gen_prime()
n = p * q * r * s

e = 0x10001
c = pow(bytes_to_long(flag),e,n)
print(n)
print(c)
# n=2220807746894627523222202124776163478527976105639908696684960749126424512881854877612451343059455214098898034661237728063590623533844736775173121931789755672281308428039481610223926648486535537266518592523804878106456736231116970493024909507160799408290646002104567544126920953422128397093317788717069423812791560021066430498563457374609893334361756465420330176244989676586479166336218556641172090718482194292197412911139771273274714095735297058566121733012205906499181231909338887015496742596987502746140740611885150614938360456497893944529865631525824696926783090332719331167229992334443881034786985550920319985734221666713601

# c=634057945874353594022022069483974274802345710976798102667748679310082300326072415452246707696914695264376634375433404430691377289430628864830705005890158416150698258253919718484678774288826541601787179699274130877838178274549997680506212326443386524962637463047111837598586603070301100227308868294313317591522323170042363202978135769499399961383546980034634956104147199359999671017544747241485716932828394760511756379878402551287792764280949085295956683651929873602289520407522116635348927049436823594743766190445303191020310627575176711547105464725518638312131699845488260895331033189716088924897737884915591463313831762560128

有一个诡异的gen_prime函数,然后模数是通过调用四次这个函数得到的

尝试print一下这四个素数,发现四个都是一样的,都是

p=q=r=s=6864797660130609714981900799081393217269435300143305409394463459185543183397656052122559640661454554977296311391480858037121987999716643812574028291115057151

或者print一下n,然后丢到factordb里面,网站会返回n是上面这个数的四次幂

那就直接调库反解就好了,而且因为模数是四个一样的素数相乘,因此最后可以拆成一个素数,具体来说就是下面这个样子

$$ n=p*q*r*s \\ c=flag^e \ (mod \ n) \\ m = c^d \ (mod \ n)=flag^{ed}\ (mod \ n) $$

其中,第二条式子可以拆成下面这样

$$ ed \equiv 1 (mod \ \phi(n)) $$

但是这里四个素数都是一样的,所以只用求一个的逆就好了

$$ ed \equiv 1 (mod \ \phi(p)) $$

上脚本,结果如下

e = 0x10001
d=int(gmpy2.invert(e,(p-1)))
print(d)
# d=5155328233496318390256865764810548697290840245466729669258087920880340553538954146776874404297340568416582885358721232128614872261196790492751513654940106623

然后解密的时候用p作为模数而不用原来的n,最后把long转成byte即可得到flag

m=pow(c,d,p)
print(long_to_bytes(m))

too_weak!

Fight_Against_Covid

题目描述:2021年12月 ,一场不见硝烟的战争于中国 陕西省西安市 打响,不宣而战。“新型冠状病毒”这个谈虎色变的字眼,跳跃到我们的面前,来势汹汹。

这是一场生命与病毒的抗争。成千上万的医护工作者坚守在抗战一线,也有更多的医护工作者纷纷请战,勇敢“逆行”在抗战一线。

昨日,张医生在检验核酸时发现一阳性病例。通过隔离该患者并调查分析其近期生活情况,分析出该患者与一位相对较早的已知患者在时空上大致有4次接触,这4次接触之间间隔时间内刚好每次都有核酸记录,最后一次检查出阳性。

张医生目前手中只有第四次样本,而作为志愿者的0xb14cb12d同学,为了帮助张医生,挺身而出,帮助他分析样本,但是他卡在一块令人头疼的 逻辑 上,并且向你发起了求助

非常鼓舞人心的题目描述,直接看文件

assert len(flag) == 32
BITS_LEN = 32 * 8
APART_LEN = 1
TIMES = 4

class CHALLENGE:
    def __init__(self,state):
        self.state = state

    def transmission_of_COVID(self):
        for i in range(TIMES):
            tmp1 = self.state >> APART_LEN
            tmp2 = self.state >> (BITS_LEN - APART_LEN) << (BITS_LEN - APART_LEN) 
            tmp_state = tmp1 ^ tmp2
            self.state  = tmp_state ^ self.state
        return self.state

if __name__ == "__main__":
    flag = bytes_to_long(flag)
    seed_man = flag 
    cha = CHALLENGE(seed_man)
    Infected_man = cha.transmission_of_COVID()
    print(Infected_man)

# Output:
# 43440857574112066559373619969745981593504262639540191600960458557114603451242

代码很明确的告诉了flag是32字节的,先随便写一个flag{xxxxxxx}试一下,并尝试在循环内打印一下两个临时变量

tmp1很好理解,每次循环右移一位,tmp2看不懂没关系,打印出来是0,也就是说整个循环操作和下面这段代码是等价的

flag = bytes_to_long(flag)
    for i in range(4):
        flag ^= flag >> 1

实际上最后的结果就是从第二个HEX位开始,每一个HEX位都与它前一个HEX位异或,那么,反解即可

得到flag{W3_w1ll_b34t_the_COVID-19!}

AweSomeSha3!

题目描述:0xb14cb12d同学 又又又又 来啦!

今天,0xb14cb12d同学想要使用他的 Super Power & Tricks 去攻破 SHA3

当然,这在目前看来,必不可能!但是 在这里 ,他竟然 成功 了?!?!

WSM ?? WSM ?? WSM ??

在你百思不得其解的时候,0xb14cb12d 同学告诉你了一个小秘密

偷偷告诉你,你可以使用一种神奇的构造的方法,让Hash的值都变成你想要的喔,我可是逻辑大师!

这题用nc命令和服务器交互,先来看一下核心代码

xor_a_b = lambda a,b:bytes([i ^ j for i,j in zip(a,b)])
and_a_b = lambda a,b:bytes([i & j for i,j in zip(a,b)])
mix_a_b = lambda a,b:bytes([a[i%len(a)]    for i in b])

msg = bytes.fromhex(self.recv().decode())
magic1 = os.urandom(len(flag))
magic2 = os.urandom(len(flag))

if len(msg) < len(flag):
    msg =  msg + os.urandom(len(flag) - len(msg))
my_and = xor_a_b(xor_a_b(xor_a_b(xor_a_b(and_a_b(msg,flag),magic1),magic2),magic2),magic1)
my_xor = xor_a_b(xor_a_b(xor_a_b(xor_a_b(xor_a_b(msg,flag),magic1),magic2),magic2),magic1)

my_mix = mix_a_b(my_xor,my_and)
the_real_mix = sha3_512(my_mix).hexdigest()

首先是三个lambda,第一个是按字节异或,第二个是按字节与,第三个是一个映射函数,把b中的字节映射到a

然后观察第九行,有一个填充判断,如果msg比flag短,则会填充一段随机数据,因此首要任务是推断出flag的长度

注意到题目需要我们传入消息的hex编码

因此,一个字节需要两个hex字符表示,因此我们传入的消息必须是2的正整数倍,如果传入的长度为0或奇数服务器不会回显

for i in range(1,50):
    __hex='00'*i
    s.send(__hex.encode())
    data=s.recv(2048)
    print(i)
    print(data)

写个脚本跑一下,传一下00的正整数倍,然后发现在i=39的时候服务器返回的sha3值不再变化了,因此可以推断flag长度为39 bytes

继续观察代码,第六第七行生成了两个随机字节,用于第11和12行的按字节与和按字节异或

但是观察到,其调用的都是按字节异或的lambda,而且magic1和magic2都异或了两次,因此这一段是迷惑人的,异或两次相当于没有异或

最后是一个映射的lambda,对flag与msg进行and和xor运算的结果进行映射, 映射结果作为sha3的输入

然后回看一下题目提示,可以通过一个巧妙地方法构造成我们想要的sha3值

因此尝试传入一个全零的串,全零和flag与运算后,会把所有位置零,和flag进行异或之后,会保持flag原有的位不变,最后mix的时候就可以映射flag中指定的位了

然后我这里的做法是让flag=falg{1234567890ABCDEFGBIJKLMNOPQRSTUVW},然后通过把全零的串的部分位置替换位其他hex值,然后观察flag中字符的映射情况

逐位试了一下,发现当替换最后一个字节的时候,也就是将最后一个0x00(一共39个0x00)替换成其他hex值的时候,mix的结果可以映射除了反花括号以外的所有flag的位

然后再回看mix的lambda和调用方式

mix_a_b = lambda a,b:bytes([a[i%len(a)]    for i in b])
my_mix = mix_a_b(my_xor,my_and)

因为全零和flag与运算后,会把所有位置零,因此除了最后一个0x00变化,其他均为0的情况下,mix的结果只有最后一位会变化,其余的均只会映射xor结果中的第一位(0x00为xor结果中的第一位),也就是flag中的第一位,也就是字符f

小结一下,到这一步,我们就可以得到了一个39 bytes的串,其中前38 bytes表示字符f,最后一个byte会根据我们传入的值的不同而变化

但是服务器只返回mix结果的sha3值,因此需要跑一下生成字符串与sha3值对应的字典,为了考虑flag中可能出现的一些奇怪的标点符号,这里将最后一个字符替换为所有可打印字符,也就是替换成0x20~0x7e

然后再对其进行一个sha3的hash,就可以得到一个字典

然后再构造串,写脚本和服务器交互拿到对应的sha3值,最后比对字典即可

Bob的健康码1

那天,Bob发现西安一码通居然崩溃了
Bob:这健康码崩溃啊,一定是他们写的不行,看我来写一个
Alice:健康码哪有这么简单就能写好啊
Bob:看,我这不就写出来一个了吗
Alice:你这健康码也太不安全了,大家用你写的,万一有个不法分子伪造绿码,要出大事了。
Bob:不可能,我这就把你标红码,有本事你就来啊。

你能帮帮Alice伪造出合法的绿码吗

依然是通过命令和服务器交互拿到flag

根据题目信息应该是一个绕过的题,有点像bugku和攻防里面的某些缝合题字符串的题

先来看一下核心代码

    def qrdecode(self , qr , size):
        qr = b64decode(qr)
        code = Image.frombytes('1' , size,qr)
        m = pyzbar.decode(code)
        return m[0].data

    def qrencode(self , data):
        qr = qrcode.QRCode(
            error_correction = qrcode.ERROR_CORRECT_L,
            border=1,
            box_size=1
        )
        qr.add_data(data)
        qr.make(fit=True)
        img = qr.make_image()
        self._send(b64encode(img.tobytes())) # 将制作好的二维码编码成base64后发送

    def send_hcode(self, name):
        if name not in database:
            self._send(b'this one is not in the database') # 用户名必须是alice bob carol david四个中的一个
        else:
            data = {
                'name':name, 
                'time':int(time.time()),    # 时间戳,和当前时间有关
                'isRed':database[name]    # 根据用户名确定健康码的颜色,1代表红色,0代表绿色,只有alice是红色
            }
            data = json.dumps(data)    # 将json格式的数据转为字符串
            self.qrencode(data)    # 将字符串传给qrcode函数制作二维码

    def checker(self , data):
        data = json.loads(data.decode())
        delta_time = time.time() - data['time']
        if delta_time < 0 or delta_time > 30: # 超时检测,需要在三十秒内完成
            self._send(b'this health code has been overdue')
            return 0
        if data['isRed'] == 1:
            self._send(b'No! your health code is red!')
            return 0
        self._send('hi '+data['name']+' your health code is green and you are healthy.')
        if data['name'] == 'alice':
            return 1
        else:
            return 0
        
    # 主线程
    def handle(self):
        signal.alarm(120)
        while 1:
            self._send(menu)
            choice = int(self._recv())
            if choice == 1:
                self._send(b'who are you')
                name = self._recv()
                self.send_hcode(name.decode())
            elif choice == 2:
                self._send(b'size:')
                size = int(self._recv()) # the height or the width of the qrcode
                self._send(b'your health code(in base64):')
                qr = self._recv()
                data = self.qrdecode(qr , (size,size))
                if self.checker(data) == 1:
                    self._send(flag)
            else:
                self._send(b'wrong!')
                exit(0)

分析服务器逻辑可以知道整个过程进行了两次base64编码,第一次是对用户信息进行编码,第二次是对二维码图片进行编码,解码过程一样,需要两次base64解码,然后从中得到用户数据

(这里吐槽一下某市的健康码系统,传输的是图片而不是图片编码)

那么就很简单了,我们只需要先get到alice的二维码,然后把里面健康码字段由1改成0,然后重新编码,发送回服务端就可以了,但是需要注意,服务端有超市检测,需要在三十秒内完成,因此直接脚本跑就行

Bob的健康码2

Bob:行行行,之前的系统确实是太简陋了,我这次随手查了一下,查到一个 aes 的加密算法特别强,就算你有现在地球上所有的算力,你也没法在百年之内得到我的密钥,这下你总没办法了吧。

Alice:aes确实很安全,但你好像用的是 ecb 分组模式呢。aes-ecb可不能保证密文完整性哦

题目描述的意思就是,之前的太简单了,简单的缝合就行,因此第二题改成了用对称加密的方式,还是先看一下服务器逻辑

这里省去了qrencode和qrdecode函数,内容和上一题的一样,都是对数据进行编码和解码

    def send_hcode(self, name , key):
        if name not in database:
            self._send(b'this one is not in the database')
        else:
            data = {
                'name':name,
                'time':int(time.time()),
                'isRed':database[name]
            }
            data = json.dumps(data).encode() # 到这里和之前一样,把json数据转成字符串,然后为了aes加密,需要编码成byte
            aes = AES.new(key = key , mode = AES.MODE_ECB) # what is aes? what is ecb? try baidu or google to search it.
            # 这里用的AES_ECB模式
            c = aes.encrypt(pad(data)) # 填充后加密
            self.qrencode(b64encode(c))    # 加密后的数据编码成base64,在制作成二维码
    def checker(self , data , key):
        data = b64decode(data)    # 第二层base64解码,第一层解码在qrdecode函数里面
        aes = AES.new(key = key , mode = AES.MODE_ECB)
        data = aes.decrypt(data)    # AES_ECB解密
        data = unpad(data)        #移除填充
        data = json.loads(data.decode())    
        delta_time = time.time() - data['time']
        if delta_time < 0 or delta_time > 30:
            self._send(b'this health code has been overdue')
            return 0
        if data['isRed'] == 1:
            self._send(b'No! your health code is red!')
            return 0
        self._send('hi '+data['name']+' your health code is green and you are healthy.')
        if data['name'] == 'alice':
            return 1
        else:
            return 0

     # 主线程
    def handle(self):
        
        signal.alarm(120)
        key = os.urandom(16)
        while 1:
            self._send(menu)
            choice = int(self._recv())
            if choice == 1:
                self._send(b'who are you')
                name = self._recv()
                self.send_hcode(name.decode() , key)
            elif choice == 2:
                self._send(b'size:')
                size = int(self._recv()) # the height or the width of the qrcode
                self._send(b'your health code(in base64):')
                qr = self._recv()
                data = self.qrdecode(qr , (size,size))
                if self.checker(data , key) == 1:
                    self._send(flag)
            else:
                self._send(b'wrong!')
                exit(0)

首先看一下主线程,第57行,发现密钥的生成是在循环外的,因此每次和服务器进行交互的时候,实际上加解密的密钥是固定的

然后看一下AES_ECB模式,这个模式的特性是每一个分组都是独立加密与解密的,不同的分组之间的加解密没有关联性(CBC等模式会有关联性)

但是和上一题的区别在于,我们不知道密钥,就没有办法再修改alice的数据了,但是由于AES_ECB的特性,我们可以将别人的绿码的数据截取下来,然后再拼接上alice的名字就可以了

之前提到了密钥key的生成是在while循环外的,因此这个方法不会造成解密不出来的情况,只要是在同一次交互中拿到的两个二维码的数据,它们用的加解密密钥都是一样的,因此可以正常解密

然后AES_ECB模式默认应该是128 bits,也就是每次加密十六个bytes,因此第一块的内容应该是{'name':alice,'ti,没有包含到alice的健康码的状态信息,但是包含到了alice的名字,因此后面的只要缝上一个别人的绿码的信息就可以了

需要注意的是,alice名字五个字符,bob三个字符,如果用bob的二维码,则第一块会包含更多的信息,导致解密之后不能构成完整的json格式的串

因此需要找一个和alice名字一样长的,获取那个人的二维码,这里用carol或者david都行,都是五个字符

具体流程如下

第一步:先获取alice的二维码,两次base64解码之后得到被AES_ECB模式加密后的字节序列,截取前16 bytes

第二步:获取carol或者david的二维码,两次base64解码后得到对应的字节序列,去掉前16 bytes,保留剩下的字节

第三步:将第一步的16 bytes与第二部中剩余的bytes拼接起来,注意第一步的16 bytes在前面

第四步:将第三部中的字节序列进行两次base64编码,然后发送给服务器,即可得到flag

一段很长的flag,56个字符

Bob的健康码3

Bob:唔,之前这个系统是不安全,但我想了想,检测核酸的人员好像有很多,基于对称密码算法的系统就一定得让他们知道密钥,如果哪天有临时检测核酸的人知道了密钥,那他就能伪造绿码了。但还好,我在密码学书上找到了基于 公钥密码系统数字签名 方案elgamal。这下用它就能解决这个问题了。

Alice:数字签名是一个好办法,但你的elgamal好像实现的不太标准呢

还没做,不想动脑子了,思路应该和前两题差不多,只不过改成了elgamal,应该还是缝一段数据到alice那里