在SECCON CTF 2019中有3道Crypto方向的题目,题目相对比较简单,在这里对题目进行一下分析。
题目描述如下:
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
分析一下源码,发现题目流程非常直接,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}
题目只给了一个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
其中username
和amount
分别是我们当前的用户名和要转账的金额,proof
和hash
两个参数我们暂时无法理解,因此我们尝试再生成一张转账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}
题目描述如下:
I'm crazy about googology!
打开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