看雪论坛
发新帖
6

[原创]2017腾讯游戏安全技术竞赛 Round 1 标准及进阶题解分析

Ericky 2017-7-10 00:13 2800

前言


为了学习死磕的精神,我也来做题。我选择了APK的题目,题目有两种难度,分别对应标准级别和进阶级别,标准级别是100分,进阶级别是200分。我先看的标准级别,然后看的进阶级别,由于自己对一些算法的熟练程度不够,逆向东西也逆得慢,所以花了不少时间。再说一点题外话,这个进阶题目和之前360移动安全竞赛的APK cm比较相似,都是RSA算法,name和RSA中的e 、n、 d参数绑定,不同之处是360的那道题目的难点在于用了ollvm来进行混淆,增大分析难度,而这一道题目的难点是需要写一个完整的注册机,这个是两道题的不同之处,好了不扯了,下面开始正题。
0x1REG_CHECK函数
Java层比较简单,只是一个reg_check函数,传入输入的key和code,然后进行验证。这个函数是整个程序的核心,reg_check函数如下:
int __fastcall CRegCheck::reg_check(int a1, int a2, _DWORD *a3, int a4)
{
  int v4; // r4@1
  char **v5; // r8@1
  int v6; // r6@1
  int v7; // r5@1
  int v8; // r0@2
  unsigned int v9; // r4@5
  int v10; // r4@5
  void *v11; // r4@6
  int v13; // [sp+20h] [bp-CD4h]@5
  void *ptr; // [sp+24h] [bp-CD0h]@2
  int v15; // [sp+28h] [bp-CCCh]@2
  int v16; // [sp+2Ch] [bp-CC8h]@2
  __int64 v17; // [sp+30h] [bp-CC4h]@2
  __int64 v18; // [sp+38h] [bp-CBCh]@7
  __int64 v19; // [sp+40h] [bp-CB4h]@7
  __int64 v20; // [sp+48h] [bp-CACh]@7
  __int64 v21; // [sp+58h] [bp-C9Ch]@5
  __int64 v22; // [sp+60h] [bp-C94h]@7
  int v23; // [sp+68h] [bp-C8Ch]@7
  int v24; // [sp+CE4h] [bp-10h]@1
 
  v4 = a4;
  v5 = (char **)a3;
  v6 = a2;
  v24 = _stack_chk_guard;
  v7 = 0;
  if ( CRegCheck::is_user_name_legal(_stack_chk_guard, (int *)a2) != 1 )
    goto LABEL_13;
  v8 = CRegCheck::get_formula_param(&v17, v6);
  ptr = 0;
  v15 = 0;
  v16 = 0;
  if ( v4 == 1 )
  {
    if ( !CBase64::base64_decode(*v5, (int)&ptr) )
      goto LABEL_8;
  }
  else
  {
    v9 = CRegCheck::get_rsa_key_seed(v8, v6);
    RSA::RSA((RSA *)&v21);
    RSA::RSAKey((RSA *)&v21, v9);
    sub_33440(&v13, (int *)v5);
    v10 = RSA::tdecrypto((int)&v21, (int)&v13, (int)&ptr);
    sub_332FC(v13 - 12);
    RSA::~RSA((RSA *)&v21);
    if ( !v10 )
    {
LABEL_8:
      v7 = 0;
      goto LABEL_9;
    }
  }
  v11 = ptr;
  if ( v15 - (_DWORD)ptr == 16 )
  {
    _aeabi_memclr(&v23, 0);
    _aeabi_memcpy(&v21, v11, 16);
    v7 = is_tangent(v20, v19, v18, v21, v22, v17);
LABEL_9:
    v11 = ptr;
    goto LABEL_11;
  }
  v7 = 0;
LABEL_11:
  if ( v11 )
    operator delete(v11);
LABEL_13:
  if ( _stack_chk_guard != v24 )
    _stack_chk_fail(_stack_chk_guard - v24);
  return v7;
}


0x2 is_user_name_legal函数


在check函数的开端,遇见的是is_user_name_legal函数,它的作用是校验了一下name的长度是否是39位,格式需要如下:
xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx (一共39位,暂且称之为8个段,在之后用segment1~segment8来表示)
x需要在 ‘0’-‘9’ 、 ‘a’-‘f’之间,否则key的合法性就不予通过。


0x3 get_formula_param函数


在get_formula_param函数中,根据输入的name 初始化了4个参数,每2个段生成一个参数,这些参数将在最后的is_tangent函数验证中用到。
具体参数代码如下:
a1 = 0;
a1 += (segment1[0] * segment8[0]) << 16 ;
a1 += segment1[1] ^ segment8[1];
a1 += segment1[2] % (segment8[2]+1)+ 1;
a1 += segment1[3] / (segment8[3]+1);
 
a2 =0;
a2 += (segment2[0] ^ segment7[0]) << 16 ;
a2 += (segment2[1] % (segment7[1]+3));
a2 += (segment2[2] / (segment7[2]+1)) + 5;
a2 += (segment2[3] + (segment7[3])) ;
a3 = 0;
a3 += (segment3[0] / (3 + segment6[0]) )<< 16 ;
a3 += (segment3[1] * segment6[1]);
a3 +=  segment3[2] %(segment6[2] + 7) +5;
a3 +=  segment3[3] + segment6[3];
a6 = 0;
a6 += (segment4[0] + segment5[0]) << 16;
a6 *= segment4[1]/(segment5[1]+2);
a6 += segment4[2] %(segment5[2] +5) +7;
a6 += segment4[3] * segment5[3];
好了,这就是最后校验用到的4个参数。


0x4 分水岭 (标准OR进阶?)


检查完输入key的合法性和初始化4个重要的参数之后,这个时候判断模式,判断做题的人是选择的什么难度,如果是选择标准难度,将会对输入的code进行base64解码,再进行计算,判断是否成功。我打算重点说说进阶难度题目的事情,所以这里就给出一组正确的key,其他的过程需要读者自行跟踪一下。
我算出来的一组key:
key:0234-5678-90ab-00ef-0f87-6543-21fe-0cba
code:A6Kf2FVPOOvERSOoIf@5l2==
如果是选择的进阶的题目,将会进入到另外一番天地,先贴一段代码,如下:
    v9 = CRegCheck::get_rsa_key_seed(v8, v6);
    RSA::RSA((RSA *)&v21);
    RSA::RSAKey((RSA *)&v21, v9);
    sub_33440(&v13, (int *)v5);
    v10 = RSA::tdecrypto((int)&v21, (int)&v13, (int)&ptr);
    sub_332FC(v13 - 12);
    RSA::~RSA((RSA *)&v21);
从这里可以看出来,进阶版这里用的是RSA算法对输入的key和code进行验证。


0x5 get_rsa_key_seed函数


虽然这个名字叫做get_rsa_key_seed,但是其实里面的功能是对输入的key进行4次hash的计算,分别使用的是RSHash、JSHash、PJWHash、以及最后一个自定义的hash值计算的方法,具体的hash 函数代码在这里,请戳这里:
http://www.partow.net/programming/hashfunctions/index.html
对name 做了hash后,会将生成的hash值传入下一个函数,这个函数是RSAKey。

0x6 RSAKey函数


原函数代码如下:
bool RSAKey()  
{  
    for(i=0;i<MAX;i++)  
        m[i]=p[i]=q[i]=n[i]=d[i]=e[i]=0;
    prime_random(p,q);
    mul(p,q,n);  
    mov(p,p1);  
    p1[0]--;        
    mov(q,q1);  
    q1[0]--;      /*/q-1;*/  
    mul(p1,q1,m);//m=(p-1)*(q-1)  
    erand(e,m);  
    rsad(e,m,d);  
    return true;  
}
看代码可以知道,这个RSAKey函数的主要作用是来生成n、e 、d的,逆向跟一下会发现,如果name不变,n、e、d也是不变的,也就是说name 和 n、e、d三个参数存在某种关系。what?因为我们知道n是p、q的乘积,而p与q这两个素数是用prime_random生成的,居然不变?点进去看看它prime_random函数的实现,发现set_rand_seed、gen_rand这两个函数,再点进去一看,都是类似这样的函数,贴其中的一个如下:
void *__fastcall CGenRand::set_rand_seed(CGenRand *this, unsigned int a2)
{
  void *result; // r0@1
 
  __asm
  {
    VMOV            S0, R0
    VCVT.F64.U32    D0, S0
  }
  result = &CGenRand::seed_;
  __asm { VSTR            D0, [R0] }
  return result;
这是什么鬼,感觉好慌。这里卡了很久,被坑了一波。最后发现APK中有x86的lib,打开x86的so看了一下,里面有正确的c代码,是一个伪随机数,算法如下:
hash_seed  = (double)(signed int)(0.00000095367431640625 * (hash_seed* 17.0 + 139.0)) * (-1048576.0)+ hash_seed * 17.0+ 139.0;
return hash_seed/10;
其中,hash_seed是一个全局变量,其实也就是get_rsa_key_seed函数的返回值,这就很明朗了,印证了固定的name对应固定e d n。同样的,在后面的erand函数也是用类似的方法生成了值。
都初始化好了之后开始了最后的解密函数tdecrypto。


0x7 tdecrypto函数


开头用了标准阶段的base64进行解密,然后所有解密的字符不能大于0x31,接着所有解密的字符会进入RSA解密阶段,充当大数的各个位,tdecrypto函数代码如下:
string tdecrypto(int d[MAX], int n[MAX], string text)  
{  
    struct slink *h,*p1,*p2;  
    char ch;  
    int i,j,k,c,count,temp;  
    i=0;  
    j=3;  
    count=0;  
    h=p1=p2=(struct slink * )malloc(LEN);  
    int kk;  
    for (kk = 0; kk < text.length(); kk++)  
    {    
        ch = text.at(kk);  
        c=ch;        
        if(j==3)  
        {  
            p1->bignum[MAX-2]=c-48;  
            j--;  
        }  
        else if(j==2)  
        {  
            temp=c-48;  
            j--;  
        }  
        else if(j==1)  
        {  
            p1->bignum[MAX-1]=temp*10+c-48;  
            j--;  
        }  
        else if (j==0)  
        {  
            p1->bignum[i]=c-48;  
            i++;  
            if(i==p1->bignum[MAX-1])  
            {   
                i=0;  
                j=3;  
                count++;  
                if (count==1)  
                    h=p1;  
                else p2->next=p1;  
                p2=p1;  
                p1=(struct slink * )malloc(LEN);  
            }  
        }  
    }  
    p2->next=NULL;   
  
    p2=(struct slink * )malloc(LEN);  
    p1=h;  
    k=0;  
    string res;  
    if(h!=NULL)
        do  
        {  
            for(i=0;i<MAX;i++)  
                p2->bignum[i]=0;  
            expmod( p1->bignum , d ,n ,p2->bignum);            
            temp=p2->bignum[0] + p2->bignum[1]*10 + p2->bignum[2]*100;  
            if (( p2->bignum[MAX-2])=='0')  
            {  
                temp=0-temp;  
            } 
            ch=temp;/*  str[k]--->ch */  
printf("0x%02x ",(unsigned char)ch);
            res += ch;  // char a4_a5[16] ={0x9e,0xf6,0x1d,0xc2,0x8d,0x01,0x00,0x00,0xfb,0x49,0x50,0x09,0x3d,0xd5,0xee,0xff};
            k++;  
            p1=p1->next;  
            p2=(struct slink * )malloc(LEN);  
        }while (p1!=NULL);  
        return res;  
}


0x8最后一战is_tangent函数


然后解密的byte数组要为16位,这点和标准阶段的一样,接着进入最后的比较,is_tangent函数中,如果看到这个函数的名字去搜索一下,会发现可能这个函数和圆的相切,圆心距离有关系。然而这只是一个陷阱,你相信了你就输了,废话不多说,仔细看看代码,你发现了什么?
(a4-a2)^2 ? 4*params1*params2?
像不像delta?没错,这就是一个简单的一元二次方程组。
只要花点耐心,把2个方程整理一下,就会发现,a6其实就是方程中的x。
而题目的意思就是说这个方程有且只有一个根的话,你就是tangent的!
所以,直接上代码,a4直接算:
 a4 = 2 * a1 * a6 + a2;
a5可能会溢出(这里被卡了,标记一波)用的是gmp大数库,可以看我的上篇文章:
 mpz_t mpz_a1, mpz_a2, mpz_a3, mpz_a4, mpz_a5, tmp, four, ten, ds;
    mpz_init_set_str( four, "4", 0x10 );
    mpz_init_set_str( ten, "10", 0x10 );
    mpz_init_set_str( ds, "1", 0x10 );
    mpz_init_set_str( mpz_a1, buf_a1, 0x10 );
    mpz_init_set_str( mpz_a2, buf_a2, 0x10 );
    mpz_init_set_str( mpz_a3, buf_a3, 0x10 );
    mpz_init_set_str( mpz_a4, buf_a4, 0x10 );
    mpz_init( mpz_a5 );
    mpz_init( tmp );
    mpz_sub( tmp, mpz_a4, mpz_a2 );
    mpz_mul( tmp, tmp, tmp );
    mpz_divexact( tmp, tmp, four );
    mpz_divexact( tmp, tmp, mpz_a1 );
    mpz_sub( mpz_a5, mpz_a3, tmp );
这样,整个过程就明晰了,然后就是编写注册机了,最后给出我算的几组解如下:
(去掉-):
key:
11111111111111111111111111111111
code:
04NPO44NP4iPWOYPP4gPO4iNWON#W4YP0ONPO4KOPiOPWOByO4NvWOgnP4gvO4NP0ONPP4KyP4GvO2NPPiGPO2GOPiiyP2YP04NOWOgoPi0OWOBPWO0NO4YYO20vWOY2OiOoW4NPW40oOiOPW4iWO2BPW4GYPQYOO4O^OOYO04OPOvOWO4KnP2B#PiBnO2OPO2YOP4YyO24vOQYWO44yO2OvPO4yO20yOiKYO4NoO4YYOQYWOOiWP4BYPOivO2KNPOiWPigNW4Gn0ONPP4GNO24WO2iOO2ByWOYvW4O#PO0P04NPPii#OOBnP2ivPOBYOOgnW4ByW40P0ONOPi4yPiinP2GyO40OP2OnWOYvO2G2OiOvWO4WO2YyPiKYOi4nOi0OPO0PPvOOO4Y=
 
key:
00000000000000000000000000000000
code:
0OYoOONWO2NPOOBvP4GPPiYOOO4POZOWOOKvO24nP4GPO4gnPOOPOiONP2YY04NOPOYWP4gvP2KOW40vW4KoP4OWP202OiOvWOOoO2GnW4OnW4OnP2YOPiKvWQOWOO4oPi4PPO4NWOKvPOKyOOgPWOKN04OPOvYOO4O^OOYO0ONPPiB#OOgYPiKnO2YPO4YnOOgYPiGP04NOW4OPPO0NW44oO4ONOiiYP2iYP4i2OiOPPOGvP24vOO4PO24oOON#PiNnPQOWOOGPOiGyWOKvO2NWO2K#P4gOP2g#0ONOPiBWW4OvOOiNWO0yO40WOiB#O4i^OiOYO4NnP20#PiOoO20oPiBnOON#OCOWOO4POiG#O2gyOOBvO2ByW4GOOigv0OOPO3==
 
key:
48671974307316312876431870418788
code:
04NPP24YOONoO4gvPig#P2BWP4YNO40v0ONPP4N#WOYPP4BoWOioOiYWO4GNO4gv04NPOiBoOi0YPOiNWOiNP4NPOOgvP4gv04NPO4ByPi4OO4OOWOYnP20yP24YOiiW0ONPP4OPWOgPPiNyWOGYWO4oP4KYO4GP04OPOvYOO4O^OOYO04NPO4GnW4OWP20YPOGYPOgyPiiOP4OP04NPPO0YPOY#OOKyP44#WONWWOBnO2iP04NPOiGWPOBYW40OP4GWWO0WOiGPW4iW0ONPP4gWOOivW44yWOY#W4YWOO4yO4gP0ONPWOg#OOYoO2KNPOBoP2BoOOK#O44P04NPP4NYP2YOOiYvO4OOOiB#OiYYWO4W0ONPOiYWO4g#PiOOO2BvO4NyOiKWP2KW0OOPO3==
谢谢观看,如有错误之处请大侠们斧正!


By Ericky
2017.7.8


本主题帖已收到 1 次赞赏,累计¥5.00
最新回复 (22)
6
Ericky 2017-7-10 00:20
2

发文章有bug

jinsheng 2017-7-10 09:48
3
抛物线的切线
RorschachL 2017-7-10 09:57
4
rsa那里走到is_prime_san函数时校验p,q有可能出现和apk不同的情况,不知道楼主遇到没有
is_tangent函数还是和你之前的一样看x86的动态库就能明显看到两个公式,那两个公式是抛物线和切线关系的公式,函数名不是陷阱-  -
6
Ericky 2017-7-10 10:39
5
RorschachL rsa那里走到is_prime_san函数时校验p,q有可能出现和apk不同的情况,不知道楼主遇到没有 is_tangent函数还是和你之前的一样看x86的动态库就能明显看到两个公式,那两个公式是抛 ...
谢谢楼上两位提醒,附上链接  https://en.wikipedia.org/wiki/Parabola
原来是抛物线的切线,高中数学
“rsa那里走到is_prime_san函数时校验p,q有可能出现和apk不同的情况”这句话没听懂,你是调的X86吗,这个我没看过,我调的是arm
1
yimingqpa 2017-7-10 14:54
6
楼主哪里来的符号文件  ~.~
FraMeQ 2017-7-10 16:37
7
a3  +=  (segment3[1]  *  segment6[1]);
这个应该是
a3  ^=  (segment3[1]  *  segment6[1]);
6
Ericky 2017-7-10 19:57
8
FraMeQ a3 += (segment3[1] * segment6[1]); 这个应该是 a3 ^= (segment3[1] * segment6[1]);
这个不影响,没问题的
6
loudy 2017-7-11 08:40
9

6666

6
loudy 2017-7-11 08:50
10
提醒一下A-F也可以,前面有个或0x20大写转小写,不知道考虑没
6
Ericky 2017-7-11 09:41
11
loudy 提醒一下A-F也可以,前面有个或0x20大写转小写,不知道考虑没
谢谢提醒,自己调试的时候试过A-F,但是写文章忘了。。也能算  嘿嘿~谢谢指正
echohello 2017-7-11 17:25
12
膜大佬,逆了两天没发现是RSA,最后半爆破做的,可惜主办方不承认这种做法
userkeyharo 2017-7-11 19:28
13
哈哈哈哈,当时也是在最后解大数一元二次方程组卡了,都想着自己手写了,发现python的numpy直接能解,高级语言就是好用
6
Ericky 2017-7-11 22:55
14
echohello 膜大佬,逆了两天没发现是RSA,最后半爆破做的,可惜主办方不承认这种做法
  好奇怎么爆破的。。。
6
Ericky 2017-7-11 22:56
15
userkeyharo 哈哈哈哈,当时也是在最后解大数一元二次方程组卡了,都想着自己手写了,发现python的numpy直接能解,高级语言就是好用[em_3]
看来要学习py了。。
8
kanxue 2017-7-12 22:04
16
重新排了一下版
Loopher 2017-7-13 09:12
17
我是逆了看到算法是RSA,发现对算法不熟悉,又去补了一下RSA的算法,还是没解出来。楼主能不能指点下应该看什么资料,提升下对算法的熟悉。
echohello 2017-7-13 18:41
18
Ericky 好奇怎么爆破的。。。
emmm,他b64解密输入以后我看是生成了个链表,然后每个链表解密出一个char,我理解链表里控制生成的就三个char,每个大小0x31,于是我就注进去调他的函数爆了,就是慢了点。。。爆一个得5min左右~
6
Ericky 2017-7-16 00:12
19
Loopher 我是逆了看到算法是RSA,发现对算法不熟悉,又去补了一下RSA的算法,还是没解出来。楼主能不能指点下应该看什么资料,提升下对算法的熟悉。
文章中有链接啊
6
Ericky 2017-7-16 00:15
20
echohello emmm,他b64解密输入以后我看是生成了个链表,然后每个链表解密出一个char,我理解链表里控制生成的就三个char,每个大小0x31,于是我就注进去调他的函数爆了,就是慢了点。。。爆一个得5min ...
不是有300到400个char吗,就算只算爆破时间也得1500到2000分钟了啊  ,得几天。不过你能得出正确的key就已经很不错了
echohello 2017-7-17 14:35
21
Ericky 不是有300到400个char吗,就算只算爆破时间也得1500到2000分钟了啊 ,得几天。不过你能得出正确的key就已经很不错了
不是啊,你是说总密码长度400左右吧,但是程序里对比结果的时候只有16个char吧,每个char的生成是一毛一样的吧,所以只要针对一个char爆破就行了啊,对应起来就很小了吧
1
隔壁雷哥 2017-7-17 16:52
22
好厉害的样子
刘lhhh 2017-7-25 19:05
23
仿佛再次感受到当初被学霸所碾压的恐惧...
返回



©2000-2017 看雪学院 | Based on Xiuno BBS | 域名 加速乐 保护 | SSL证书 又拍云 提供 | 微信公众号:ikanxue
Time: 0.013, SQL: 10 / 京ICP备10040895号-17