首页
论坛
专栏
课程

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

6天前 1110

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

6天前
1110

目录

 

听说这个是这个题是2016年腾讯游戏移动安全赛的题
主要是注册机的编写
以前自己学习拿来练习ARM汇编和算法逆向的
自己写的ARM的汇编的就不贴了 这里主要是F5后的

1. java层定位关键点

夜神模拟器安装apk 输入name和code
图片描述

 

打开androidkiller搜索字符串Check Fail!
图片描述

 

找到关键点,发现了一个NativeCheckRegister
图片描述

 

找到NativeCheckRegister的声明的地方

 

图片描述

 

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

2. so层分析

1. 找关键函数

libCheckRegister.so 拖入IDA中
找到相关函数,因为函数不多我们可以直接看,如果函数多可以用ctrl+f搜索
图片描述
双击进去
图片描述
由于是jni函数,因此我们导入相关结构体并重命名

2. 导入jni 并重新命名

按insert快捷键,导入JNI相关结构体
图片描述
图片描述
图片描述

 

参数,变量,函数名都重命名得到如下所示
图片描述

3. 算法分析

我们重命名的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;
}

4. 注册机思路

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

1. 逻辑流程

s[] <--用户名name
v15[]<--密码pwd
v12[]<--s[]
v13[]<--v15[]
v13[]<--v12[]

2. 思路

  1. 我们的注册机是已知name求pwd
  2. 根据上面的逻辑我们可以得到求解过程
    用户名name -> s[] -> v12[] -> v13[]->v15[]->密码pwd

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

    图片描述

  4. 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
}

3. 注册机

图片描述

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得到密码

2. 源码

main.cpp

#include<stdio.h>
#include<windows.h>
#include "base64.h"
#include <string>


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 <string>

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

base64.cpp

#include "base64.h"
#include <iostream>

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;
}

4. 总结与心得

  1. 算法逆向真的是体力活,坐不住根本分析不下,这里是F5后分析的比较简单,干看ARM汇编更吃体力
  2. 常用加解密的算法还是要了解啊,最好熟悉,这样对逆向的帮助很大
  3. 注册机的编写心得:

    1. 能用现有可以利用的代码就用
    2. 算法逆向有时候就像做数学题,就是已知条件求未知,通过找他们之间的关系,一步一步打通关系,最后得到解,但是有时候这个解并不是完美的解。
  4. 练习的时候还是要少用F5 多看ARM汇编,这才是基本功

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


[推荐]看雪企服平台,提供安全分析、定制项目开发、APP等级保护、渗透测试等安全服务!

上传的附件:
上一主题 下一主题
最新回复 (5)
joker陈 3天前
2
0
感谢分享。
Editor 3天前
3
0
感谢分享~
DlyWtF700 1天前
4
0
不错不错。
kevin lee 13小时前
5
0
感谢分享,正准备看这块呢。
岭南散人 1 10小时前
6
0
不明觉厉
游客
登录 | 注册 方可回帖
返回