我为什么那么水 发表于 2015-8-23 16:38:37

唱吧登录算法分析

唱吧登录分析
版本信息:
package: name='com.changba' versionCode='631' versionName='6.3.2'
apk的assets目录
changba6321/assets/
├── test.aac
├── test.wav
├── test2.wav

其中的test.wav、test2.wav分别为arm及x86下使用,这里仅说明arm结构
test.wav是一个音频文件,可以正常播放,但是这个文件中还包含了一个jar包和一个so文件
在apk的KTVApplication.A中会将jar包和so文件释放出来,释放文件路径分别为/data/data/com.changba/app_dex/classes.jar、/data/data/com.changba/app_dex/test.wav
随后,KTVApplication.A会通过DexClassLoader加载上面的jar包,并通过反射机制调用jar包中GaoDLoader的setName以及test函数

public static void setName(File _name) {
      GaoDLoader.file = _name;
    }

    public static void test(String path) {
      Gao.jflag = 1024;
      try {
            System.load(String.valueOf(path) + "lib" + "secret." + "so");
      }
      catch(UnsatisfiedLinkError v0) {
            System.err.println("WARNING: Could not load library!");
      }

      try {
            if(GaoDLoader.file != null && (GaoDLoader.file.exists())) {
                System.load(GaoDLoader.file.getAbsolutePath());
                return;
            }

            System.load(String.valueOf(path) + GaoDLoader.name);
      }
      catch(UnsatisfiedLinkError v0) {
            System.err.println("WARNING: Could not load library!");
      }


在test函数中加载两个so,libsecret.so在当前版本中是没有的,第二个so即为前面释放出来的test.wav

test.wav的jni_onload中注册了com.changba.utils;Gao->o这个native函数

public static native String o(Gao this, String arg1);


在KTVApplication.A中还加载了一个common的so,这个so直接在apk对应的lib目录下,libcommon.so注册了com.changba.utils;JNIUtils中声明的5个native函数

    public static native String getCodeS(JNIUtils this, String arg1)

    public static native String getJiangKey(JNIUtils this, String arg1)

    public static native String getSecretKey(JNIUtils this, String arg1)

    public static native String getUserInfo(JNIUtils this, String arg1, String arg2, String arg3)

public static native boolean isCodeS(JNIUtils this, String arg1)


通过抓包可以发现,唱吧的登录过程如下:
1.从api.changba.com获取cookie,传递的参数
action=llogin&method=ktv&macaddress=09%3A00%3A27%3Aed%3A52%3A37&channelsrc=changba&bless=1&version=6.3.2&deviceid=09%3A00%3A27%3Aed%3A52%3A37
2. 向https://secure.changba.com发起请求,传递的参数
ac=llogin
ssid=   //由第一步中的cookie.check_code计算得到

//视情况而定是否需要,check_code如果以c22a1开头则不需要,否则需要为urlencode编码形式
captcha=
//登录名
email=
//登录密码的md5
md5pwd=
//固定值
bless=1&channelsrc=changba
//设备的mac地址
macaddress=
deviceid=
//固定值
version=6.3.2
//生成的校验码
seret=
_usrinfo=

这里仅分析seret以及_usrinfo参数的获取,其他的都比较简单

这两个参数分别由com.changba.utils;JNIUtils->getSecretKey以及com.changba.utils;JNIUtils->getUserInfo生成


getSecretKey通过阅读libcommon.so,再结合动态调试,可以很快分析出来
.text:A8F9A390               STR   R12,
.text:A8F9A394               MOV   R8, R12
.text:A8F9A398               BL      memset
.text:A8F9A39C               LDR   R1, =(aS - 0xA8F9A3B0)
.text:A8F9A3A0               MOV   R2, R6
.text:A8F9A3A4               ADD   R0, SP, #0x1050+s ; s
.text:A8F9A3A8               ADD   R1, PC, R1      ; "~@%s^@"
.text:A8F9A3AC               BL      sprintf         ; 拼接传递的参数
.text:A8F9A3B0               ADD   R0, SP, #0x1050+s ; s
.text:A8F9A3B4               BL      strlen
.text:A8F9A3B8               ADD   R2, SP, #0x1050+var_1044
.text:A8F9A3BC               MOV   R1, R0
.text:A8F9A3C0               ADD   R0, SP, #0x1050+s
.text:A8F9A3C4               BL      md5_hex         ; 获得md5
.text:A8F9A3C8               ADD   R1, SP, #0x1050+src ; src
.text:A8F9A3CC               MOV   R2, #0xA      ; n
.text:A8F9A3D0               MOV   R0, SP          ; dest
.text:A8F9A3D4               BL      strncpy         ; 取生成md5的5-15位
.text:A8F9A3D8               LDR   R3,
.text:A8F9A3DC               MOV   R2, R6

getUserInfo的分析则比较麻烦,getUserInfo中调用的是com.changba.utils;Gao->o,而这个函数的实际定义在前面释放的test.wav中,需要在test.wav的jni_onload调用中找到注册的地址
.text:00000AF8               MOV   R1, R6
.text:00000AFC               LDR   R3,
.text:00000B00               BLX   R3
.text:00000B04               SUBS    R5, R0, #0
.text:00000B08               BEQ   loc_B94
.text:00000B0C               LDR   R3,
.text:00000B10               MOV   R0, R4
.text:00000B14               MOV   R1, R5
.text:00000B18               LDR   R12,
.text:00000B1C               ADD   R2, SP, #0x28+var_1C
.text:00000B20               MOV   R3, #1
.text:00000B24               BLX   R12             ; 调用env->registernatives
.text:00000B28               CMP   R0, #0
.text:00000B2C               BLT   loc_B94

动态跟踪这里的registernatives,再对照libdvm.so,

.text:0004DD0C
.text:0004DD0C loc_4DD0C                               ; CODE XREF: sub_4DBF0+ECj
.text:0004DD0C                                       ; sub_4DBF0+104j
.text:0004DD0C               STRB.W          R11,
.text:0004DD10               MOV             R1, R9 //这里的R1为注册函数的地址
.text:0004DD12               BL            _Z15dvmUseJNIBridgeP6MethodPv ; dvmUseJNIBridge(Method *,void *)
.text:0004DD16               ADD.W         R8, R8, #1
.text:0004DD1A               ADDS            R7, #0xC
.text:0004DD1C
.text:0004DD1C loc_4DD1C                               ; CODE XREF: sub_4DBF0+42j
.text:0004DD1C               CMP             R8, R10
.text:0004DD1E               BLT             loc_4DC34
可以定位到Gao.o的函数偏移为8E0

这是它的伪代码
int __fastcall sub_8E0(int a1)
{
int v1; // r3@1
int v2; // r5@1
const char *v3; // r0@1
char *v4; // r6@1
size_t v5; // r0@1
int result; // r0@1
char v7; // @1
char s; // @1
char v9; // @1
char v10; // @1
char v11; // @1
char v12; // @1
char v13; // @1
int v14; // @1

v1 = *(_DWORD *)a1;
v14 = _stack_chk_guard;
v2 = a1;
v3 = (const char *)(*(int (**)(void))(v1 + 676))();
v4 = hello(v3);//对传递参数中的字符挨个处理,得到一个和
memset(&s, 0, 0x400u);
s = 36;
v12 = 36;
v9 = 126;
v10 = 33;
v11 = 126;
sprintf(&v13, "%d%sKcFO7ABaunKYFC8G%s", v4, "changba609", "gqoZhQ");//将上面得到的和拼接一个新的字符串
v5 = strlen(&s);
md5_hex(&s, v5, &v7);//取md5
snprintf(&s, 0xBu, "%s", &v7);//取md5的10位
result = (*(int (__fastcall **)(int, char *))(*(_DWORD *)v2 + 668))(v2, &s);
if ( v14 != _stack_chk_guard )
    _stack_chk_fail(result);
return result;
}

hello函数:
char *__fastcall hello(const char *a1)
{
char *v1; // r0@1
char *v2; // r4@1
int v3; // r3@4
int v4; // r5@6
int v5; // r0@8
int v6; // t1@9

v1 = strstr(a1, "com/");
v2 = v1;
if ( v1 )
{
    if ( strlen(v1) > 4 )
    {
      v3 = (unsigned __int8)v2;
      if ( v2 )
      {
      v4 = (int)(v2 + 4);
      v2 = 0;
      do
      {
          switch ( v3 )
          {
            case 48:
            v5 = sub_1F74(49);
            break;
            case 49:
            v5 = sub_BC4(49);
            break;
            case 50:
            v5 = sub_1F74(50);
            break;
            case 51:
            v5 = sub_30E4(51);
            break;
            case 52:
            v5 = sub_1EE4(52);
            break;
            case 53:
            v5 = sub_1434(53);
            break;
            case 54:
……
hello函数看起来很麻烦,其实只要注意几个特殊字符的处理就可以了,算法如下
def ca(c):
    if c==ord('0'):
      return 49*49
    elif c==ord('9'):
      return 59*59
    elif c==ord('?'):
      return 63*68*68
    elif c==ord('a'):
      return 100*100
    elif c==ord('z'):
      return 126*126
    elif c==ord('='):
      return 0
    else:
      return c*c

hello函数里应该是故意混杂了很多分支,目的是为了静态分析难以找到Gao.o

我已经在pc上用python实现了这个完整的登陆过程,代码还是不放了,以免不必要的麻烦
$ python changba6321.py
nikyname:n****
password:****
login success!
{u'errorcode': u'ok', u'result': {u'homeurl': u'http://changba.com/u/189***', u'vip': 0, u'ismember': 0, u'accesstoken2': None, u'accesstoken3': None, u'fansnum': 0, u'city': u'\u676d\u5dde', u'district': u'', u'title': u'', u'score': 0, u'location': u' ', u'latitude': u'0', u'memberlevel': u'', u'email': u'n****@changba.com', u'province': u'\u6d59\u6c5f', u'account_type': u'\u5531\u5427', u'agetag': u'90\u540e', u'accounttype3': u'\u6700\u6dd8', u'accounttype2': u'\u6700\u6dd8', u'viptitle': u'', u'memberid': u'189109611', u'astro': u'\u6469\u7faf\u5ea7', u'phone': u'18*****', u'birthday': u'1990-01-01', u'account_original': u'18****', u'headphoto': u'http://img.changba.com/cache/photo/4/4.jpg', u'userlevel': {u'starRank': u'10\u4e07\u540d\u4ee5\u5916', u'weekCost': -1, u'richLevel': 0, u'monthCost': -1, u'starLevel': 0, u'userid': u'101831092', u'pop': -1, u'richRank': u'10\u4e07\u540d\u4ee5\u5916', u'nextStarLevel': 0, u'cost': -1, u'monthPop': -1, u'richLevelName': u'', u'starLevelName': u'', u'nextRichLevel': 0}, u'accountid2': None, u'accountid3': None, u'nickname': u'n****', u'level': u'1', u'access_token': u'85a951aaf******a', u'gender': u'1', u'userid': u'101*****', u'longitude': u'0', u'token': u'T43f8d****a', u'signature': u'', u'accountid': u'n****', u'valid': 1, u'friendsnum': 0}}

整个分析过程还是有不少问题,也没有办法把所有问题展示出来,只能说痛并快乐着……

仅作技术交流,请勿滥用

水波摇曳 发表于 2015-8-23 16:49:11

感谢分析 赞一个~

默小坑 发表于 2015-8-23 16:53:53

感谢分享,赞两个

十万个为什么 发表于 2015-8-23 19:19:53

菜鸟不懂

freeparty 发表于 2015-8-23 23:13:18

我怎么记得在其他地方见过

huluxia 发表于 2015-8-23 23:13:39

感谢分享

听鬼哥说故事 发表于 2015-8-24 09:27:47

不错,终于舍得放出来了哇~~赞一个~~~~

huang4727603 发表于 2015-8-24 10:49:53

这个的加密我也看过,安卓下我看的时候有2个加密,一个是根据url +干扰 md5得到一个参数,然后是全部参数根据字符换算成数字,全部加起来,然后再进行md5,加密,得到另一个字段,当时没有找到这个代码,我还是一个个字符穷举过来算出来的userinfo="$~!~$"+num.to_s+"changba609KcFO7ABaunKYFC8GgqoZhQ"#把num填入字符串最后生成新的字符串 当时写的算法是这样子的 不知道现在变了没,楼主研究的很仔细,比我更深入哈哈 学习

我为什么那么水 发表于 2015-8-24 12:26:29

freeparty 发表于 2015-8-23 23:13
我怎么记得在其他地方见过

在看雪和这里发了

我为什么那么水 发表于 2015-8-24 12:28:31

huang4727603 发表于 2015-8-24 10:49
这个的加密我也看过,安卓下我看的时候有2个加密,一个是根据url +干扰 md5得到一个参数,然后是全部参数根 ...

恩,我看的这个版本也差不多这样,大神以后多分享啊O(∩_∩)O哈哈~
页: [1] 2
查看完整版本: 唱吧登录算法分析