咱们书接上回(没想到还能接上),在前边说我注意到了 Gitea 中的改动,出题人对 openssl 项目中的 crypto/rand/drbg_lib.c 文件中一个生成随机数的函数进行了修改,将原本生成32字节随机数写死了。
12345 |
uint8_t rand0_32[32] = {0x67, 0xc6, 0x69, 0x73, 0x51, 0xff, 0x4a, 0xec, 0x29, 0xcd, 0xba, 0xab, 0xf2, 0xfb, 0xe3, 0x46, 0x7c, 0xc2, 0x54, 0xf8, 0x1b, 0xe8, 0xe7, 0x8d, 0x76, 0x5a, 0x2e, 0x63, 0x33, 0x9f, 0xc9, 0x9a};for(int i=0;i<outlen;i++){ out[i] = rand0_32[i % 32];} |
当时猜测的是数字签名系统计算 msg1 签名,生成临时密钥的时候调用了这个函数。事实上,服务端在生成私钥时调用了该函数!
第三关
我们看到数字签名系统调试数据包中服务端使用的公钥(No.66)
随后进行本地测试,验证上面的随机数是否为服务端私钥
1234567891011 |
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey,X25519PublicKeyfrom cryptography.hazmat.primitives.kdf.hkdf import HKDFrand0 = [0x67, 0xc6, 0x69, 0x73, 0x51, 0xff, 0x4a, 0xec, 0x29, 0xcd, 0xba, 0xab, 0xf2, 0xfb, 0xe3, 0x46, 0x7c, 0xc2, 0x54, 0xf8, 0x1b, 0xe8, 0xe7, 0x8d, 0x76, 0x5a, 0x2e, 0x63, 0x33, 0x9f, 0xc9, 0x9a]sk = "".join(hex(i)[2:].rjust(2,'0') for i in rand0)print(sk)privatekey=X25519PrivateKey.from_private_bytes(bytes.fromhex(sk))print((privatekey.public_key()._raw_public_bytes().hex())) |
注意到和流量包中的公钥是相等的,于是我们就可以用服务端的私钥和客户端的公钥计算预主密钥,然后导入 wireshark 进行会话解密。
整个流量包中有两次会话的协商,我们先在第一个 Client Key Exchange 中抓取客户端的第一个公钥(No.69)
然后计算它们的协商密钥
1234567891011121314151617 |
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey,X25519PublicKeyfrom cryptography.hazmat.primitives.kdf.hkdf import HKDFrand0 = [0x67, 0xc6, 0x69, 0x73, 0x51, 0xff, 0x4a, 0xec, 0x29, 0xcd, 0xba, 0xab, 0xf2, 0xfb, 0xe3, 0x46, 0x7c, 0xc2, 0x54, 0xf8, 0x1b, 0xe8, 0xe7, 0x8d, 0x76, 0x5a, 0x2e, 0x63, 0x33, 0x9f, 0xc9, 0x9a]sk = "".join(hex(i)[2:].rjust(2,'0') for i in rand0)# print(sk)privatekey=X25519PrivateKey.from_private_bytes(bytes.fromhex(sk))# print((privatekey.public_key()._raw_public_bytes().hex()))publickey=X25519PublicKey.from_public_bytes(bytes.fromhex('a0022027e0390ead7d82e1e74ae2d2f045fbf72896b9846d7f28bfa184280e3e'))result=privatekey.exchange(publickey)print(result.hex()) |
得到 7ff739dbe782d963e54e3242d83b3a01a6535aed3579f6a514a664b363915903
另外找到 Client Hello 里的随机数(No.64)
预主密钥的格式为 PMS_CLIENT_RANDOM[空格]Random[空格]sharekey
于是第一个预主密钥为
1
|
PMS_CLIENT_RANDOM 9d8f92cc2ac8f33293da5169d49c82794c660fc937bd0c1b05f5e062e491da85 7ff739dbe782d963e54e3242d83b3a01a6535aed3579f6a514a664b363915903
|
同理我们在 No.3334 可以找到另一个 Random,在 No.3341 可以找到另一个客户端的公钥
最终预主密钥文件为
12 |
PMS_CLIENT_RANDOM 9d8f92cc2ac8f33293da5169d49c82794c660fc937bd0c1b05f5e062e491da85 7ff739dbe782d963e54e3242d83b3a01a6535aed3579f6a514a664b363915903PMS_CLIENT_RANDOM b5dbfb40bc4c2b1a46bbc594fc89a56c17fe7db891beb7c111691516bd3117d1 4c8c1680018a8dd48749d642b6a6df5cc2104cb98842b82b0d748430108b8f61 |
随后【编辑】->【首选项】->【TLS】
导入后我们即可看到解密后的流量。
追踪一下 HTTP 流即可看到签名系统的 用户名密码 以及 flag3
另外代理 socks 代理的用户名和密码可以在 No.19 的数据包中找到
伪造签名
进入数字签名系统后,
我们需要计算新消息的签名。
首先 SM2 签名理论上是不会有什么问题的,并且前面一题的考点已经是私钥泄露了,那么这里应该是没法直接获取私钥的。在签名中,与私钥同等重要的,就是临时密钥了。在上一篇文章中我们猜测这里可能是临时密钥重用。不过那需要至少已知两条签名我们才能恢复私钥,所以这个思路应该可以否定了。不过,我们在第二关还获取到了一份数字签名系统签名验签源码:sign-verify.c,那么切入点显然会在这了。
在其中的 Sign 函数中,我们注意到
1234567 |
//Generate Random Numberunsigned char randomScalar[32];unsigned int i_time=0;time_parse(message, &i_time);if(derive_from_time(i_time,randomScalar,32))goto err;BN_bin2bn(randomScalar, 32, k); |
看到 time_parse 和 derive_from_time 函数
123456789101112131415161718192021222324252627282930313233343536373839404142 |
inttime_parse(char *str_time, unsigned int *i_time){struct tm s_time;/* strptime(str_time,"%Y年%m月%d日%H:%M:%S",&s_time);s_time.tm_isdst = -1;*i_time = mktime(&s_time); */int year, month, day, hour, minute,second;sscanf(str_time,"%d-%d-%d %d:%d:%d", &year, &month, &day, &hour, &minute, &second);s_time.tm_year= year-1900;s_time.tm_mon= month-1;s_time.tm_mday= day;s_time.tm_hour= hour;s_time.tm_min= minute;s_time.tm_sec= second;s_time.tm_isdst= -1;*i_time = mktime(&s_time);return 0;}int derive_from_time(unsigned int seed, unsigned char *randomScalar, int length) { if (randomScalar == NULL || length <= 0) { return 1; // Invalid input } unsigned int currentSeed = seed; int generatedLength = 0; while (generatedLength < length) { unsigned char shaOutput[SHA256_DIGEST_LENGTH]; SHA256((const unsigned char *)¤tSeed, sizeof(currentSeed), shaOutput); int remainingLength = length - generatedLength; int copyLength = remainingLength < SHA256_DIGEST_LENGTH ? remainingLength : SHA256_DIGEST_LENGTH; memcpy(randomScalar + generatedLength, shaOutput, copyLength); generatedLength += copyLength; currentSeed++; } return 0; // Success} |
乱七八糟的,但是总而言之,随机数 k 和消息中的时间相关。
那么思路就很显然了:我们可以计算签名 msg1 时使用的临时密钥 k,有了 k 也就能恢复签名用的私钥 sk,从而也就能给 msg2 签名了。
由于 c 的大数计算可麻烦,这里还是先用它的代码把临时密钥 k 打印出来先
编译指令:gcc tmpk.c -L. -l crypto -l ssl -o tmpk
(把 tmpk.c 放在 openssl 目录下)
123456789 |
//Generate Random Numberunsigned char randomScalar[32];unsigned int i_time=0;time_parse(message, &i_time);if(derive_from_time(i_time,randomScalar,32))goto err;BN_bin2bn(randomScalar, 32, k);BN_print_fp(stdout, k); printf("\n"); |
得到 D2D569D2A7250B2B27DF909C9AFC1FD9E0A555AEC4BFB5D80CD71F70ADACF414
已知临时密钥 $k$ ,根据签名值我们可以获取 $r,s$ ,而计算私钥 sk 的公式为 $sk = \frac{k-s}{s+r}$
注意到这里有一个坑点,签名里的 r 和 s 用 FlipEndian 处理过,字节序变化了,所以我们在计算的时候也要相应处理
12345678 |
from Crypto.Util.number import *r = 0x37AF670C4742BD0C8D7CF68FCEBFE61885AA630695D50A15DF279CD64327466Fr = bytes_to_long(long_to_bytes(r)[::-1])s = 0x6701CFB5F356887B9441323FDC08FBA900E1050109FD95F024DC9C178CEBE7A4s = bytes_to_long(long_to_bytes(s)[::-1])n = 0xFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123k = 0xD2D569D2A7250B2B27DF909C9AFC1FD9E0A555AEC4BFB5D80CD71F70ADACF414print((k-s)*inverse(s+r,n)%n) |
得到私钥 104515905597970870556286963199400550747760654012576876144731059595513283165045
验证一下
和公钥一致!
所以我们可以构造私钥文件 pri_pub/priSM2.key ( hex(bytes_to_long(long_to_bytes(sk::-1]))
)
1
|
753bffd7cd2353cbe72702159162f8da8f7118d8b4944fe74ddbf7e2fee711e7
|
然后把main函数修改一下
12345678910111213141516171819 |
int main(){unsigned char pub[64];unsigned char pri[64];unsigned char message1[128] = "2023-8-10 09:11:13, A transfers 50000.00 to B.";unsigned char message2[128] = "2023-8-10 11:31:01, B transfers 50000.00 to A.";unsigned char digest[32];unsigned char sig1[64];unsigned char sig2[64];int ret;printf("msg1:\t%s\n",message2);ret = Sign_Prifile(message2, sig1);user_printf_hex("sig1:\t",sig1,64);ret = Verify_Pubfile(message2, sig1);printf("verify:\t%d\n",ret);return 0;} |
运行得到 msg2 的签名
完结!撒花!
(PS:做到现在,仍然不知道 AAA 是怎么在没拿到 flag3 的情况下进入签名系统,完成签名计算的,疑惑。难道说他们找到了签名系统的洞可以注册用户?)
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可联系QQ 643713081,也可以邮件至 [email protected] - source:Van1sh的小屋
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论