分析SECCON CTF 2019中Crypto方向题目
2019-12-24 10:34:39 Author: xz.aliyun.com(查看原文) 阅读量:299 收藏

前言

在SECCON CTF 2019中有3道Crypto方向的题目,题目相对比较简单,在这里对题目进行一下分析。

coffee_break

题目描述如下:

The program "encrypt.py" gets one string argument and outputs ciphertext.

Example:

$ python encrypt.py "test_text"
gYYpbhlXwuM59PtV1qctnQ==
The following text is ciphertext with "encrypt.py".

FyRyZNBO2MG6ncd3hEkC/yeYKUseI/CxYoZiIeV2fe/Jmtwx+WbWmU1gtMX9m905

encrypted.py

分析一下源码,发现题目流程非常直接,key1是自定义函数encrypt的密钥,key2是AES的密钥,且key1、key2均已知,题目将flag先做一次encrypt,然后对结果做padding,再进行AES加密(采用ECB模式,同时对key2也进行padding)。由于key2和padding的规则均已知,那么我们很容易就可以恢复出enc1,那么接下来就是写encrypt函数的逆decrypt函数了。

decrypt函数很好写,改成减法就可以了:

def decrypt(key, text):
    s = ''
    for i in range(len(text)):
        s += chr((((ord(text[i]) - 0x20) - (ord(key[i % len(key)]) - 0x20)) % (0x7e - 0x20 + 1)) + 0x20)
    return s

最后写成solver如下:

from Crypto.Cipher import AES
import base64

def decrypt(key, text):
    s = ''
    for i in range(len(text)):
        s += chr((((ord(text[i]) - 0x20) - (ord(key[i % len(key)]) - 0x20)) % (0x7e - 0x20 + 1)) + 0x20)
    return s

c = 'FyRyZNBO2MG6ncd3hEkC/yeYKUseI/CxYoZiIeV2fe/Jmtwx+WbWmU1gtMX9m905'

key1 = "SECCON"
key2 = "seccon2019"

c = base64.b64decode(c.encode('ascii'))
cipher = AES.new(key2 + chr(0x00) * (16 - (len(key2) % 16)), AES.MODE_ECB)
enc1 = cipher.decrypt(c)
print(decrypt(key1, enc1))

执行脚本即可得到flag:

SECCON{Success_Decryption_Yeah_Yeah_SECCON}

ZKPay

题目只给了一个web链接,访问链接之后界面如下:

有注册页面,那我们随便注册一个tom用户,注册之后登陆,界面如下:

简单浏览一下就会发现网站提供给了我们三个功能:

第一个页面(Home)展示当前的余额以及收款记录,初始有500余额,如果我们的余额>1000000那么就会显示flag。
第二个页面(Send)允许我们转账给他人,我们输入一个金额后会生成一个二维码,别人扫了这个二维码,就会收到我们的转账。
第三个页面(Receive)允许我们收款,我们扫了别人转账的二维码,我们就会收到这一笔金额。

我们尝试输入100,成功生成一张二维码如下:

我们尝试输入大于我们当前余额的金额,比如1000,则会提示Don't cheat!,同时二维码生成失败:

那么我们识别一下生成的转账100的二维码的内容,发现得到内容如下:

username=bob001&amount=100&proof=MHp0dPknFIRzzPKQI7QyNrqB3eaYrmt1zBRDKQZZmOAlMCAw6BA66tCUJUCGsuIE0eSyN5yFxcbRuf1x7HkaXFNoKiYxCjCsrZvaYHK0r5I3t5INyvLN7VDwKxyxm2PZPqDGnXz+KnO6P6tHXK+g/ZGyUz8+V+A/xG6y5E1nbrR4+PoMIL0KMSAwG2en1UBWnIayMomcoQfm9ajwU4fUQz91sIYfkiwKCwcxCjBlmnbjpYZUs4BQcUS16JjOrnjRRFPqC3UgCm4VQgeHLDEgMHdwhL+9bMK+UjLe0LwHdujShpbE7HMarWGhrrSmVqMRMQowuWBl8E3VdZEy4y5H1uBwcd27qUN1DFzW43SSD9YOYywwCjBbRw4C+aG3ulLAWOWunjPOaW95iJLRoWuQ5s7Nuu1/BTEK&hash=a7dc02e0891c6af6e6e47cdd41618822ce076b6dacfd9beb2025ace7613bd94b

其中usernameamount分别是我们当前的用户名和要转账的金额,proofhash两个参数我们暂时无法理解,因此我们尝试再生成一张转账200的二维码并再次识别,得到结果如下:

username=bob001&amount=200&proof=MHp0dPknFIRzzPKQI7QyNrqB3eaYrmt1zBRDKQZZmOAlMCAw6BA66tCUJUCGsuIE0eSyN5yFxcbRuf1x7HkaXFNoKiYxCjCsrZvaYHK0r5I3t5INyvLN7VDwKxyxm2PZPqDGnXz+KnO6P6tHXK+g/ZGyUz8+V+A/xG6y5E1nbrR4+PoMIL0KMSAwG2en1UBWnIayMomcoQfm9ajwU4fUQz91sIYfkiwKCwcxCjBlmnbjpYZUs4BQcUS16JjOrnjRRFPqC3UgCm4VQgeHLDEgMHdwhL+9bMK+UjLe0LwHdujShpbE7HMarWGhrrSmVqMRMQowuWBl8E3VdZEy4y5H1uBwcd27qUN1DFzW43SSD9YOYywwCjBbRw4C+aG3ulLAWOWunjPOaW95iJLRoWuQ5s7Nuu1/BTEK&hash=a7dc02e0891c6af6e6e47cdd41618822ce076b6dacfd9beb2025ace7613bd94b

和第一次的识别结果比较一下就会发现,除了amount参数变化了以外,其余参数均不变化,那么我们尝试直接将amount改为1000000,然后再次注册一个账号收款,看能否收到这笔1000000的汇款:

可以看到系统再次提示我们no cheat!,我们猜测它的check逻辑很有可能是判断当前余额是否大于转账金额,如果是的话转账通过,否则转账失败,这里bob001的账户余额小于1000000,因此在alice001这个账户这里无法接受这笔汇款。

那我们可以换个思路想一想,如果我们把amount参数改为负数,比如-1000000,那么再check的时候,由于转账金额是负数,那么当前余额确实大于转账金额,就可以通过check,此时我们再用另外一个账号来收款,这样发起转账的一方由于扣款数为负,相当于增加了余额,就可以以此来达成题目条件。我们尝试将amount的值改为-1000000,重新生成一张二维码并使用alice001账户来接收这笔汇款:

可以看到这笔转账成功了,alice001的账户余额变为了-999500元:

此时我们回到bob001账户,可以看到我们的账户余额变为了1000500,当然也就拿到了flag:

flag:

SECCON{y0u_know_n07h1ng_3xcep7_7he_f4ct_th47_1_kn0w}

Crazy Repetition of Codes

题目描述如下:

I'm crazy about googology!

crc.py
requirements.txt

打开requirements.txt可以看到题目告诉我们pycrypto的版本是2.6.1,这个就是明确了一下crc.py中使用的pycrypto的版本(即from Crypto.Cipher import AES那行),我们参考一下即可。

然后我们打开crc.py,简单审计一下源码,可以发现题目的逻辑很简单,key是由连续6次CRC32值的bytes形式拼接而成的,然后使用这个key对flag进行了AES加密,那么我们的任务很明确,就是求出key就可以了。

继续看一下可以发现每次CRC32操作的数据都是已知的(依次为"TSG", "is", "here", "at", "SECCON", "CTF!"这6个字符串),而每次crc的值都是由自身反复更新得到的(初始值为0),最后把经过int("1" * 10000)次CRC32循环操作得到的最终值作为key的一部分,这里注意int("1" * 10000)是一个相当巨大的数字,我们是不可能真的跑这么多次循环然后直接看一下crc的最终值的,因此肯定要进行化简。

正如CRC32的名字那样,我们每次CRC32操作的输出是32比特的数,那么根据鸽巢原理,在大约2*32次步骤之后肯定会有循环出现,即crc的值又会回到初值0,那么我们就可以通过遍历先看一下多少次循环后crc的值又变回0,把这个当做一个循环周期,然后把`int("1" 10000)%循环周期`当做是真正的有效循环,这样就可以直接做有效循环然后看crc的最终值是多少,然后把得到的结果依次拼接形成key再拿去做AES解密,即可得到flag。

但是在写脚本的时候我们会发现,即时这种方法用python跑仍然太慢了,那么我们可以尝试写成C++的solver来跑,这里给出一版C++的solver,首先我们把CRC32的周期计算出来(6次的周期都是一样的):

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<stdint.h>

typedef unsigned int uint;
uint POLYNOMIAL = 0xEDB88320;
int have_table = 0;
uint table[256];

void make_crc32_table(){
    int i, j, crc;
    have_table = 1;
    for (i = 0 ; i < 256 ; i++)
        for (j = 0, table[i] = i ; j < 8 ; j++)
            table[i] = (table[i]>>1)^((table[i]&1)?POLYNOMIAL:0);
}

uint crc32(uint crc,char *buff, int len){
    if (!have_table) make_crc32_table();
    crc = ~crc;
    for (int i = 0; i < len; i++)
        crc = (crc >> 8) ^ table[(crc ^ buff[i]) & 0xff];
    return ~crc;
}

int main(){
    char * buf = "TSG";
    uint32_t _crc32 = 0;
    make_crc32_table();
    int i = 0;
    unsigned int cycle = 1;
    while(1) {
        _crc32 = crc32(_crc32, buf, 3);
        if (_crc32 == 0) {
            break;
        }
        cycle ++;
    }
    printf("%d\n",cycle);
    return 0;
}

跑出来周期是1431655765,然后我们再使用python算出有效周期:

>>> int("1" * 10000)%1431655765
169873741L

接下来就可以把6次的crc的值直接跑出来了:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<stdint.h>

typedef unsigned int uint;
uint POLYNOMIAL = 0xEDB88320;
int have_table = 0;
uint table[256];

void make_crc32_table(){
    int i, j, crc;
    have_table = 1;
    for (i = 0 ; i < 256 ; i++)
        for (j = 0, table[i] = i ; j < 8 ; j++)
            table[i] = (table[i]>>1)^((table[i]&1)?POLYNOMIAL:0);
}

uint crc32(uint crc,const char *buff, int len){
    if (!have_table) make_crc32_table();
    crc = ~crc;
    for (int i = 0; i < len; i++)
        crc = (crc >> 8) ^ table[(crc ^ buff[i]) & 0xff];
    return ~crc;
}

int main() {
    const char *buf[] = {"TSG", "is", "here", "at", "SECCON", "CTF!"};
    int buflen[] =  {3,2,4,2,6,4};
    make_crc32_table();
    int i=0,j=0; 
    for(i=0;i<6;i++){
        uint32_t _crc32 = 0;
        for(j=0;j<169873741;j++) {
            _crc32 = crc32(_crc32, buf[i], buflen[i]);
        }
        printf("%s %u\n",buf[i],_crc32);
    }
}

得到结果如下:

TSG 2962998607
is 3836056187
here 2369777541
at 3007692607
SECCON 1526093488
CTF! 3679021396

接下来按照题目要求将其拼接为key,然后进行解密即可得到flag:

from Crypto.Util.number import *

crclist = [2962998607,3836056187,2369777541,3007692607,1526093488,3679021396]
key = ""

for i in crclist:
    key += long_to_bytes(i)

c = '79833173d435b6c5d8aa08f790d6b0dc8c4ef525823d4ebdb0b4a8f2090ac81e'.decode('hex')
aes = AES.new(key, AES.MODE_ECB)
flag = aes.decrypt(c)
print flag

flag:

SECCON{Ur_Th3_L0rd_0f_the_R1NGs}

参考

https://en.wikipedia.org/wiki/Cyclic_redundancy_check
https://en.wikipedia.org/wiki/Pigeonhole_principle


文章来源: http://xz.aliyun.com/t/6969
如有侵权请联系:admin#unsafe.sh