пятница, 28 марта 2014 г.

Анализируем исходный код 30 apk с помощью PMD

Как — то раз, бродя по просторам интернета, я наткнулся на PMD. PMD служит для поиска «общих недостатков» кода, таких как пустые блоки, создание объекта, который никогда не используется, и так далее. Кроме того, в составе PMD присутствует CPD(copy-paste-detector), который может находить дублирование кода. PMD полностью поддерживает такие языки как Java, JavaScript, XML, XSL, а CPD может работать и с более длинным списком технологий: Java, C, C + +, C #, PHP, Ruby, Fortran, JavaScript.

Стало интересно, а сколько же таких «общих недостатков» можно найти скажем в 30 приложениях Android, которые спокойно лежат себе на 4pda? 


1. Необходимо преобразовать .apk в .jar.

Для этого нужно использовать всем известную dex2jar. Использовать ее очень просто: распаковываем, да пишем примерно следующее: 

./dex2jar.sh имя_вашего_apk.apk.

После процесса преобразования в вашей папке вы найдете файл — «имя_вашего_apk_dex2jar.jar». В этом архиве уже лежат классы, необходимые для последующего анализа, но в не читабельном виде, так сказать. Для того, чтобы мы могли как следует покопаться в нормальных классах, нужно их декомпилировать. 


2. Преобразовываем код из бинарников в читаемый вид.


Из всех проверенных мной преобразователей самым удачным выглядит fernflower. Чтобы воспользоваться им, нужно выполнить команду: 

java -jar fernflower.jar имя_вашего_apk_dex2jar.jar имя_директории

Создастся папка с названием «имя_директории», в которой будет лежать файл с таким же именем, как и исходный. Но в этом случае код можно анализировать.


3. Распаковка архива.


Jar и apk файлы являются архивами, так что их можно запросто распаковать с помощью утилиты unzip.

unzip имя_файла -d имя_директории

4. Натравливаем PMD.


Для анализа исходного кода PMD использует определенный набор правил. Весь список можно глянуть вот тут: правила. Но мы будем использовать всего два набора: android и basic. Для анализа нужно прописать следующую строчку:

./run.sh pmd -d + имя_директории_с_исходниками -f text -R rulesets/java/basic.xml,rulesets/java/android.xml -version 1.7 -language java

В общем — то все, осталось лишь объединить все шаги в единый скрипт и подвести статистику.


5. Пишем простой скрипт на python. 


Сначала обговорим структуру проекта. В корне пусть будут папки: apks(куда мы положим 30 наших произвольных apk'ашек), dex2jar(где будет лежать распакованная одноименная программа), pmd(куда распакуем архив с pmd), наш сприпт. 



import commands
import os
import fnmatch
import matplotlib.pyplot as plt
import collections


abs_path_dir = os.getcwd()
read_jars_folder_name = abs_path_dir + "/apks/read_jars"
text_file_errors = open('all_errors', 'w')


def ensure_dir(f):
    d = os.path.dirname(f)
    if not os.path.exists(d):
        os.makedirs(d)


def covert_to_jar(name_apk):
    os.system(abs_path_dir + "/dex2jar/dex2jar.sh " +
              os.getcwd() + "/apks/" + name_apk)


def conver_to_read_jar(name_file, name_folder):
    full_dir_name = abs_path_dir + "/apks/" + name_file
    path = "java -jar " + abs_path_dir + "/projects/fernflower.jar " + \
        full_dir_name + " " + name_folder
    os.system(path)


def get_all_files_with_pattern(pattern, folder):
    files = []
    for f in os.listdir(folder):
        if fnmatch.fnmatch(f, pattern):
            files.append(f)
    return files


def convert_all_apk_to_jar():
    for f in get_all_files_with_pattern("*.apk", abs_path_dir + "/apks/"):
        covert_to_jar(f)


def convert_all_jar_to_read_jar():
    ensure_dir(read_jars_folder_name)
    print get_all_files_with_pattern("*.jar", abs_path_dir + "/apks/")
    for f in get_all_files_with_pattern("*.jar", abs_path_dir + "/apks/"):
        conver_to_read_jar(f, read_jars_folder_name)


def unpack_all_jars_to_projects():
    files = get_all_files_with_pattern("*.jar", read_jars_folder_name)
    for f in files:
        path = "unzip " + read_jars_folder_name + "/" + \
            f + " -d " + abs_path_dir + "/projects/" + f[:-4]
        os.system(path)


def global_convert_to_projects():
    convert_all_apk_to_jar()
    convert_all_jar_to_read_jar()
    unpack_all_jars_to_projects()
    analysis_all_projects()


def project_analysis(folder):
    path_to_pmd = abs_path_dir + "/pmd/bin/"
    command = path_to_pmd + "run.sh pmd -d " + folder + \
        " -f text -R rulesets/java/basic.xml,rulesets/java/android.xml -version 1.7 -language java"
    status, output = commands.getstatusoutput(command)
    text_file_errors.write(output)


def analysis_all_projects():
    dir_name_proj = abs_path_dir + "/projects/"
    for folder in os.listdir(dir_name_proj):
        project_analysis(dir_name_proj + folder)
    text_file_errors.close()


def get_end_words(x, i):
    answer = ""
    for i in xrange(int(i), len(x)):
        answer += x[int(i)] + " "
    return answer


def delete_small_values(d):
    b = {}
    for k, v in d.iteritems():
        if v > 2:
            b[k] = v
    return b


def get_str_dict(d):
    s = ""
    for k, v in d.iteritems():
        s += str(k) + " - " + str(v) + "\n"
    return s


def draw_graph(d):
    d = delete_small_values(d)
    d = collections.OrderedDict(sorted(d.items()))
    text_statistic = open("statistic.txt", "w")
    text_statistic.write(get_str_dict(d))
    text_statistic.close()
    plt.bar(range(len(d)), d.values(), align='center')
    plt.xticks(range(len(d)), d.keys())
    plt.show()


def get_all_errors():
    strings = open("all_errors", "r+")
    warnings = {}
    for string in strings.readlines():
        x = string.split()
        key = ""
        if len(x[1]) > 2:
            key = get_end_words(x, 1)
        else:
            key = get_end_words(x, 2)

        if key in warnings:
            warnings[key] += 1
        else:
            warnings[key] = 1
    # print warnings
    return warnings


def main():
    global_convert_to_projects()
    text_file_errors.close()
    draw_graph(get_all_errors())


if __name__ == '__main__':
    main()



После работы скрипта, мы получим как статистику в текстовом виде, так и диаграмму(диаграмма строится при помощи самого популярного средства под Python — matplotlib). 


6. Что же все — таки получилось? 


А вышло примерно следующее: 


Avoid empty synchronized blocks — 2099

Avoid instantiating Boolean objects; reference Boolean.TRUE or Boolean.FALSE or call Boolean.valueOf() instead. — 269
Avoid using a branching statement as the last in a loop. — 170
Dont create instances of already existing BigInteger and BigDecimal (ZERO, ONE, TEN) — 10
Empty initializer was found — 41
Empty static initializer was found — 41
Ensure you override both equals() and hashCode() — 149
Invoke equals() on the object you've already ensured is not null — 9
Overriding method merely calls super — 310
The null check here is misplaced; if the variable is null there will be a NullPointerException — 10
These nested if statements could be combined — 22
Unnecessary final modifier in final class — 7501
Useless parentheses. — 3582
An empty statement (semicolon) not part of a loop — 4144
Do not hard code the IP address — 101
Do not hardcode /sdcard. — 11
Do not use if statements that are always true or always false — 21
An operation on an Immutable object (String, BigDecimal or BigInteger) wont change the object itself — 4
super should be called at the end of the method — 72
super should be called at the start of the method — 86

image


Выводы можете делать сами. Мой же вывод такой:

1. PMD достоин того, чтобы использовать его повсеместно, так как таким образом можно избежать не только дублирование кода и создание неиспользуемых переменных, но и выявить более серьезные ошибки. 
2. Прогоняйте ваш код через обфускатор. Желательно всегда.

Ссылки:

PMD: pmd

dex2jar: dex2jar