某打卡软件的定位地址修改,实现任何地点打卡

查看 175|回复 11
作者:bravin   
本内容相对初级,包含
1,逻辑代码定位
2,反资源混淆
3,smali生成和修改
一 反资源混淆
公司的打卡软件,某力,并不是钉钉


首页.png (108.7 KB, 下载次数: 1)
下载附件
2022-11-9 10:10 上传

进入首页后有当前时间和当前的位置信息
首先把apk包拖到jadx里发现代码和资源都被混淆了


jadx目录.png (22.18 KB, 下载次数: 1)
下载附件
2022-11-9 10:24 上传

使用apktool重新打包,发现大量报错,如下
W: D:\apktool\deliwithres\res\layout\a0.xml:6: error: No resource identifier found for attribute 'lottie_autoPlay' in package 'com.delicloud.app.smartoffice'
W:
W: D:\apktool\deliwithres\res\layout\a0.xml:6: error: No resource identifier found for attribute 'lottie_loop' in package 'com.delicloud.app.smartoffice'
W:
W: D:\apktool\deliwithres\res\layout\a2.xml:5: error: No resource identifier found for attribute 'layout_constraintLeft_toLeftOf' in package 'com.delicloud.app.smartoffice'
W:
W: D:\apktool\deliwithres\res\layout\a2.xml:5: error: No resource identifier found for attribute 'layout_constraintTop_toBottomOf' in package 'com.delicloud.app.smartoffice'
W:
W: D:\apktool\deliwithres\res\layout\a2.xml:6: error: No resource identifier found for attribute 'layout_constraintLeft_toLeftOf' in package 'com.delicloud.app.smartoffice'
W:
W: D:\apktool\deliwithres\res\layout\a2.xml:6: error: No resource identifier found for attribute 'layout_constraintTop_toBottomOf' in package 'com.delicloud.app.smartoffice'
W:
W: D:\apktool\deliwithres\res\layout\a2.xml:7: error: No resource identifier found for attribute 'layout_constraintTop_toBottomOf' in package 'com.delicloud.app.smartoffice'
这个问题的解决可以参考本站另一个帖子
这是因为属性名被混淆了,但是资源名的混淆关系是保存在了public.xml文件中的,并且资源的id值是不变的,所以,我们只需要根据id的值把xml文件中的属性名替换成混淆后的名称就可以了
例如layout_constraintStart_toStartOf这个属性
可以在attr里找到属性对应的值


属性查找.png (146.95 KB, 下载次数: 0)
下载附件
2022-11-9 10:40 上传

然后在public里找到属性对应的混淆后的名字fd


属性.png (16.08 KB, 下载次数: 1)
下载附件
2022-11-9 10:41 上传

写个java代码把资源文件里的别名全部替换掉
[Java] 纯文本查看 复制代码public class Main {
    static Random random = new Random();
    static Pattern p = Pattern.compile("\"(.*?)\"");
    static Pattern p1 = Pattern.compile("D:(.*?)xml");
    static Pattern p2 = Pattern.compile("'(.*?)'");
    static Matcher m;
    static Matcher m1;
    static Matcher m2;
    public static String fix(String a){
        return a.substring(1, a.length() - 1);
    }
    public static void handleAttr(String filename) {
        try {
            FileInputStream in = new FileInputStream(filename);
            InputStreamReader inReader = new InputStreamReader(in, StandardCharsets.UTF_8);
            BufferedReader bufReader = new BufferedReader(inReader);
            String line = null;
            while ((line = bufReader.readLine()) != null) {
                line = line.trim();
                if (line.startsWith("public static final int ")) {
                    line = line.replace("public static final int ", "");
                    line = line.replace(";", "");
                    String[] temp = line.split(" = ");
                    if (attrNameIdMap.containsKey(temp[0])) {
                        throw new IllegalStateException("重复KEY");
                    } else {
                        attrNameIdMap.put(temp[0], temp[1]);
                    }
                }
            }
            bufReader.close();
            inReader.close();
            in.close();
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("读取" + filename + "出错!");
        }
    }
    public static void handleErrorLog(String filename) {
        String lastFileName = null;
        String fileContent = null;
        try {
            FileInputStream in = new FileInputStream(filename);
            InputStreamReader inReader = new InputStreamReader(in, StandardCharsets.UTF_8);
            BufferedReader bufReader = new BufferedReader(inReader);
            String line = null;
            while ((line = bufReader.readLine()) != null) {
                line = line.trim();
                if (line.length() > 10) {
//                    System.out.println(line);
                    String address = null;
                    String attribute = null;
                    String packageName = null;
                    m1 = p1.matcher(line);
                    m2 = p2.matcher(line);
                    while (m1.find()) {
                        address = m1.group();
                    }
                    while (m2.find()) {
                        if (attribute == null) {
                            attribute = fix(m2.group());
                        } else if (packageName == null) {
                            packageName = fix(m2.group());
                        }
                    }
                    if (!address.equals(lastFileName)) {
                        if (lastFileName != null) {
                            // 保存文件
                            FileIOUtils.writeFileFromString(lastFileName, fileContent, false);
                        }
                        fileContent = FileIOUtils.readFile2String(address);
                    }
                    lastFileName = address;
                    // 获取别名
                    String id = attrNameIdMap.get(attribute);
                    if (id == null) {
                        System.out.println("未找到数据" + attribute);
                        throw new IllegalStateException("未找到数据");
                    } else {
                        String newName = publicIdNameMap.get(id);
                        fileContent = fileContent.replaceAll(":" + attribute + "=", ":" + newName + "=");
                        System.out.println("attribute: " + attribute + " newName:" + newName + " id:" + id);
                    }
//                    System.out.println("address: " + address + " attribute:" + attribute + " packageName:" + packageName);
                }
            }
            FileIOUtils.writeFileFromString(lastFileName, fileContent, false);
            bufReader.close();
            inReader.close();
            in.close();
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("读取" + filename + "出错!");
        }
    }
    public static void handlePublicXML(String filename) {
        try {
            FileInputStream in = new FileInputStream(filename);
            InputStreamReader inReader = new InputStreamReader(in, "UTF-8");
            BufferedReader bufReader = new BufferedReader(inReader);
            String line = null;
            while ((line = bufReader.readLine()) != null) {
                line = line.trim();
//                System.out.println("第" + i + "行:" + line);
                if (line.startsWith(" publicIdNameMap = new HashMap();
    private static Map attrNameIdMap = new HashMap();
    public static void handle() {
        try {
            List files = FileUtils.listFilesInDir("D:\\apktool\\deliwithres\\smali_classes2\\com\\delicloud\\app\\jsbridge\\ui\\fragment");
            for (File file : files) {
                String content = FileIOUtils.readFile2String(file);
                if (content.contains(".method public a(Lcom/delicloud/app/jsbridge/entity/request/LocationGetRequest;Lcom/d")) {
                    System.out.println(file.getPath());
                    break;
                }
            }
        }catch (Exception e) {
        }
    }
    public static void main(String[] args) {
        // write your code here
        handlePublicXML("D:\\apktool\\deliwithres\\res\\values\\public.xml");
        handleAttr("D:\\decompiler\\attr.txt");
        handleErrorLog("D:\\decompiler\\errorlog.txt");
    }
}
再用apktool打包,发现可以成功打包
二 逻辑定位和修改
使用adb shell ps指令找到app的pid


pid.png (2.69 KB, 下载次数: 0)
下载附件
2022-11-9 10:51 上传

当app在定位页的时候,在cmd命令行里使用adb shell dumpsys activity top > D:\apktool\activityTop.txt命令拉取顶部activity信息
然后在文件中搜索刚才获得的pid号,能够直接定位到目标app的activity名称和当前activity的View结构


activity.png (15.65 KB, 下载次数: 1)
下载附件
2022-11-9 10:56 上传



view.png (123.27 KB, 下载次数: 0)
下载附件
2022-11-9 10:56 上传

在代码里查看activity的java代码,能够找到页面的布局文件名称R.layout.activity_smart_office_access
在jadx里查看布局文件,发现布局文件很简单,只是一个容器而已


content.png (26.85 KB, 下载次数: 0)
下载附件
2022-11-9 11:02 上传

在activity里能够看到5个fragment变量,从名称上可以猜测是和首页的5个tab对应起来的,所以定位的逻辑大概率在第一个fragment里,既HomePageFragment里
在jadx里查看lib里的so文件,凭经验可以知道app用的是高德地图sdk,直接在HomePageFragment暴力搜索Location,发现了高德地图的定位回调


location.png (39.64 KB, 下载次数: 1)
下载附件
2022-11-9 11:13 上传

当时冒出来一个思路,在百度地图开放平台里查到公司的经纬度,回调里的经纬度改成公司的经纬度是不是就可以了,先按照这个思路操作
找到HomePageFragment文件的smali文件,定位到方法,把经纬度改掉


jwd.png (18.89 KB, 下载次数: 0)
下载附件
2022-11-9 11:18 上传

打包签名安装,发现首页提示无法定位,动态调试smali,发现代码是执行了,经纬度确实被修改了,但是为啥还是不行呢,猜测可能有其它地方处理了定位逻辑,另外首页是展示了位置地址的,但是高德地图回调方法里只是对经纬度做了处理,并没有涉及到地址的处理,所以这也能印证,肯定有其它地方处理地址,那就继续找吧
回到HomePageFragment代码,然后可以发现HomePageFragment里嵌套着HomeWebViewContainerFragment,而HomeWebViewContainerFragment里逻辑简单,嵌套着SDKWebViewFragment,到这里能够发现,打卡的业务逻辑是放在WebView里的,但是webView的定位还是要通过jsBridge调用app的原生方法处理的。SDKWebViewFragment里有大量业务代码,直接搜索Location,能够找到一个方法
[Java] 纯文本查看 复制代码@Override // com.delicloud.app.jsbridge.b
    public BaseSDKResult a(LocationGetRequest locationGetRequest, com.delicloud.app.jsbridge.main.c cVar) {
        if (h.cn(this.mContentActivity)) {
            if (l.j(this)) {
                AddressModel addressModel = (AddressModel) dm.a.am(this.mContentActivity, com.delicloud.app.commom.b.abt);
                if (locationGetRequest.isCache() && addressModel != null && System.currentTimeMillis() - addressModel.getCache_time() () { // from class: com.delicloud.app.jsbridge.ui.fragment.SDKWebViewFragment.6
                    @Override // jf.ae
                    public void a(ad adVar) throws Exception {
                        if (SDKWebViewFragment.this.alD == null) {
                            return;
                        }
                        SDKWebViewFragment.this.alD.a(new a.InterfaceC0142a() { // from class: com.delicloud.app.jsbridge.ui.fragment.SDKWebViewFragment.6.1
                            @Override // com.delicloud.app.tools.utils.a.InterfaceC0142a
                            public void a(double d2, double d3, String str, String str2, String str3, String str4) {
                                zArr[0] = false;
                                LocationGetResult locationGetResult2 = new LocationGetResult();
                                locationGetResult2.setData(new LocationGetResult.LocationGetData(Double.valueOf(d2), Double.valueOf(d3), str, str2));
                                Log.i(SocializeConstants.KEY_LOCATION, com.delicloud.app.http.utils.c.ai(locationGetResult2));
                                SDKWebViewFragment.this.a(com.delicloud.app.jsbridge.b.aHj, locationGetResult2);
                            }
                            @Override // com.delicloud.app.tools.utils.a.InterfaceC0142a
                            public void tx() {
                                zArr[0] = false;
                                SDKWebViewFragment.this.a(com.delicloud.app.jsbridge.b.aHj, new BaseSDKResult(JsSDKResultCode.GET_LOCATION_RESULT_FAIL));
                            }
                        });
                    }
                }).timeout(60L, TimeUnit.SECONDS).subscribeOn(kh.b.adP()).observeOn(jh.a.Zq()).subscribe(new ai() { // from class: com.delicloud.app.jsbridge.ui.fragment.SDKWebViewFragment.5
                    @Override // jf.ai
                    public void onComplete() {
                    }
                    @Override // jf.ai
                    public void onSubscribe(jj.c cVar2) {
                    }
                    @Override // jf.ai
                    /* renamed from: d */
                    public void onNext(Long l2) {
                        if (zArr[0]) {
                            SDKWebViewFragment.this.a(com.delicloud.app.jsbridge.b.aHj, new BaseSDKResult(JsSDKResultCode.GET_LOCATION_RESULT_FAIL));
                        }
                    }
                    @Override // jf.ai
                    public void onError(Throwable th) {
                        if (zArr[0]) {
                            SDKWebViewFragment.this.a(com.delicloud.app.jsbridge.b.aHj, new BaseSDKResult(JsSDKResultCode.GET_LOCATION_RESULT_FAIL));
                        }
                    }
                });
            }
            return null;
        }
        com.delicloud.app.uikit.utils.d.a(this.mContentActivity);
        return new BaseSDKResult(JsSDKResultCode.IBEACON_NEED_LOCATION_PERMISSION);
    }
这个方法里,获取了一个AddressModel对象,然后检查了这个对象的缓存有效性,如果有效就使用这个对象,否则使用jsbridge调用原生方法,并且这个而的AddressModel使用了name和address对象,而app首页是有地址信息的,所以我们猜测这里可能是真正获取地址的方法
找到这个方法后,修改起来就不难了,思路是重写AddressModel类,让类生成时就包含公司地址信息并且不允许修改。
使用Android studio 中新建一个工程,创建AddressModel对象,把set方法中更改属性的代码全部去掉
[Java] 纯文本查看 复制代码import java.io.Serializable;
public class AddressModel implements Serializable {
    private String address = "公司的地址";
    private long cache_time = System.currentTimeMillis();
    private Double latitude = 0D;// 公司的纬度
    private Double longitude = 0D;// 公司的精度
    private String name = "公司";
    public AddressModel(long j2, Double d2, Double d3, String str, String str2) {
    }
    public AddressModel() {
    }
    public long getCache_time() {
        return this.cache_time;
    }
    public void setCache_time(long j2) {
    }
    public Double getLatitude() {
        return this.latitude;
    }
    public void setLatitude(Double d2) {
    }
    public Double getLongitude() {
        return this.longitude;
    }
    public void setLongitude(Double d2) {
    }
    public String getName() {
        return this.name;
    }
    public void setName(String str) {
    }
    public String getAddress() {
        return this.address;
    }
    public void setAddress(String str) {
    }
}
使用java2smali插件,把AddressModel类编译成smali文件,使用这个smali文件中的init方法和set get方法替换反编译出来的AddressModel对应的smali文件里的方法
然后再创建一个类
[Java] 纯文本查看 复制代码public class AAA {
   
    void a(){
        AddressModel addressModel = new AddressModel();
        System.out.println(addressModel.getAddress());
        addressModel = null;
    }
}
再编译出smali文件,里面的方法如下
.method a()V
    .registers 4
    .prologue
    .line 10
    new-instance v0, Lcom/bravin/game/entity/AddressModel;
    invoke-direct {v0}, Lcom/bravin/game/entity/AddressModel;->()V
    .line 11
    .local v0, "addressModel":Lcom/bravin/game/entity/AddressModel;
    sget-object v1, Ljava/lang/System;->out:Ljava/io/PrintStream;
    invoke-virtual {v0}, Lcom/bravin/game/entity/AddressModel;->getAddress()Ljava/lang/String;
    move-result-object v2
    invoke-virtual {v1, v2}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
    .line 12
    const/4 v0, 0x0
    .line 13
    return-void
.end method
找到反编译SDKWebViewFragment的smali文件中的.method public a(Lcom/delicloud/app/jsbridge/entity/request/LocationGetRequest;Lcom/delicloud/app/jsbridge/main/c;)Lcom/delicloud/app/jsbridge/entity/result/BaseSDKResult;方法
按照AAA类里的方法的逻辑,替换掉原有方法里的AddressModel对象生成的逻辑
.line 2207
    iget-object p2, p0, Lcom/delicloud/app/jsbridge/ui/fragment/SDKWebViewFragment;->mContentActivity:Landroidx/appcompat/app/AppCompatActivity;
    const-string v1, "key_address_cache"
        
        new-instance p2, Lcom/delicloud/app/comm/entity/tools/AddressModel;
        
        invoke-direct {p2}, Lcom/delicloud/app/comm/entity/tools/AddressModel;->()V
    .line 2209
    invoke-virtual {p1}, Lcom/delicloud/app/jsbridge/entity/request/LocationGetRequest;->isCache()Z
    move-result p1
    if-nez p1, :cond_0
注意if-nez p1, :cond_0这一行和原先的判断不一样,原先是if-eqz p1, :cond_0
然后打包签名安装,发现app可以获取到地址了,并且地址信息就是我们在AddressModel里填写的信息,找个离公司远的地方打开app,发现还是可以打卡,说明我们的修改成功了
总结
这个app并没有什么复杂的反逆向手段,适合新人练手,在定位要修改的业务逻辑时,要结合页面上的元素去定位,另外android studio的java2smali插件可以大大减少写smali的困难度

下载次数, 方法

bravin
OP
  


lye123456 发表于 2022-11-9 13:56
修改过的app打卡后,公司后台不知道会不会发现的??

这个就不知道了哈
hacker922   


keview 发表于 2022-11-9 14:06
钉钉可用吗,后台会不会报使用了虚拟定位

以前的这种类似修改 钉钉后台是会报的 钉钉用的量太大了,不要想了 就算有,爆出来估计也很快死
24unicorns   

真不错啊,学到了
chinalihao   

很厉害的,感谢分享
jone33   

虽然有用,但不建议啊
刘留留   

太厉害了,感谢分享,学习了
lye123456   

修改过的app打卡后,公司后台不知道会不会发现的??
童年低调   

厉害了我个哥
smnra   

来了来了   ,前排,  厉害
您需要登录后才可以回帖 登录 | 立即注册