2023 熵密杯 revenge

admin 2024年9月28日11:54:40评论20 views字数 5971阅读19分54秒阅读模式

咱们书接上回(没想到还能接上),在前边说我注意到了 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)

2023 熵密杯 revenge

随后进行本地测试,验证上面的随机数是否为服务端私钥

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()))

2023 熵密杯 revenge

注意到和流量包中的公钥是相等的,于是我们就可以用服务端的私钥和客户端的公钥计算预主密钥,然后导入 wireshark 进行会话解密。

整个流量包中有两次会话的协商,我们先在第一个 Client Key Exchange 中抓取客户端的第一个公钥(No.69)

2023 熵密杯 revenge

然后计算它们的协商密钥

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)

2023 熵密杯 revenge

预主密钥的格式为 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】

2023 熵密杯 revenge

导入后我们即可看到解密后的流量。

追踪一下 HTTP 流即可看到签名系统的 用户名密码 以及 flag3

2023 熵密杯 revenge

另外代理 socks 代理的用户名和密码可以在 No.19 的数据包中找到

2023 熵密杯 revenge

伪造签名

进入数字签名系统后,2023 熵密杯 revenge

我们需要计算新消息的签名。

首先 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 *)&currentSeed, 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

验证一下

2023 熵密杯 revenge

和公钥一致!

所以我们可以构造私钥文件 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 的签名

2023 熵密杯 revenge

2023 熵密杯 revenge

完结!撒花!

(PS:做到现在,仍然不知道 AAA 是怎么在没拿到 flag3 的情况下进入签名系统,完成签名计算的,疑惑。难道说他们找到了签名系统的洞可以注册用户?)

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可联系QQ 643713081,也可以邮件至 [email protected] - source:Van1sh的小屋

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年9月28日11:54:40
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   2023 熵密杯 revengehttps://cn-sec.com/archives/3093714.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息