分析SECCON CTF 2019中Crypto方向题目

2019-12-24 约 520 字 预计阅读 3 分钟

声明:本文 【分析SECCON CTF 2019中Crypto方向题目】 由作者 moneymaker 于 2019-12-24 09:34:39 首发 先知社区 曾经 浏览数 124 次

感谢 moneymaker 的辛苦付出!

分析SECCON CTF 2019中Crypto方向题目

前言

在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

关键词:[‘安全技术’, ‘CTF’]


author

旭达网络

旭达网络技术博客,曾记录各种技术问题,一贴搞定.
本文采用知识共享署名 4.0 国际许可协议进行许可。

We notice you're using an adblocker. If you like our webite please keep us running by whitelisting this site in your ad blocker. We’re serving quality, related ads only. Thank you!

I've whitelisted your website.

Not now