-
-
[原创]CTF2018第十三题分析(qwertyaa)
-
2018-7-10 22:26 2298
-
分析主要流程
首先看一遍main
函数,如下:
// local variable allocation has failed, the output may be wrong! int __cdecl main(int argc, const char **argv, const char **envp) { __int128 v3; // xmm1 __int128 v4; // xmm2 __int128 v5; // xmm3 __int128 v6; // xmm6 __int128 v7; // xmm7 __int128 v8; // xmm8 __int64 v9; // rdx __int64 v10; // rax __int64 v11; // rdx __int64 v12; // rax __int64 v13; // rdx __int64 v15; // rax __int64 v16; // rax __int64 v17; // rax _BYTE hexkey[5]; // [rsp+20h] [rbp-198h] char Format[4]; // [rsp+30h] [rbp-188h] char v20; // [rsp+34h] [rbp-184h] double res; // [rsp+40h] [rbp-178h] double v22[4]; // [rsp+50h] [rbp-168h] __int64 message; // [rsp+70h] [rbp-148h] __int64 v24; // [rsp+78h] [rbp-140h] __int64 v25; // [rsp+80h] [rbp-138h] __int64 v26; // [rsp+88h] [rbp-130h] char key[11]; // [rsp+90h] [rbp-128h] int v28; // [rsp+190h] [rbp-28h] sub_401F00(*(__int64 *)&argc, (__int64 *)argv); hexkey[4] = 0; memset(key, 0, 0x100uLL); v20 = 0; v22[0] = 0.0; v22[1] = 0.0; v22[2] = 0.0; v22[3] = 0.0; res = 0.0; v28 = 0; *(_DWORD *)hexkey = 0; *(_DWORD *)Format = 0; message = 0LL; v24 = 0LL; v25 = 0LL; v26 = 0LL; initVals(); sub_401C50((__int64)&v28, (__int64)key, v9, 252); v10 = 0LL; do { v11 = (byte_408D00[v10] ^ 0x19) - (unsigned int)v10; *((_BYTE *)&message + v10) = (byte_408D00[v10] ^ 0x19) - v10; ++v10; } while ( v10 != 16 ); printf(Format, key, v11, &message); Format[0] = byte_408D70 ^ 0x25; Format[1] = (byte_408D71 ^ 0x25) - 1; scanf(Format, key, key, Format); // input if ( strlen(key) != 10 ) // key len error { v12 = 0LL; do { v13 = (byte_408D10[v12] ^ 0xF) - (unsigned int)v12; *((_BYTE *)&message + v12) = (byte_408D10[v12] ^ 0xF) - v12; ++v12; } while ( v12 != 16 ); goto LABEL_6; } if ( (unsigned int)hex2bin((__int64)Format, (__int64)key, (__int64)hexkey, (__int64)key, 10) != 5 ) { v15 = 0LL; do { v13 = (byte_408D20[v15] ^ 0x21) - (unsigned int)v15; *((_BYTE *)&message + v15) = (byte_408D20[v15] ^ 0x21) - v15; ++v15; } while ( v15 != 16 ); goto LABEL_6; } HIWORD(v22[0]) = *(_WORD *)hexkey; v22[2] = v22[0]; *(_WORD *)((char *)&v22[1] + 5) = *(_WORD *)&hexkey[2]; HIBYTE(v22[1]) = hexkey[4]; v22[3] = v22[1]; sub_4015E0((__int64)Format, (__int64)key, &res, v22, *(unsigned __int64 *)&v22[1], v3, v4, v5, v6, v7, v8); Format[0] = byte_408D72 ^ 0x12; Format[1] = (byte_408D73 ^ 0x12) - 1; Format[2] = (byte_408D74 ^ 0x12) - 2; // %lf sprintf(Format, key, Format, key, *(_QWORD *)&res, res, *(double *)&v3, res); if ( key[1] != '.' ) { v16 = 0LL; do { v13 = (aPvutFg[v16] ^ 0x3F) - (unsigned int)v16; *((_BYTE *)&message + v16) = (aPvutFg[v16] ^ 0x3F) - v16; ++v16; } while ( v16 != 16 ); // Oh failed goto LABEL_6; } if ( sqrt( (double)(key[0] - 48) * (double)(key[0] - 48) + (double)(key[2] - 48) * (double)(key[2] - 48) + (double)(key[3] - 48) * (double)(key[3] - 48)) <= 15.5 || hexkey[2] & 0xF ) { v17 = 0LL; } else { v17 = 0LL; if ( fabs(v22[2] + v22[3] - res) < 0.003 ) { do { v13 = (byte_408D50[v17] ^ 0x47) - (unsigned int)v17; *((_BYTE *)&message + v17) = (byte_408D50[v17] ^ 0x47) - v17;//Cong ++v17; } while ( v17 != 16 ); goto LABEL_6; } } do { v13 = (byte_408D60[v17] ^ 0x37) - (unsigned int)v17; *((_BYTE *)&message + v17) = (byte_408D60[v17] ^ 0x37) - v17;//Come on ++v17; } while ( v17 != 16 ); LABEL_6: printf(Format, key, v13, &message); return 0; }
里面的所有字符串都简单加密了一下,动态调试即可得他们分别是key len error.
、key char error
、%lf
、Ohhh.Try again
、Come on, go on
、Congratulation
。
程序里的fabs
和浮点取负似乎不一定能够识别出来,在了解到COERCE_UNSIGNED_INT64
用于将参数强制转换为unsigned __int64
(等价于*(unsigned __int64*)(&x)
)以及double
的最高位是符号位后很容易手工识别出来。
而程序中的exp
函数可以通过函数中的一些魔数识别,sqrt
可以通过异常处理时用的字符串识别,将这两个函数重新命名后按F5可以识别出不少东西来。
而开头的initVals
、sub_401C50
似乎是在初始化一些东西,或许是在训练网络,里面的GetTickCount
似乎用于反调试,这一点和 lelfei 以前的CM很像。
输入后是判断输入的key是否长度为10,并将其进行“hex2bin
”,然后将结果放到两个double
高2/3byte上去。
接下来将其传入一个函数记作nnRun
,通过传递地址返回一个double
,接下来进行一些检查:
- 返回的
double
用%lf
打印后第二个字符是'.'
- 返回的
double
用%lf
打印后第一、三、四个字符的平方和开根号后要大于15.5
(代入9/9/9
和9/9/8
可知这三位必须都是9
才满足要求) hex2bin
后的第三字符 & 0xf 结果为0(即原始密钥的对应位为0)- 传入
nnRun
的两个double
之和与返回的double
之差的绝对值要小于0.03
而阅读nnRun
可知,但传入double
不在(1,10)的范围内时返回的double
必为10.0,而这显然不满足条件1,而double
最高位是符号位,接下来是阶码,后面才是尾数,而改变key改变的是这两个double
的高位内容,所以所剩的key的可能数不多了(总共仅几百万种可能)。
实现nnRun
但是由于前面两个用于初始化的函数执行的很慢,所以我们要直接Dump初始化结果,并自已实现一遍nnRun
才能较快穷举。
仔细看几遍,就可写出nnRun
的代码如下:
void nnRun(double *out, double *in) { double mid[20]; in[0] = (in[0] - 0.01 + 1.0) / (9.99 - 0.01 + 1.0); in[1] = (in[1] - 0.03 + 1.0) / (9.99 - 0.03 + 1.0); for (int i = 0; i != 18; i++) { mid[i] = 1.0 / (exp(-(in[1] * wei1[i * 2 + 1] + in[0] * wei1[i * 2])) + 1.0); } double sum = 0.0; for (int i = 0; i != 18; i++) sum += wei2[i] * mid[i]; if (in[2] <= 1.0 || in[2] >= 10.0 || in[3] <= 1.0 || in[3] >= 10.0) { // wei2[0] = wei2[0]*wei2[0]; // modify the weight *out = 10.0; } else { *out = sum * (19.32 - 0.26 + 1.0) + 0.26 - 1.0; } }
其中的wei*
是被前面所述的两个函数初始化的内容。
这似乎是一个两层的神经网络,第一层有sigmoid
函数,第二层直接乘权重,最后求和,这里似乎是在用神经网络模拟加法。(然而我并不懂神经网络,这些都是瞎猜的。(懂了似乎对这道题解题也没太大帮助。))
暴力取得key
毕竟即使懂,由于神经网络很难被人类直接看懂,暴力是个不错的选择。
由第一段可知,所剩下的可能数已经不多,枚举可知double
的最高两bit必须在3ff1h-4023h
的范围内。
暴力的程序如下("defs.h"
是IDA的plugins文件夹下的头文件):
#include "defs.h" #include <math.h> #include <stdio.h> #include <stdlib.h> #include <string.h> __int64 weiLL1[40] = { 0x0BFF0645E9156DBC1, 0x3FB5B59C6AE0004B, 0x0BFE0FD9DEF0F5F9A, 0x3FE685E23E24E853, 0x0BFBEDEBCA72F7512, 0x0BFD40B39A1B3CB8C, 0x0BFE87D3F640F203C, 0x3FD5B9DCAD3E9529, 0x3FDA27F983BCFDC3, 0x3FD62BE5D9589EAE, 0x0BFE581B69A36B717, 0x3FE6801BBF527252, 0x3FDEAE2EA9FA99CC, 0x3FC290C00A49CF36, 0x0BFE842DEACE9DB6E, 0x0BFF30F05544464E8, 0x0BFE6EC1FDA490984, 0x0BFC7C4E2F5A93E07, 0x0BFD8D6B32E2FB6F9, 0x0BFDC416F857C9468, 0x3FE7F90F71FEB8D9, 0x0BFD2CC5C4EF8FBF4, 0x0BFD1F8160A51CD2B, 0x0BFE5202BE8B76C05, 0x0BFF23B46C97DC5A4, 0x0BFD7E42B9359DDEB, 0x0BFD7C90E0C010F65, 0x0BFD48AAE09661ADC, 0x3FCD2D7C013B771C, 0x3FD2520DDF5608B2, 0x0BFE4BF1ED4E7248A, 0x3FD5EBAD5FB98A18, 0x3FBA13DBE1D40BC8, 0x0BFB123848B37921F, 0x0BFE993D71BB7483C, 0x3FD1BB92E0D38D8F}; double *wei1 = (double *)weiLL1; double wei2[20] = { -0.1194112006523917, 0.3642266945402522, -0.3350415063325052, -0.6162511508923307, 1.038326594084909, 0.1932021480282533, 1.001826904076745, 0.4434245541912034, -0.5712156493805307, -1.368248566749872, 0.1587663304193661, -1.153231630918381, 0.2535851365039063, -0.9888794593893403, 0.6114148602075213, 0.1491657913886502, 0.5188520292352635, 0.2985569369090871}; void nnRun(double *out, double *in) { double mid[20]; in[0] = (in[0] - 0.01 + 1.0) / (9.99 - 0.01 + 1.0); in[1] = (in[1] - 0.03 + 1.0) / (9.99 - 0.03 + 1.0); for (int i = 0; i != 18; i++) { mid[i] = 1.0 / (exp(-(in[1] * wei1[i * 2 + 1] + in[0] * wei1[i * 2])) + 1.0); } double sum = 0.0; for (int i = 0; i != 18; i++) sum += wei2[i] * mid[i]; if (in[2] <= 1.0 || in[2] >= 10.0 || in[3] <= 1.0 || in[3] >= 10.0) { // wei2[0] = wei2[0]*wei2[0]; // modify the weight *out = 10.0; } else { *out = sum * (19.32 - 0.26 + 1.0) + 0.26 - 1.0; } } char key[20] = {0}; char cmd[200] = {0}; char val[200] = {0}; void swap(char *x, char *y) { _WORD *a = (_WORD *)x; _WORD *b = (_WORD *)y, t = *a; *a = *b; *b = t; } int main() { double x = 0.0, y[4] = {0.0}; for (int i = 0; i < 0x10000; i++) if ((i & 0xf) == 0) { // For hexkey[2] & 0xF == 0 for (int j = 0x3f; j < 0x41; j++) { x = 0.0; *(unsigned short *)((char *)&x + 5) = i; (*((unsigned char *)&(x) + (sizeof(x) / sizeof(unsigned char) - 1))) = j; if (x > 1.0 && x < 10.0) { for (int k = 0x3ff1; k <= 0x4023; k++) { y[0] = 0.0; HIWORD(y[0]) = k; y[1] = x; y[2] = y[0]; y[3] = y[1]; double ret = 0.0; nnRun(&ret, y); sprintf(val, "%lf", ret); if (val[1] == '.' && ret >= 9.99 && fabs(y[2] + y[3] - ret) < 0.003) { sprintf(key, "%04X%04X%02X", k, i, j); swap(key, key + 2); swap(key + 4, key + 6); { // Check it by run reality printf("Testing %s...\n", key); FILE *f = fopen("in.txt", "wb"); fprintf(f, "%s", key); fclose(f); system("cmd.exe /c nncrackme.exe < in.txt > out.txt"); f = fopen("out.txt", "rb"); memset(val, 0, sizeof val); fgets(val, 200, f); fclose(f); if (strcmp(val, "Input your KEY:Congratulation\r\n") == 0) { printf("Success key: %s\n", key); } } } } } } } return 0; }
这个程序最后还会把枚举出来的正确密钥用NNCrackme.exe来验证一遍,当然这个其实是不必要的(因为我一开始省略了一些条件判断让NNCrackme.exe来直接判断)。
最后得到的key为F13FE02140
。
[培训]二进制漏洞攻防(第3期);满10人开班;模糊测试与工具使用二次开发;网络协议漏洞挖掘;Linux内核漏洞挖掘与利用;AOSP漏洞挖掘与利用;代码审计。