xx医美登陆协议分析
2019-10-30 14:16:33 Author: bbs.pediy.com(查看原文) 阅读量:191 收藏

在开始正文之前, 说一些题外话。

    1.这是本人在论坛注册后的第三次发帖,如果有格式错误, 发布的版块错误, 主题不合规侵犯什么权利之类的, 请各位明示指出。管理员也可以删帖。

    2.因为本人水平有限, 某些地方写的可能是错的, 也请各位大佬指正, 不胜感激。

    3.当前文章是xx医美8月的版本8月份当月分析了大部分字段,写文章拖到了9月份,写完之后一直不满意,因为还有一些字段没有分析清楚就草草了结,而且整个思路也不是特别清楚,像流水账一样。现在在翻之前笔记的时候翻到了这个,想着不发出来比较可惜 ,发出来后大佬们指出来我一些错误我也能更好的进步,所以发出来跟大佬们交流下。

    4:文章于9月首发于易锦,作者为本人,当前版本是发布后两天内修改,修改内容不多。

以下正文:

写文章初衷

    1 最近在学习协议分析这块,很多时候一些apk只是简单看一下,大概看清楚流程就不想继续详细去分析了。没有很详细的去分析一个apk,想借这次写文章,梳理一下整个流程。

    2 答应了一个大佬要写一篇文章发出来。

    3 因为水平比较菜且时间不足,这次就选了一个简单的apk,虽然还有几个同样简单的apk在备选之列,但是我每天出入工地的时候,电梯广告总是在播放xx医美的广告,做女人整好 这个广告我每天都会看到几遍,那就撸它把。

这里选择了分析用账号密码登陆的登陆协议


测试账号:13355556666

测试密码:kkk111222

burpsuite 抓登陆包:



格式下各个字段 看着清楚一些

POST /v8/passport/apploginnew HTTP/1.1
Cookie: __usersign__=1568523653313812392; expires=Tue, 15-Oct-2019 15:59:59 GMT; Max-Age=2631546; path=/; domain=.soyoung.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 458
Host: api.soyoung.com
Connection: close
Accept-Encoding: gzip, deflate
User-Agent: okhttp/3.10.0

xy_device_token=1402b247ff996e94f16ce53a27377309e2
&password=kkk111222
&request_id=00e09fba6857bf87ed88030d6fd6680f
&uuid=ffffffff-9dbd-915d-ffff-ffffde5a499b
&app_id=2
&lver=7.32.2
&login_name=13355556666
&_sign=%242a%2405%24xWMKvGsGdANgOytr88Tqf.mPsuq6BAFUnKr2ITTyWJriXaidIyEO6
&device_id=175988620
&sys=2
&key=2f77dcb9f66cacdc4cb1f592974816f2
&uid=0
&sm_device_id=20190813220351b85597663f19089a4a4550cbc15f62f4014b4f187300ca7f
&pinyin=soyoung
&_time=1568523793
&cityId=0

当我看到密码是明文的时候 真的是一口血吐了出来  很久没看到这么坦诚相见的apk了

当前app字符串并没有加密 因为要分析密码登录的协议  所以 可以搜索类 

login_name password  这样的字段 

尝试搜索后发现 password 在源码中引用太多 不太好定位分析 所以选择 login_name字段定位

当前app搜索 login_name可以 直接到到关键位置的  如下图


但是谨慎起见 为了防止流程跟踪出现错误

我一般会结合monitor 的方法跟踪 相互印证  打开monitor  按下登陆键


一般此类登陆协议 整理方法调用的时候 我一般会搜索 onclick() 这个按键的处理的方法 因为是按了登陆键才触发登陆流程的

  onclick() 一路跟随 整理出相关调用层级

onclick()
void onViewClick(View)
void a(PasswordLoginFragment)
void userlogin()
void getContent(String, String)      
void pwdLoginRequest(String, String)
void passwordLoginRequest(String, String)
Observable passwordLoginRequest(String, String)
Observable post(String, HashMap<String, String>)    
TreeMap getCommonParasm(HashMap<String, String>) 
TreeMap getCommonParasm(HashMap<String, String> , String)    

整理调用层级的时候 可以用 jadx-gui 看一下这些方法的源码  方便后续分析

比较有亮点的是以下两个方法

1 void passwordLoginRequest(String, String)

2 TreeMap getCommonParasm(HashMap<String, String> , String)  


上面方法中 看到了登陆数据包的大量字段  所以决定重点分析两个方法

这里准备动态调试结合静态分析  所以先反编译apk  

am start -D com.youxiang.soyoungapp/.ui.SplashActivity

调试启动app

转发端口

adb forward tcp:8700 jdwp:20951

动态调试观察第一个 比较有料的函数  void passwordLoginRequest(String, String)



1 void passwordLoginRequest(String, String) 

调试得知 两个参数是明文的 手机号和密码  根据调试结果 修改变量名 增加可读性

   public Observable passwordLoginRequest(String str_phone, String str_pass) {
        HashMap map_login_data = new HashMap();
        String str_key = FlagSpUtils.getKey(str_phone, str_pass);
        map_login_data.put("login_name", str_phone);  
        map_login_data.put("password", str_pass);
        map_login_data.put("key", str_key);
        map_login_data.put("sm_device_id", SmAntiFraud.getDeviceId());
        return this.post(LoginUrl.PWD_LOGIN, map_login_data);
    }

上面的函数里有4个字段 

login_name   		明文
password     		明文
key          		getkey返回值
sm_device_id 		getDeviceId()

getkey()方法





login_name   明文
password     明文
key          md5(lavion_soyoung@2013_ + "手机号" + "_" + "密码")
sm_device_id getDeviceId()

getDeviceId()



搜索公司名字后 找到了 sdk的官网


看起来就是这个sdk产品 生产了唯一标识 可能防范黑产表哥把


一路跟踪 找到产生id的函数  



当前sdk混淆做的比较彻底 变量方基本都是 000OOooo之类的命名

对当前方法简单分析后 修改变量名 如下图


这里不继续分析  因为继续分析下去篇幅太大 有兴趣的小伙伴可以下载sdk 看看源码 或者自己直接逆向

login_name   明文
password     明文
key          md5(lavion_soyoung@2013_ + "手机号" + "_" + "密码")
sm_device_id getDeviceId() 第三方sdk返回值

第一个有字段的函数分析完毕  下面开始第二个 

--------------------------------------------------------------------------------------------------------------------------------

2 getCommonParasm(HashMap<String, String> hashMap, String str)  函数

大部分字段在这里完成拼接 下面是jadx-gui 反编译的源码

    public static TreeMap<String, String> getCommonParasm(HashMap<String, String> hashMap, String str) {
        TreeMap<String, String> treeMap = new TreeMap<>();
        if (hashMap != null) {
            treeMap.putAll(hashMap);
        }
        UserInfo user = UserDataSource.getInstance().getUser();
        if (TextUtils.isEmpty(treeMap.get("uid"))) {
            treeMap.put("uid", user.getUid());
        }
        if (TextUtils.isEmpty(treeMap.get("xy_token"))) {
            String xy_token = user.getXy_token();
            if (!TextUtils.isEmpty(xy_token)) {
                treeMap.put("xy_token", xy_token);
            }
        }
        if (TextUtils.isEmpty(treeMap.get("lat"))) {
            treeMap.put("lat", LocationHelper.getInstance().latitude);
        }
        if (TextUtils.isEmpty(treeMap.get("lng"))) {
            treeMap.put("lng", LocationHelper.getInstance().longitude);
        }
        if (TextUtils.isEmpty(treeMap.get("cityId"))) {
            treeMap.put("cityId", LocationHelper.getInstance().district_id);
        }
        String string = AppPreferencesHelper.getString("device_id");
        String string2 = AppPreferencesHelper.getString(AppPreferencesHelper.XY_DEVICE_TOKEN);
        if (TextUtils.isEmpty(uuid)) {
            uuid = DeviceUtils.getUUID(Utils.getApp());
        }
        String str2 = "2";
        treeMap.put(NotificationCompat.CATEGORY_SYSTEM, str2);
        treeMap.put("lver", AppUtils.getAppVersionName());
        treeMap.put("pinyin", getChannelID(Utils.getApp()));
        if ("com.youxiang.soyoungapp.diary".equals(str)) {
            str2 = "40";
        } else if ("com.youxiang.tw".equals(str)) {
            str2 = "39";
        } else if ("com.youxiang.soyoungapp.peri".equals(str)) {
            str2 = MessageConstantInterface.MessageType_Reply;
        }
        treeMap.put("app_id", str2);
        treeMap.put("device_id", string);
        treeMap.put(AppPreferencesHelper.XY_DEVICE_TOKEN, string2);
        treeMap.put("uuid", uuid);
        long currentTimeMillis = System.currentTimeMillis();
        treeMap.put("_time", (currentTimeMillis / 1000) + "");
        treeMap.put("request_id", createRequestId(currentTimeMillis, str));
        treeMap.put("_sign", paramsSign(getParamsString(treeMap)));
        return treeMap;
    }

手动修改变量名 提升可读性后

 public static TreeMap getCommonParasm(HashMap map_login_arg, String str_page_name) {
        TreeMap map_login_data = new TreeMap();
        if(map_login_arg != null) {
            map_login_data.putAll(map_login_arg);
        }

        UserInfo userinfo = UserDataSource.getInstance().getUser();
        if(TextUtils.isEmpty(((CharSequence)map_login_data.get("uid")))) {
            map_login_data.put("uid", userinfo.getUid());
        }

        if(TextUtils.isEmpty(((CharSequence)map_login_data.get("xy_token")))) {
            String str_xy_token = userinfo.getXy_token();
            if(!TextUtils.isEmpty(str_xy_token)) {
                map_login_data.put("xy_token", str_xy_token);
            }
        }

        if(TextUtils.isEmpty(((CharSequence)map_login_data.get("lat")))) {
            map_login_data.put("lat", LocationHelper.getInstance().latitude);
        }

        if(TextUtils.isEmpty(((CharSequence)map_login_data.get("lng")))) {
            map_login_data.put("lng", LocationHelper.getInstance().longitude);
        }

        if(TextUtils.isEmpty(((CharSequence)map_login_data.get("cityId")))) {
            map_login_data.put("cityId", LocationHelper.getInstance().district_id);
        }

        String str_dev_id = AppPreferencesHelper.getString("device_id");
        String str_device_token = AppPreferencesHelper.getString("xy_device_token");
        if(TextUtils.isEmpty(ApiHeader.uuid)) {
            ApiHeader.uuid = DeviceUtils.getUUID(Utils.getApp());
        }

        String str_app_Id = "2";
        map_login_data.put("sys", "2");
        map_login_data.put("lver", AppUtils.getAppVersionName());
        map_login_data.put("pinyin", ApiHeader.getChannelID(Utils.getApp()));
        if("com.youxiang.soyoungapp.diary".equals(str_page_name)) {
            str_app_Id = "40";
        }
        else if("com.youxiang.tw".equals(str_page_name)) {
            str_app_Id = "39";
        }
        else if("com.youxiang.soyoungapp.peri".equals(str_page_name)) {
            str_app_Id = "50";
        }

        map_login_data.put("app_id", str_app_Id);
        map_login_data.put("device_id", str_dev_id);
        map_login_data.put("xy_device_token", str_device_token);
        map_login_data.put("uuid", ApiHeader.uuid);
        long l_curtime = System.currentTimeMillis();
        map_login_data.put("_time", l_curtime / 1000L + "");
        map_login_data.put("request_id", ApiHeader.createRequestId(l_curtime, str_page_name));
        map_login_data.put("_sign", ApiHeader.paramsSign(ApiHeader.getParamsString(map_login_data)));
        return map_login_data;
    }

列出函数中字段的顺序 逐个分析

uid                      userinfo.getUid() 返回值
xy_token                 登陆数据包未出现不理会
lat                      登陆数据包未出现不理会
lng                      登陆数据包未出现不理会
cityId                   LocationHelper.getInstance().district_id
device_id                AppPreferencesHelper.getString("device_id");
xy_device_token          AppPreferencesHelper.getString("xy_device_token");
uuid                     ApiHeader.uuid
sys                       "2"
lver                     AppUtils.getAppVersionName()
pinyin                   ApiHeader.getChannelID(Utils.getApp()
app_id                   对比包名给出
_time                    System.currentTimeMillis() / 1000
request_id               ApiHeader.createRequestId(l_curtime, str_page_name)
_sign                    ApiHeader.paramsSign(ApiHeader.getParamsString(map_login_data)                              

整理后

//未出现成员
xy_token                 登陆数据包未出现不理会
lat                      登陆数据包未出现不理会
lng                      登陆数据包未出现不理会

//固定值和不依赖自定义对象的成员
sys                       "2"
_time                    System.currentTimeMillis() / 1000
app_id                   对比包名给出    


//成员被AppPreferencesHelper方法返回值赋值
device_id                AppPreferencesHelper.getString("device_id");
xy_device_token          AppPreferencesHelper.getString("xy_device_token");

//成员被ApiHeader 方法返回值赋值
uuid                     ApiHeader.uuid  
pinyin                   ApiHeader.getChannelID(Utils.getApp()
request_id               ApiHeader.createRequestId(l_curtime, str_page_name)
_sign                    ApiHeader.paramsSign(ApiHeader.getParamsString(map_login_data) 
 
//以来其他自定义对象的成员                                            
uid                      userinfo.getUid() 返回值                                              
lver                     AppUtils.getAppVersionName()  
cityId                   LocationHelper.getInstance().district_id

这里 首先看 device_id  和 xy_device_token



跟随 getString方法 看到这里调用了AppSpHelper.getInstance().getString(str);、


跟随进去之后 发现调用了AppSpHelper.getString()方法


这里的sp成员的类型 时安卓sdk的一个接口 基于xml实现数据的存取

具体可以看官方的接口文档

综上 可知  device_id      xy_device_token   两个字段的值是直接访问 xml 文件从键值对取值

继续分析别的字段

uuid 

ApiHeader.uuid = DeviceUtils.getUUID(Utils.getApp());


由上图可知uuid的算法如上  uuid = new uuid(android_id, imei.hashcode() << 20 | imsi.hashcode())


request_id 


request_id的算法也比较简单  request_id = md5(包名 + 毫秒数)

 pinyin 


这里检测了是否被调试 如果被调试 pinyin字段的值就是 soyong 这个操作是真的6


这里的 v2_1.flags & 2 == 0 等同于下面的语句

applicationInfo.flags & ApplicationInfo.FLAG_DEBUGGABLE == 0

所以是在检测当前是否处于调试状态 

如果未处于调试状态  会调用另一个sdk的方法 获取返回值

com.leon.channel.helper.ChannelReaderUtil::getChannel(context)

_sign 

_sign的值是由 ApiHeader.paramsSign 方法决定的


分析方法后可知 _sign字段的由来_sign = md5 ( getSign() + "&" + login_data)

这里的login_data是之前拼凑的所有字段

查看getSign()调用层级






可以看到  getsign的参数就是 createsign的返回值  且createsign()并无参数





createsign 并没有参数

以下是getsign源码 手动更改了一些变量名字提高可读性


核心代码在这里

uid 


UserInfo::getuid();
  

这里的uid要么是0  要么就是数据成员uid  userinfo是一个数据类 很多地方都可以调用setuid直接设置uid 





跟随过去 一般是从 json里面获取获取key对应的value 

lver 

可以看到这个lver  参数是调用api获取了版本  

cityId 


仔细跟随 获取城市id的位置


发现又是跟之前一样的套路


cityId 这个字段是 用 SharedPreferences 基于xml来获取数据

//未出现成员
xy_token                 登陆数据包未出现不理会
lat                      登陆数据包未出现不理会
lng                      登陆数据包未出现不理会

//固定值和不依赖自定义对象的成员
sys                       "2"
_time                    System.currentTimeMillis() / 1000
app_id                   对比包名给出    


//成员被AppPreferencesHelper方法返回值赋值
device_id               // SharedPreferences 基于xml来获取数据 
xy_device_token         // SharedPreferences 基于xml来获取数据 

//成员被ApiHeader 方法返回值赋值
uuid          			//uuid = new uuid(android_id, imei.hashcode() << 20 | imsi.hashcode()) 
pinyin        			//被调试:soyong 不被调试调用第三方sdk
request_id    			//request_id = md5(包名 + 毫秒数) 
_sign         			//_sign = md5 ( getSign() + "&" + login_data) getSign()为native
 
//以来其他自定义对象的成员                                            
uid                             //0 or 从json获取对应的值                            
lver          			//调用api获取了版本   		
cityId      			// SharedPreferences 基于xml来获取数据 

大部分的字段在这里完成分析 文章待润色 暂时告一段落 

2019.09.17  写于深圳

2019.10.30 发布于深圳

[公告][征集寄语] 看雪20周年年会 | 感恩有你,一路同行


文章来源: https://bbs.pediy.com/thread-255287.htm
如有侵权请联系:admin#unsafe.sh