首页
社区
课程
招聘
[原创]KCTF 2019Q4总决赛 第二题 南充茶坊
2019-12-9 16:25 6796

[原创]KCTF 2019Q4总决赛 第二题 南充茶坊

2019-12-9 16:25
6796
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 ###


[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

最后于 2019-12-9 16:27 被readyu编辑 ,原因:
上传的附件:
收藏
点赞7
打赏
分享
最新回复 (5)
雪    币: 3
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
xianbufu 2019-12-15 14:27
2
0
请问解压后的size是2734080 bytes,是怎么得出的?
雪    币: 11704
活跃值: (966)
能力值: ( LV12,RANK:779 )
在线值:
发帖
回帖
粉丝
readyu 12 2019-12-15 17:48
3
0
xianbufu 请问解压后的size是2734080 bytes,是怎么得出的?
在 aPsafe_depack 断点可看到size。
雪    币: 3
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
xianbufu 2019-12-16 15:20
4
0
dump.bin是转存文件吗?请问楼主是怎样转存的?
雪    币: 11704
活跃值: (966)
能力值: ( LV12,RANK:779 )
在线值:
发帖
回帖
粉丝
readyu 12 2019-12-16 19:27
5
0
xianbufu dump.bin是转存文件吗?请问楼主是怎样转存的?
用IDA调试, 在 aPsafe_depack 断点停下 ,执行那个 python脚本,就保存了。 也可以用其他方法。

如下内容保存为 dump.py, 
这个目录可以自己修改: d:\\temp\\dump\\dump.bin

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


最后于 2019-12-16 19:29 被readyu编辑 ,原因:
雪    币: 3
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
xianbufu 2019-12-17 15:28
6
0
谢谢楼主热心回复,给楼主一个赞,那dump_267k_bin.exe是不是对生成三个py文件没有起到作用
游客
登录 | 注册 方可回帖
返回