首页   

Android算法逆向学习笔记之2016腾讯游戏安全移动赛题Tencent2016A(含注册机)

看雪学苑  · 互联网安全  · 5 年前

听说这个是这个题是2016年腾讯游戏移动安全赛的题

主要是注册机的编写

以前自己学习拿来练习ARM汇编和算法逆向的

自己写的ARM的汇编的就不贴了 这里主要是F5后的


 java层定位关键点


夜神模拟器安装apk,输入name和code:


 

打开 Androidkiller 搜索字符串Check Fail!


 

找到关键点,发现了一个NativeCheckRegister


 

找到NativeCheckRegister的声明的地方

 

 

我们发现它是一个Native函数,在so层,因此我们只能去so层用ida去分析。



 so层分析



一、 找关键函数

libCheckRegister.so 拖入IDA中,找到相关函数,因为函数不多我们可以直接看,如果函数多可以用ctrl+f搜索。




双击进去



由于是jni函数,因此我们导入相关结构体并重命名



二、 导入jni 并重新命名

按insert快捷键,导入JNI相关结构体。




 

参数,变量,函数名都重命名得到如下所示:




三、算法分析

我们重命名的check里面就是我们想要的算法了,点击进去具体分析。


首先也是参数变量函数名重新命名。

 

我们发现这里识别错误,数组大量使用了[j*4] 我们可以将数组类型转化为int。最后我们得到代码是:


int __fastcall check(const char *name, char *pwd)
{
char *v2; // r6@1
signed int v3; // r5@1
int result; // r0@2
signed int v5; // r4@3
char *v6; // r7@4
int v7; // r3@4
int v8; // r4@6
int j; // r4@7
int v10; // r1@8
const char *v11; // [sp+Ch] [bp-464h]@1
int v12[5]; // [sp+18h] [bp-458h]@7
int v13[5]; // [sp+2Ch] [bp-444h]@7
int s[5]; // [sp+40h] [bp-430h]@3
int v15[234]; // [sp+54h] [bp-41Ch]@5
int v16; // [sp+454h] [bp-1Ch]@1

v2 = pwd;
v11 = name;
v16 = _stack_chk_guard; // 堆栈检查 用于保护堆栈
v3 = j_j_strlen(name); // 求用户名长度
if ( (unsigned int)(v3 - 6) > 0xE )
goto LABEL_18; // 这里我们得到成立的条件是 userLength-6<=14
j_j_memset(s, 0, 0x14u); // 数组置0
v5 = 0; // 循环计数器 相当于i 这里我们也重命名一下
do
{
v6 = (char *)s + v5; // 对数组s[]赋值
v7 = v11[v5 % v3] * (v5 + 20160126) * v3; // v7=user[i%userLength]*(i+20160126)*userLength
++v5; // i++
*(_DWORD *)v6 += v7; // s[i]+=user[i%userLength]*(i+20160126)*userLength
}
while ( v5 != 16 ); // i<16
j_j_memset(v15, 0, 0x400u); // 数组置0
if ( sub_146C(v2) > 1024 || (v8 = sub_1498((char *)v15, v2), v8 != 20) )
{ // 这个条件等一会分析
LABEL_18: // 现在先分析其他的 并且重命名给出具体的算法
result = 0;
}
else
{
j_j_memset(v12, 0, 0x14u); // 数组置0
j_j_memset(v13, 0, 0x14u); // 数组置0
j = 0; // j=0 计数器 我们还是重命名为j
do
{ // v12[] v13[] 赋值
v10 = v15[j]; // v10=v15[j] 刚才对pwd算出的数组
v12[j] = s[j] / 10; // v12[j]=s[j]/10
v13[j] = v10;
++j; // j++
}
while ( j != 5 ); // j<5
result = 0;
if ( v13[4] + v12[0] == v13[2] // v13[4]+v12[0]==v13[2]
&& v13[4] + v12[0] + v12[1] == 2 * v13[4] // v12[0]+v12[1]==v13[4]
&& v12[2] + v13[3] == v13[0] // v12[2]+v13[3]==v13[0]
&& v12[2] + v13[3] + v12[3] == 2 * v13[3] )// v12[2]+v12[3]==v13[3]
{
result = (unsigned int)(v12[4] + v13[1] - 3 * v12[2]) <= 0;
} // 若要返回true 则这个表达式必须返回true
// 也就是要这个等式成立
}
if ( v16 != _stack_chk_guard ) // 堆栈检查 用于保护堆栈
j_j___stack_chk_fail(result); // 如果堆栈发生错误 返回false
return result;
}



四、 注册机思路

我们这里梳理下流程逻辑和思路


1、逻辑流程

s[]
v15[]
v12[]
v13[]
v13[]


2. 思路

① 我们的注册机是已知name求pwd

② 根据上面的逻辑我们可以得到求解过程
用户名name -> s[] -> v12[] -> v13[]->v15[]->密码pwd

③ 那么v12[]和v13[]的关系怎么得到 ?



④v15[]->pwd怎么获得?


那就要我们分析上面那两个函数了



分析 sub_1498发现这是个base64的解码函数


一开始看到头都大了 这个算法的计算量又大了


不过当看到一个数组的时候,立马就点进去看看有什么特征




里面除了一些0x40外,其他的字符特别像base64的表。

即使是base64表也不能保证这个表没有变形。还是看代码吧......


base64解码注释


解码的具体代码,里面注释了详细的解码过程:


int __fastcall DecodeBase64(char *v15, char *pwd)
{
char *v2; // r3@1
int v3; // r3@3
int v4; // r2@3
int v5; // r5@3
char *v6; // r3@4
int v7; // r3@5
int v8; // r6@5

v2 = pwd; // 设i=0
do // 取出第一个不为0x40的值
++v2; // i++
while ( (unsigned __int8)g_arry[(unsigned __int8)*(v2 - 1)] <= 0x3Fu );// arry[pwd[i]]>0x3f
// pwd[i]在数组内对应的值是不是<=0x3f
v3 = v2 - pwd;
v4 = v3 - 1; // 求个数
v5 = 3 * ((v3 + 2) / 4); // v15的长度
while ( 1 )
{
v6 = v15;
if ( v4 <= 4 ) // 剩下最后4个字节或者不足4个字节的时候跳出循环
break;
v4 -= 4; // 长度 v4循环-4
*v15 = ((unsigned __int8)g_arry[(unsigned __int8)pwd[1]] >> 4) | 4 * g_arry[(unsigned __int8)*pwd];//
// v15[0]=g_arry[pwd[1]>>4|4*g_arry[pwd[0]]]
v15[1] = ((unsigned __int8)g_arry[(unsigned __int8)pwd[2]] >> 2) | 16 * g_arry[(unsigned __int8)pwd[1]];//
// v15[1]=g_arry[pwd[2]>>2|16*g_arry[pwd[1]];
v7 = (unsigned __int8)pwd[2];
v8 = (unsigned __int8)pwd[3];
pwd += 4; // pwd循环到下个四字节
v15[2] = (g_arry[v7] << 6) | g_arry[v8]; // v15[2]=(g_arry[pwd[2]<<6)|g_arry[pwd[3]]
v15 += 3; // v15跳到下三个字节
}
if ( v4 > 1 ) // 如果剩余1-3个字节的时候
{
*v15 = ((unsigned __int8)g_arry[(unsigned __int8)pwd[1]] >> 4) | 4 * g_arry[(unsigned __int8)*pwd];//
// v15[0]=g_arry[pwd[1]]>>4|4*g_arry[pwd[0]]
if ( v4 == 2 ) // 当剩两个字节的时候
{
v6 = v15 + 1; // v15[1]=0
}
else
{
v15[1] = ((unsigned __int8)g_arry[(unsigned __int8)pwd[2]] >> 2) | 16 * g_arry[(unsigned __int8)pwd[1]];//
// v15[1]=g_arry[pwd[2]]>>2|16*g_arry[pwd[1]]
if ( v4 == 4 ) // 当剩余4个字节
{
v6 = v15 + 3; // v15[3]=0
v15[2] = (g_arry[(unsigned __int8)pwd[2]] << 6) | g_arry[(unsigned __int8)pwd[3]];//
// v15[2]=g_arry[pwd[2]<<6|g_arry[pwd[3]]
}
else
{
v6 = v15 + 2; // v15[2]=0
}
}
}
*v6 = 0;
return v5 - (-v4 & 3); // 返回解码后的长度
}


sub_146C分析


这里和上个函数的开头差不多,通过我们分析得知这是获得解码后的长度:


int __fastcall sub_146C(char *pwd)
{
char *v1; // r3@1

v1 = pwd;
do
++v1; // i=0
while ( (unsigned __int8)g_arry[(unsigned __int8)*(v1 - 1)] <= 0x3Fu );// arry[pwd[i]]<=0x3f
return 3 * ((v1 - pwd + 2) / 4) + 1; // return 3*((i+2)/4)+1
}



注册机




一、流程

1、写注册机前再梳理一遍,name得到一个数组s[],可以由ida得到


do
{
v6 = (char *)s + v5;
v7 = v11[v5 % v3] * (v5 + 20160126) * v3;
++v5;
*(_DWORD *)v6 += v7;
}


2、由s[]可以得到v12[]


3、v12[]可以得到v13[]

v13[0]=2v12[2]+v12[3]
v13[1] <= 3  v12[2]-v12[4]
v13[2]=2*v12[0]+v12[1]
v13[3]=v12[2] + v12[3]
v13[4]=v12[0]+v12[1]


4、v13[]得到v15[]


5、v15[]通过encodebase64得到密码



二、 源码

main.cpp


#include
#include
#include "base64.h"
#include


int main()
{
const unsigned char name[20] = {0};

char *v2; // r6@1
signed int v3; // r5@1
int result; // r0@2
signed int v5; // r4@3
char *v6; // r7@4
int v7; // r3@4
int v8; // r4@6
int j; // r4@7
int v10; // r1@8
const unsigned char *v11; // [sp+Ch] [bp-464h]@1
int v12[5] = {0}; // [sp+18h] [bp-458h]@7
int v13[5] = {0}; // [sp+2Ch] [bp-444h]@7
int s[5] = {0}; // [sp+40h] [bp-430h]@3
int v15[234] = {0}; // [sp+54h] [bp-41Ch]@5

printf("请输入name:");
scanf_s("%s", name, 20);

// name->s

v11 = name;
v5 = 0;
v3 = strlen((const char*)name);
do
{
v6 = (char *)s + v5;
v7 = v11[v5 % v3] * (v5 + 20160126) * v3;
++v5;
*(DWORD *)v6 += v7;
} while (v5 != 16);

// s->v12
j = 0;
do
{
v12[j] = s[j] / 10;
++j;
} while (j != 5);

// v12->v13
v13[0] = 2 * v12[2] + v12[3];
v13[1] = 3 * v12[2] - v12[4];
v13[2] = 2 * v12[0] + v12[1];
v13[3] = v12[2] + v12[3];
v13[4] = v12[0] + v12[1];

// v13->v15
j = 0;
do
{
v15[j] = v13[j];
++j;
} while (j != 5);

// v15->pwd
std::string encodeStr = base64_encode((unsigned char*)v15, 20);
const char* pwd = encodeStr.c_str();

printf("密码是:%s",pwd);
system("pause");
return 0;
}


base64.h


#pragma once
#include

std::string base64_encode(unsigned char const*, unsigned int len);
std::string base64_decode(std::string const& s);


base64.cpp


#include "base64.h"
#include

static const std::string base64_chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"0123456789+/";


static inline bool is_base64(unsigned char c) {
return (isalnum(c) || (c == '+') || (c == '/'));
}

std::string base64_encode(unsigned char const* bytes_to_encode, unsigned int in_len) {
std::string ret;
int i = 0;
int j = 0;
unsigned char char_array_3[3];
unsigned char char_array_4[4];
while (in_len--) {
char_array_3[i++] = *(bytes_to_encode++);
if (i == 3) {
char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
char_array_4[3] = char_array_3[2] & 0x3f;
for (i = 0; (i < 4); i++)
ret += base64_chars[char_array_4[i]];
i = 0;
}
}

if (i)
{
for (j = i; j < 3; j++)
char_array_3[j] = '\0';
char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
char_array_4[3] = char_array_3[2] & 0x3f;
for (j = 0; (j < i + 1); j++)
ret += base64_chars[char_array_4[j]];
while ((i++ < 3))
ret += '=';
}
return ret;
}

std::string base64_decode(std::string const& encoded_string) {
int in_len = encoded_string.size();
int i = 0;
int j = 0;
int in_ = 0;
unsigned char char_array_4[4], char_array_3[3];
std::string ret;

while (in_len-- && (encoded_string[in_] != '=') && is_base64(encoded_string[in_])) {
char_array_4[i++] = encoded_string[in_]; in_++;
if (i == 4) {
for (i = 0; i < 4; i++)
char_array_4[i] = base64_chars.find(char_array_4[i]);
char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4);
char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3];

for (i = 0; (i < 3); i++)
ret += char_array_3[i];
i = 0;
}
}
if (i) {
for (j = i; j < 4; j++)
char_array_4[j] = 0;
for (j = 0; j < 4; j++)
char_array_4[j] = base64_chars.find(char_array_4[j]);
char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4);
char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3];
for (j = 0; (j < i - 1); j++) ret += char_array_3[j];
}
return ret;
}



总结与心得


① 算法逆向真的是体力活,坐不住根本分析不下,这里是F5后分析的比较简单,干看ARM汇编更吃体力。

② 常用加解密的算法还是要了解啊,最好熟悉,这样对逆向的帮助很大。

③ 注册机的编写心得:

  • 能用现有可以利用的代码就用

  • 算法逆向有时候就像做数学题,就是已知条件求未知,通过找他们之间的关系,一步一步打通关系,最后得到解,但是有时候这个解并不是完美的解。

④ 练习的时候还是要少用F5 多看ARM汇编,这才是基本功

⑤ 书读百遍其义自见,同样逆向百遍其义自见




- End -




看雪ID:lcdxsun         

https://bbs.pediy.com/user-768906.htm



本文由看雪论坛 lcdxsun 原创

转载请注明来自看雪社区



热门图书推荐

立即购买!




热门文章阅读

1、议题征集 | 2019看雪安全开发者峰会

2、QQ 浏览器 JecStruct 协议

3、FastHook——实现.dynsym段和.symtab段符号查询

4、CVE-2017-13772分析及mips栈溢出利用总结




公众号ID:ikanxue

官方微博:看雪安全

商务合作:wsc@kanxue.com



点击下方“阅读原文”,查看更多干货!
推荐文章
IMI财经观察  ·  鄂志寰:打造国之大者货币崛起的独特实践  ·  7 月前  
© 2022 51好读
删除内容请联系邮箱 2879853325@qq.com