1. 题目分析
南充茶坊这个题目tea.rar,6.78 MB ,下载了好几次,每次一半就断掉了,解压失败。下载了五次才成功。
2019/12/02 12:15 7,114,666 tea.rar
然后解压有三个文件:
2019/11/28 10:22 6,972,465 南充茶坊.exe
2019/11/27 22:46 1,253,376 README.txt.exe
2019/11/30 12:48 384 公开用户名序列号.txt
主程序就是: 南充茶坊.exe
txt里有个key,看了一下有768 bit.
name = "89F3A5FD45E2A383"
key= CA1F4C8740C8E620C4AC306F2C3BCB3FD7ABCC1378D94779DD78481EDC412ACE40A2092B4726A259367C43F3AA3DDDDF54C2513318A11C61867BC43882498F32EBD694C77786A9091FC1333E810888C01049E97F15E4B64123A913E5794B0614
南充茶坊.exe 是一个winrar自解压程序, 解开后 发现一个exe,一个dll。 推测有压缩壳,应该是调用aplib解压的。
2019/11/28 10:09 6,961,608 CM.exe
2014/07/18 10:53 12,800 aplib.dll
2. 提取pyc, 反编译py文件
2.1 解压CM.exe ,得到1个python 打包的exe。 用IDA静态分析了一把,然后下断点调试,里面有aPsafe_depack(), 从0x8000 偏移量开始, 解压了 0x28131 字节 (164145 字节), 解压到0x140000000, 解压后的size是2734080 bytes,也就是267 KB。
aPsafe_depack(buf, 0x28131i64, v5, (unsigned int)fp);
v6 = sub_7FF7A57C128C(v5);
写了个脚本把exe提取出来:
idadump.py
import idaapi
start_address=0x140000000
data_length=2734080
data = idaapi.dbg_read_memory(start_address, data_length)
fp = open('d:\\temp\\dump\\dump.bin', 'wb')
fp.write(data)
fp.close()
2.2 这个dump_267k_bin.exe,看了一番, 发现是python编译pyc,然后打包做成的exe。然后发现CM.exe每次启动,会开启一个新的进程CM。
有一堆python的库解压到temp目录底下, 名字是 _MEI????? 比如 _MEI26002 的目录。
但是没发现CM.pyc文件,只有一个很小的 CM.exe.manifest
1,027 CM.exe.manifest
折腾这个dump_267k_bin.exe, 没找到突破口。 所以又继续分析CM.exe.
2.3 找python反编译的方法
python-exe-unpacker, decompile-py2exe, pyinstxtractor 等等。 试了好几个都不行。
搜到下面这篇, 帮上了忙。
https://laucyun.com/33359ed9f725529ac9b606d054c8459d.html
方法是:
step 1: 通过pyi-archive_viewer工具提取出.pyc文件,然后将.pyc文件解码成.py文件。
step 2: 同时要注意,PyInstaller在打包.pyc时,会把.pyc的magic和时间戳去掉,所以需要手工修复。做法是抄一个正常的pyc文件,用winhex打开,把前面16个bytes补充到目标上。
step 3: 修复完成后,使用 uncompyle6 将.pyc文件解码成.py文件。
step 1:
C:\Python37\Lib\site-packages\PyInstaller\utils\cliutils>archive_viewer.py D:\work\pediy_2019_ctf\2019q4\02-tea-ZhongyaRing\tea1\CM.exe pos, length, uncompressed, iscompressed, type, name [(0, 252, 326, 1, 'm', 'struct'), (252, 1104, 1807, 1, 'm', 'pyimod01_os_path'), (1356, 4372, 9350, 1, 'm', 'pyimod02_archive'), (5728, 7422, 18670, 1, 'm', 'pyimod03_importers'), (13150, 1863, 4171, 1, 's', 'pyiboot01_bootstrap'), (15013, 754, 1178, 1, 's', 'CM'), (15767, 480, 1027, 1, 'b', 'CM.exe.manifest'), (16247, 20112, 36352, 1, 'b', 'Crypto\\Cipher\\_AES.cp37-win_amd64.pyd'), (36359, 7603, 15872, 1, 'b', 'Crypto\\Hash\\_SHA256.cp37-win_amd64.pyd'), (2406251, 1598380, 3745280, 1, 'b', 'python37.dll'), (4004631, 49687, 137216, 1, 'b', 'pywintypes37.dll'), ... 省略若干行 ... (5002231, 0, 0, 0, 'o', 'pyi-windows-manifest-filename CM.exe.manifest'), (5002231, 214384, 781191, 1, 'x', 'base_library.zip'), (5216615, 1467669, 1467669, 0, 'z', 'PYZ-00.pyz')] ? U: go Up one level O <name>: open embedded archive name X <name>: extract name Q: quit ? X CM to filename? CM.pyc ? O PYZ-00.pyz Name: (ispkg, pos, len) {'CMpub': (0, 17, 2250), 'Crypto': (1, 2267, 597), ... 省略若干行 ... 'general': (0, 387075, 918), 'genericpath': (0, 387993, 1775), 'getopt': (0, 389768, 3121), ... 省略若干行 ... 'xml.sax.xmlreader': (0, 1431869, 5400), 'zipfile': (0, 1437269, 23205)} ? U: go Up one level O <name>: open embedded archive name X <name>: extract name Q: quit ? X CMpub to filename? CMpub.pyc ? X general to filename? general.pyc ?
step 2 : 修复pyc;
step3: uncompyle6 反编译pyc,最后得到3个文件:
CM.py
CMpub.py
general.py
3. 分析算法, 分解 RSA Numbers
发现CMpub里有10组768 bits的 RSA-N 和 E, 记作 n[0] ~ n[9] , e[0] ~ e[9]。 其中n[0], e[0]又被分成4组192 bit的small n使用。
一共 9个768 bits的N, 4个192bits的N
。
修改CM.py 让它打印中间过程数据,判定出算法主要流程:
取username的SHA256值, 生成了一个序列seq,大概70多个字节,序列每一项的值为0-9。 0只用到一次,而1~9可以用到多次。
估计作者意图是9个RSA-768有一些弱点,让人通过各种方法分解n得到p,q; 或者通过攻击特定的e,n得到d。
于是就用各种工具去分解,去测试。把google到github上的CTF RSATools都轮了一遍。
从factordb 或者wiki的 RSA_number 分解了n[9], 是RSA Lab公布的挑战中,以及被分解的RSA-768;
通过两两组合求公约数, gcd(n[i], n[j]) (i, j 从1到8) ,分解了 n[7] 和 n[8] ;
通过RDLP轮了一遍,得到n[1]是小因子组合;
通过msieve -v -e 分解了 n[2] 和 n[3] , 大概花了15分钟每个。
通过CTFTools 的boneh_durfee.sage 从e/n 分解了 n[6];
于是只剩下n[4]和n[5];
一边分解,一边又刷了一下factordb,发现n[4]已经有了p,q, 不知道谁上传的,发现p,q很解近, p/q = 384 bits, p-q ~= 206 bits ,应该使用Fermat 分解法。
于是只剩下n[5]。
这里面n[5]也是蒙了很久。
一直在想:作者的成语接龙,循环的脑洞是什么? 以为9个n有什么联系, 比如前一个n的Low Bits 和后一个n的 High Bits能连起来,最终形成一个圈。
反正试了很久都没对准脑电波。 可能是:不需要d,就只用e,n 也能解?
这时候碰巧也搜到关键字: cycling attack RSA 或者 Cycle attack on RSA 的网页。
就是拿e,n 不断循环powmod,密文循环若干次,能得到明文。
试了一下3000次循环, 发现存在固定周期973。伪代码:
/// find cycle //// int maxk = 3000; Big c0 = rand(); Big c = c0; int i = 5; for(int j = 1; j <= maxk; j++) { c = pow(c, e[i], n[i]); // k = 973 if(c == c0) { printf("c = c0, find i= %d, j = %d\n", i, j); } if(c == 1) { printf("c = 1, find i= %d, j = %d\n", i, j); } }
c = c0, find i= 5, j = 973
c = c0, find i= 5, j = 1946
c = c0, find i= 5, j = 2919
就考虑, 那么就是 e*d = e^973 满足 e*d = 1 mod (lcm(p-1)*(q-1)) , 从而可用的 d=e^972
但是这个d太大了, 不方便。所以还是得从 e,d 分解出n, 求得合适的 d < n才好。
方法一: 原理可以参考 我2017年的贴:
https://bbs.pediy.com/thread-218631.htm
从 (N,E,D) 计算P ,Q 。 也有至少2种方法。
伪代码描述如下:
// 通过求解 w^2 = 1 mod n, we get p=gcd(w-1, n),有1/2概率求出非平凡因子p,一般2次即可
// k*phi = ed -1 = r * 2^s
// a = rand(n)
// w = a^r (mod n)
// for ( j = 0 ; j < s ; j++)
// if (w != 1) && (w != n-1 )
// if w^2 = 1 (mod n)
// gcd(w-1,n) or gcd(w+1 ,n ) will have 1/2 chance to get p.q
// 随机选择a,一般 2-3次, 即可求出p
///// method 1 //////////// // find w^2 = 1 mod n, then gcd(w + 1, n) = p or gcd(w - 1, n) = p printf("for p[5], method 1 \n", tm); tm = time(NULL); ek973 = pow(e[5], 973); ed = ek973 - 1; // k*phi = ed - 1 int s = 0; while(ed % 2 == 0) { s++; ed /= 2; } printf("s=%d, get: ed - 1 = r * 2^s \n", s); find = 0; for(int t = 2; t <= 7; t++) // try t=2, 3, 4, 5 ,... { w = pow(t, ed, n[5]); int is = 0; while(is < s) { is++; w = pow(w, 2, n[5]); Big p = gcd(w + 1, n[5]); if(p > 1) { char buf[1024] = {0}; printf("try t =%d, s = %d ,get p\n", t, s); buf << p; printf("%s\n", buf); find = 1; break; } } if(find == 1) break; } tm = time(NULL) - tm; printf("method 1, time cost: %d seconds\n\n", tm);
for p[5], method 1
s=6, get: ed - 1 = r * 2^s
try t =2, s = 6 ,get p
CCAF369F85A28753B04A2281DB60A6D7E242F58429F0746D88CCFB0738DEE49B542655F6ACA3952CFA99A14FCB5C198D
method 1, time cost: 15 seconds
方法二: 后来,看到pdf里说,可以直接用序列Xi+1 - Xi, gcd(Xi+1 - Xi, n) = p ,于是有了第2个方法。
// m^(e^973) = m mod n // e^973 = 1 (mod phi) // d = e^972 (mod phi) // (m^e)^(e^972) = m mod n // gcd(m^e - m, n) = p or gcd ( m^(e-1) - 1, n) = p printf("for p[5], method 2, Begin gcd \n", tm); tm = time(NULL); srand(tm); Big m1, m2, mp, np, one = 1; maxk = 973; m1 = 2; for(i = 1; i < maxk; i++) { m2 = pow(m1, e[5], n[5]); mp = m2 - m1; m1 = m2; if((mp == n[5]) || (mp == 0)) { printf("bug m2 == m1, i = %d !\n", i); break; } np = gcd(mp, n[5]); if(np > one) { char buf[1024] = {0}; printf("try i =%d, get p\n", i); buf << np; printf("%s\n", buf); break; } } tm = time(NULL) - tm; printf("method 2, gcd time cost: %d seconds\n\n", tm);
for p[5], method 2, Begin gcd
try i =1, get p
CCAF369F85A28753B04A2281DB60A6D7E242F58429F0746D88CCFB0738DEE49B542655F6ACA3952CFA99A14FCB5C198D
method 2, gcd time cost: 0 seconds
4. 计算得出flag
附录了 CM_keygen_rsa768.rar :
编译通过的C++代码, miracl大数运算库。
int main(int argc, char ** argv)
{
char name[] = "KCTF";
char seq[]="4544666477169559668468842297334251961227358993685119239612837573423252397359";
rsa768_seq(seq, name);
return 0;
}
### final round n[0] 192 bits * 4 ###
name=KCTF
key= 2B04A95B39B037AF05F043DD043093A68FD96C2120035C84CA8E11D216E4F2D794A0E882E05CDB0E7777B07139860964002ABB292ADFE11C6BA9140103078E034B4B3453CFAE2BB81E71920462FC08E59EB36989B85CA0AE0B2C5DB94EFD732A
### final done ###
[公告]安全测试和项目外包请将项目需求发到看雪企服平台:https://qifu.kanxue.com
最后于 6小时前 被readyu编辑 ,原因: