如何安全地打印日志

如何打印日志?这不是很简单,直接使用android.util.Log这个类不就行了?然而,日志属于非常敏感的信息;逆向工程师在逆向你的程序的时候,本来需要捕捉你程序的各种输出,然后进行推测,顺藤摸瓜然后得到需要的信息;一旦你的日志泄漏,无异于门户洞开,破解你的程序如入无人之境。
安全的概念本来就是相对的,如果破解你程序的代价远远大于破解得到的价值,那么就可以认为程序是“安全的”;这里就分析一下,为了提高程序的安全性,在打印日志的时候应该注意什么。
首先看看绝大部分公司以及开发者的做法:

日志开关+日志类

为了在release版本里面没有日志输出,一个最简单的想法是:把所有打印日志的语句放在一个if(DEBUG)的语句里面;在日常开发的时候,DEBUG开关打开,发布正式版本的时候关闭这个开关即可,大致思路如下:

public class LogUtil {
    private static boolean DEBUG = true;// 发布的时候修改为false
    
    public static void d(String tag, String msg) {
        if (DEBUG) android.util.Log.d(TAG, msg);
    }

    // 其他debug方法
}

接下来看一个真实的例子,国外的一个apk,名字叫做powerclean;包名:com.lionmobi.powerclean;我们安装这个包;发现很正常,没有任何日志输出;然后我们逆向这个apk;随便翻看几个类,发现很多地方有类似日志输出:


image

我们打开这个叫做x的类,虽然被混淆过了,但是意思很明白,跟我们上面的思路一样:

package com.lionmobi.util;

import android.util.Log;

public class x {
    private static boolean a;

    static {
        x.a = false;
    }

    public static void d(String arg1, String arg2) {
        if(x.a) {
            Log.d(arg1, arg2);
        }
    }

    public static void e(String arg1, String arg2) {
        if(x.a) {
            Log.e(arg1, arg2);
        }
    }

    public static void i(String arg1, String arg2) {
        if(x.a) {
            Log.i(arg1, arg2);
        }
    }
}

这是一个真实的例子,而且这个app的用户还不少;接下来我们看看这种方式有什么问题。

静态反编译打开日志开关

上面的那种方式有一个问题:虽然在release版本里面,确实没有日志输出;但是输出日志的代码依然存在,只是没有执行到!(if条件不成立)所以,有没有办法让这些代码执行到呢?简单来说,就是能不能在release版本里面把这个DEBUG变量弄成true呢?当然可以!而且做法还非常简单。
我们使用apktool反编译得到这个apk的smali代码;然后上面的反编译告诉我们,这个日志类的位置是:com.lionmobi.util.x我们打开这个x.smali文件,内容如下:

.class public Lcom/lionmobi/util/x;
.super Ljava/lang/Object;


# static fields
.field private static a:Z


# direct methods
.method static constructor <clinit>()V
    .locals 1

    const/4 v0, 0x0 # 修改为0x1 (True)

    sput-boolean v0, Lcom/lionmobi/util/x;->a:Z #初始化位置

    return-void
.end method

.method public static d(Ljava/lang/String;Ljava/lang/String;)V
    .locals 1

    sget-boolean v0, Lcom/lionmobi/util/x;->a:Z

    if-eqz v0, :cond_0

    invoke-static {p0, p1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I

    :cond_0
    return-void
.end method

.method public static e(Ljava/lang/String;Ljava/lang/String;)V
    .locals 1

    sget-boolean v0, Lcom/lionmobi/util/x;->a:Z

    if-eqz v0, :cond_0

    invoke-static {p0, p1}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I

    :cond_0
    return-void
.end method

.method public static i(Ljava/lang/String;Ljava/lang/String;)V
    .locals 1

    sget-boolean v0, Lcom/lionmobi/util/x;->a:Z

    if-eqz v0, :cond_0

    invoke-static {p0, p1}, Landroid/util/Log;->i(Ljava/lang/String;Ljava/lang/String;)I

    :cond_0
    return-void
.end method

很明白,那个叫做a的静态变量就是我们的开关, 它的初始化在哪个静态代码块里面;新建了一个局部变量0x0然后赋值给了a;因此,我们把这个0x0修改为0x1就打开了这个开关。很简单吧,接下来我们把修改好的smali打包回去,然后签名得到一个新的可以运行的apk;运行一下看看结果。果然,一大堆的日志输出了出来,你的程序每一步在干什么都自己告诉别人了,都不需要去猜;我就随便截个图,感受下:


image

让release版本里面不包含日志代码

从上面的分析我们得到一个结论:如果需要程序是“日志安全的”,那么release版本里面不应该存在输出日志的代码。
如何做到这一点呢?我们可以做一个工具,开发的时候,正常打印日志;一旦需要发布版本,把所有打印日志的语句代码,全部删除掉。代码很简单,用一些正则表达式就可以做到。
事实上,我们也可以使用一些别的工具,来实现这个类似的功能;那就是proguard;提到这个工具,很多认只是觉得他是一个代码混淆的工具,实际上,它还可以帮你剔除无用代码!什么样的代码是无用代码呢?

if (true) {
    // statement;
}

类似于这样,静态编译的时候被认为“永远不会执行的代码”,就被认为是无用代码,会被这个工具直接优化掉,生成的class文件里面,这个if语句直接就没有了。这个功能,完美符合我们的需求;我们只需要把输出日志的代码用这样的if语句包围起来,然后release的时候肯定会用这个工具混淆;然后,在release版本里面,所有的输出日志的代码全部都没有了!不会像以前一样,留下一个影子,只是不做事。

正确的做法

最终,我们所有打印日志的语句应该如下:

// 必须是static final 也就是常量,这样才能在编译器优化;删除if块
private static final boolean DEBUG = true; 

if (DEBUG) {
    android.util.Log.d(TAG, "msg to print");
}

然后,使用proguard优化代码即可。
看起来简单,好像也与最初的“日志开关”没有什么区别,仔细分析一下:

日志开关必须是静态常量

对比一下正确的做法与最开始的日志开关,一个是一个静态变量,一个是静态常量;如果是常量的话,那么就是永远不变的,那么当DEBUG变量为False的时候proguard可以理所当然地认为,这一部分代码时绝对不会被执行的,这样,打印日志的语句就会被优化(删除)掉;如果是一个变量,那么在运行期间就有可能改变它的值(private仅仅是对于程序员的改变,对于编译器以及运行时,没有什么改不了),这样proguard就会置之不理,这样你的日志代码就暴露出来了,一字之差,失之千里。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容

  • ORA-00001: 违反唯一约束条件 (.) 错误说明:当在唯一索引所对应的列上键入重复值时,会触发此异常。 O...
    我想起个好名字阅读 4,970评论 0 9
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,067评论 1 32
  • “手心手背都是肉,但是手掌肉比手背厚”,我笑着对妈妈说。曾经看过这样一句话,所有的玩笑都是不敢言语的真话。其实我看...
    砖缝的小草阅读 6,684评论 3 23
  • 坐车回单位的路上,快到站的时候从窗口看到隔壁小黑狗顺着环山路往下溜达,我忽然觉得小黑是真的不会回来了。之前它一直留...
    靈湮凉阅读 283评论 0 0
  • 前言 之前在平台发布过一篇关于AS导入二次开发系统包的文章,得到很多开发者的回馈和讨论,有兴趣的可以回看AS中导入...
    诡异的叶子阅读 1,156评论 0 0