2017腾讯游戏安全技术竞赛 Round 1 标准及进阶题解分析
PS:抱歉,很久没冒泡了。最近太忙了,发几篇之前参加比赛的笔记,分享一下。前言
为了学习死磕的精神,我也来做题。我选择了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; // @5
void *ptr; // @2
int v15; // @2
int v16; // @2
__int64 v17; // @2
__int64 v18; // @7
__int64 v19; // @7
__int64 v20; // @7
__int64 v21; // @5
__int64 v22; // @7
int v23; // @7
int v24; // @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 * segment8) << 16 ;
a1 += segment1 ^ segment8;
a1 += segment1 % (segment8+1)+ 1;
a1 += segment1 / (segment8+1);
a2 =0;
a2 += (segment2 ^ segment7) << 16 ;
a2 += (segment2 % (segment7+3));
a2 += (segment2 / (segment7+1)) + 5;
a2 += (segment2 + (segment7)) ;
a3 = 0;
a3 += (segment3 / (3 + segment6) )<< 16 ;
a3 += (segment3 * segment6);
a3 +=segment3 %(segment6 + 7) +5;
a3 +=segment3 + segment6;
a6 = 0;
a6 += (segment4 + segment5) << 16;
a6 *= segment4/(segment5+2);
a6 += segment4 %(segment5 +5) +7;
a6 += segment4 * segment5;
好了,这就是最后校验用到的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=p=q=n=d=e=0;
prime_random(p,q);
mul(p,q,n);
mov(p,p1);
p1--;
mov(q,q1);
q1--; /*/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, }
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, int n, 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=c-48;
j--;
}
else if(j==2)
{
temp=c-48;
j--;
}
else if(j==1)
{
p1->bignum=temp*10+c-48;
j--;
}
else if (j==0)
{
p1->bignum=c-48;
i++;
if(i==p1->bignum)
{
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=0;
expmod( p1->bignum , d ,n ,p2->bignum);
temp=p2->bignum + p2->bignum*10 + p2->bignum*100;
if (( p2->bignum)=='0')
{
temp=0-temp;
}
ch=temp;/*str--->ch */
printf("0x%02x ",(unsigned char)ch);
res += ch;// char a4_a5 ={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 路过五次提笔写作业 膜拜大牛 {:17_1065:} 感谢大牛 膜拜大牛 仰望下 离大师还远 慢慢学习 就冲我看不懂 顶一个 看着这么一大堆就头疼{:1_937:} 看的我头疼 谢谢楼主分享经验,很不错,进来学习一下。。{:17_1070:} 膜拜大佬666