如何安全地打印日志

如何打印日志?這不是很簡單,直接使用android.util.Log這個類不就行了?然而,日志屬于非常敏感的信息;逆向工程師在逆向你的程序的時候,本來需要捕捉你程序的各種輸出,然后進行推測,順藤摸瓜然后得到需要的信息;一旦你的日志泄漏,無異于門戶洞開,破解你的程序如入無人之境。
安全的概念本來就是相對的,如果破解你程序的代價遠遠大于破解得到的價值,那么就可以認(rèn)為程序是“安全的”;這里就分析一下,為了提高程序的安全性,在打印日志的時候應(yīng)該注意什么。
首先看看絕大部分公司以及開發(fā)者的做法:

日志開關(guān)+日志類

為了在release版本里面沒有日志輸出,一個最簡單的想法是:把所有打印日志的語句放在一個if(DEBUG)的語句里面;在日常開發(fā)的時候,DEBUG開關(guān)打開,發(fā)布正式版本的時候關(guān)閉這個開關(guān)即可,大致思路如下:

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

    // 其他debug方法
}

接下來看一個真實的例子,國外的一個apk,名字叫做powerclean;包名:com.lionmobi.powerclean;我們安裝這個包;發(fā)現(xiàn)很正常,沒有任何日志輸出;然后我們逆向這個apk;隨便翻看幾個類,發(fā)現(xiàn)很多地方有類似日志輸出:


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的用戶還不少;接下來我們看看這種方式有什么問題。

靜態(tài)反編譯打開日志開關(guān)

上面的那種方式有一個問題:雖然在release版本里面,確實沒有日志輸出;但是輸出日志的代碼依然存在,只是沒有執(zhí)行到!(if條件不成立)所以,有沒有辦法讓這些代碼執(zhí)行到呢?簡單來說,就是能不能在release版本里面把這個DEBUG變量弄成true呢?當(dāng)然可以!而且做法還非常簡單。
我們使用apktool反編譯得到這個apk的smali代碼;然后上面的反編譯告訴我們,這個日志類的位置是:com.lionmobi.util.x我們打開這個x.smali文件,內(nèi)容如下:

.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的靜態(tài)變量就是我們的開關(guān), 它的初始化在哪個靜態(tài)代碼塊里面;新建了一個局部變量0x0然后賦值給了a;因此,我們把這個0x0修改為0x1就打開了這個開關(guān)。很簡單吧,接下來我們把修改好的smali打包回去,然后簽名得到一個新的可以運行的apk;運行一下看看結(jié)果。果然,一大堆的日志輸出了出來,你的程序每一步在干什么都自己告訴別人了,都不需要去猜;我就隨便截個圖,感受下:


image

讓release版本里面不包含日志代碼

從上面的分析我們得到一個結(jié)論:如果需要程序是“日志安全的”,那么release版本里面不應(yīng)該存在輸出日志的代碼。
如何做到這一點呢?我們可以做一個工具,開發(fā)的時候,正常打印日志;一旦需要發(fā)布版本,把所有打印日志的語句代碼,全部刪除掉。代碼很簡單,用一些正則表達式就可以做到。
事實上,我們也可以使用一些別的工具,來實現(xiàn)這個類似的功能;那就是proguard;提到這個工具,很多認(rèn)只是覺得他是一個代碼混淆的工具,實際上,它還可以幫你剔除無用代碼!什么樣的代碼是無用代碼呢?

if (true) {
    // statement;
}

類似于這樣,靜態(tài)編譯的時候被認(rèn)為“永遠不會執(zhí)行的代碼”,就被認(rèn)為是無用代碼,會被這個工具直接優(yōu)化掉,生成的class文件里面,這個if語句直接就沒有了。這個功能,完美符合我們的需求;我們只需要把輸出日志的代碼用這樣的if語句包圍起來,然后release的時候肯定會用這個工具混淆;然后,在release版本里面,所有的輸出日志的代碼全部都沒有了!不會像以前一樣,留下一個影子,只是不做事。

正確的做法

最終,我們所有打印日志的語句應(yīng)該如下:

// 必須是static final 也就是常量,這樣才能在編譯器優(yōu)化;刪除if塊
private static final boolean DEBUG = true; 

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

然后,使用proguard優(yōu)化代碼即可。
看起來簡單,好像也與最初的“日志開關(guān)”沒有什么區(qū)別,仔細(xì)分析一下:

日志開關(guān)必須是靜態(tài)常量

對比一下正確的做法與最開始的日志開關(guān),一個是一個靜態(tài)變量,一個是靜態(tài)常量;如果是常量的話,那么就是永遠不變的,那么當(dāng)DEBUG變量為False的時候proguard可以理所當(dāng)然地認(rèn)為,這一部分代碼時絕對不會被執(zhí)行的,這樣,打印日志的語句就會被優(yōu)化(刪除)掉;如果是一個變量,那么在運行期間就有可能改變它的值(private僅僅是對于程序員的改變,對于編譯器以及運行時,沒有什么改不了),這樣proguard就會置之不理,這樣你的日志代碼就暴露出來了,一字之差,失之千里。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • ORA-00001: 違反唯一約束條件 (.) 錯誤說明:當(dāng)在唯一索引所對應(yīng)的列上鍵入重復(fù)值時,會觸發(fā)此異常。 O...
    我想起個好名字閱讀 5,958評論 0 9
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對...
    cosWriter閱讀 11,645評論 1 32
  • “手心手背都是肉,但是手掌肉比手背厚”,我笑著對媽媽說。曾經(jīng)看過這樣一句話,所有的玩笑都是不敢言語的真話。其實我看...
    磚縫的小草閱讀 6,880評論 3 23
  • 坐車回單位的路上,快到站的時候從窗口看到隔壁小黑狗順著環(huán)山路往下溜達,我忽然覺得小黑是真的不會回來了。之前它一直留...
    靈湮涼閱讀 332評論 0 0
  • 前言 之前在平臺發(fā)布過一篇關(guān)于AS導(dǎo)入二次開發(fā)系統(tǒng)包的文章,得到很多開發(fā)者的回饋和討論,有興趣的可以回看AS中導(dǎo)入...
    詭異的葉子閱讀 1,578評論 0 0

友情鏈接更多精彩內(nèi)容