Прячем пакеты, файлы и функции в Golang

D2

Администратор
Регистрация
19 Фев 2025
Сообщения
4,380
Реакции
0
Автор: tenfield
Эксклюзивно для форума: xss.is

Введение
Привет любителям Golang. Привет любителям Python. Продолжим разговоры об обфускации бинарников Golang. Сегодня у меня для вас классы для обфускации имен пакетов, файлов, функций.

Содержание
- Go. Демо проект
- Схема работы обфускатора
- Пишем обфускатор на python
- Обфускатор имен файлов
- Обфускатор имен пакетов
- Обфускатор функций
- Запуск обфускатора
- Сборка проекта и повторный анализ бинарника
- Выводы


Go. Демо проект
Создадим папку project. Далее работать будем только в этой директории. Создадим файл main.go в директории с демо проектом.
Точка входа, функция main, вызывает функцию SolveUltimateQuestionOfLife из пакета xss.
Функция SolveUltimateQuestionOfLife просто ожидает несколько секунд и возвращает строку или сообщение.

Код с оформлением (BB-коды): Скопировать в буфер обмена
Код:
// main.go
package main

import (
    "fmt"
    "main/xss"
)

func main() {
    sol := xss.SolveUltimateQuestionOfLife()
    fmt.Println("Solve", sol)
}

Код с оформлением (BB-коды): Скопировать в буфер обмена
Код:
// xss/main.go
package xss

import (
    "time"
)

func SolveUltimateQuestionOfLife() string {
    time.Sleep(8 * time.Second)
    return "12345678"
}

И для сборки проекта необходимо создать файл go.mod. Это файл указывающий минимальную версию golang и зависимости проекта.
Код с оформлением (BB-коды): Скопировать в буфер обмена
Код:
// go.mod
module test
go 1.23

Стандартная сборка проекта: "go build . -o main"
И запуск: "./main"
Вывод: "Solve 12345678"

Посмотрим что прячется внутри бинарника
art.png


Вот это сегодня мы будем скрывать.


Схема работы обфускатора
Как и ранее со строками, сделаем этап перед сборкой, скрывающий артефакты, сейчас это имена функций, пакеты и имена файлов
python3 pre_build.py go-project/ -> go build -> bin.exe без артефактов

Пишем обфускатор на python
Как писал DildoFagins в статье /threads/106900/, удобно организовывать обфускаторы с помощью классов. Полностью согласен.

Далее последовательно разберем несколько однотипных обфускаторов. Структура кода очень похожа и обфускаторы расположены от простого к более сложному.
Создадим класс Obfuscator принимающий путь до Go проекта.
В конструкторе проверяем что указанная директория существует и запускаем обработку (функция processing).

Обфускатор имен файлов
Python: Скопировать в буфер обмена
Код:
class FileNameObfuscator:
    def __init__(self, src_dir: str, ) -> None:
        if not Path(src_dir).exists():
            raise FileNotFoundError(f"The source path '{src_dir}' does not exist.")
        self.src_dir = src_dir
        self.processing()

    def processing(self):
        for file in Path(self.src_dir).glob("**/*.go"):
            file_new = Path(file).parent.joinpath(f'{random_string(size=randint(5, 10))}.go')

            try:
                Path(file).rename(file_new)
            except Exception as e:
                print(e)

Функция processing
Функция рекурсивно обходит проект и, для каждого файла с расширением .go, генерирует новое имя.
Затем пытается переименовать файл, либо выводит сообщение об ошибке.

В отличие от обфускации пакетов (рассмотрим далее), здесь новые имена файлов не запоминаются.

Обфускатор имен пакетов
Python: Скопировать в буфер обмена
Код:
class PackageObfuscator:
    def __init__(self, src_dir: str, ) -> None:
        if not Path(src_dir).exists():
            raise FileNotFoundError(f"The source path '{src_dir}' does not exist.")
        self.src_dir = src_dir
        self.processing()

    def get_folders(self) -> set[str]:
        for i in Path(self.src_dir).glob("**/*"):
            if i.is_dir():
                yield i.name

    def processing(self):
        # генерируем новые имена пакетам
        packages = {}
        for i in self.get_folders():
            packages.update({
                i: random_string(size=randint(5, 10))
            })
        # переименовываем папки
        for old_folder, new_folder in packages.items():
            new_path = Path(self.src_dir).joinpath(new_folder)
            Path(self.src_dir).joinpath(old_folder).rename(new_path)

        # заменяем импорты и объявления пакетов
        for i in Path(self.src_dir).glob("**/*.go"):
            if not i.is_file():
                continue
            with open(i, 'r') as f:
                content = f.read()
            for old_package, new_package in packages.items():
                # заменяем импорты
                content = content.replace(f'main/{old_package}', f'main/{new_package}')
                # заменяем вызовы функций
                content = content.replace(f'{old_package}.', f'{new_package}.')
                # заменяем название пакета
                content = content.replace(f'package {old_package}', f'package {new_package}')
            with open(i, 'w') as f:
                f.write(content)

Функция processing
Для обфускации имен пакетов недостаточно заменить имена папок как мы делали это ранее с файлами.
Замена потребуется в месте объявления пакета, и в месте использования пакета.
Перед началом изменений, необходимо подготовить план изменений. План удобно хранить в dict, в формате {"старое_имя_пакета": "новое_имя_пакета"}.
После этого, по воле бога и согласно плану, переименовываем директории.
Далее необходимо в каждом .go файле из проекта заменить импорты, объявления, вызовы. Проще объяснить это на примере:

Пусть план в нашем примере содержит {'xss':'damagelab'}.
Так как пакет изменился, необходимо заменить объявление пакета (в начале файла, перед секцией импортов).
Код с оформлением (BB-коды): Скопировать в буфер обмена
package xss
Заменяем на
Код с оформлением (BB-коды): Скопировать в буфер обмена
package damagelab

Во всех файлах проекта, где содержится импорт из текущего пакета, необходимо обновить имя пакета.
Код с оформлением (BB-коды): Скопировать в буфер обмена
Код:
import (
    "fmt"
    "main/xss"
)
Заменяем на
Код с оформлением (BB-коды): Скопировать в буфер обмена
Код:
import (
    "fmt"
    "main/damagelab"
)

Во всех файла проекта, необходимо заменить вызовы функций из текущего проекта. Вызов функции в Go формируется следующим образом: "пакет.Функция(аргументы)". Обновляем пакет.
Код с оформлением (BB-коды): Скопировать в буфер обмена
xss.SolveUltimateQuestionOfLife()
Превращаем в
Код с оформлением (BB-коды): Скопировать в буфер обмена
damagelab.SolveUltimateQuestionOfLife()
И повторить каждый раз в каждом файле из проекта.


Обфускатор функций
Python: Скопировать в буфер обмена
Код:
class FunctionsObfuscator:
    def __init__(self, src_dir: str):
        if not Path(src_dir).exists():
            raise FileNotFoundError(f"The source path '{src_dir}' does not exist.")
        self.src_dir = src_dir
        self.processing()

    def gen_func_name(self, is_exportable=False) -> str:
        new_func_name = random_string(size=randint(5, 10))
        if is_exportable:
            random_char = random_string(size=1, chars='ABCDEFGHIJKLMNOPQRSTUVWXYZ')
            new_func_name = f'{random_char}{new_func_name}'
        else:
            random_char = random_string(size=1, chars='abcdefghijklmnopqrstuvwxyz')
            new_func_name = f'{random_char}{new_func_name}'
        return new_func_name

    def parse_func_name(self, line: str) -> str:
        regex = r"(?:func )([a-zA-Z0-9_-]*)"
        matches = re.findall(pattern=regex, string=line, flags=re.IGNORECASE)
        assert len(matches) == 1
        return matches[0]

    def parse_funcs(self) -> dict[str, str]:
        funcs = dict()
        for i in Path(self.src_dir).glob("**/*.go"):
            with open(i, 'r') as f:
                content = f.read()
                for line in content.split('\n'):
                    if not line.startswith('func '):
                        continue
                    assert len(line.split('func')) == 2
                    func_name = self.parse_func_name(line)
                    assert len(func_name)
                    if func_name in ['main', 'init']:
                        continue
                    if func_name in funcs.keys():
                        raise Exception(f"File {i}\t{func_name=} is not unique")
                    # Если функция начинается с большой буквы
                    # Это экспортируемая функция
                    funcs.update({
                        func_name: self.gen_func_name(func_name[0].isupper())
                    })

    def processing(self):
        funcs = self.parse_funcs()
        for i in Path(self.src_dir).glob("**/*.go"):
            with open(i, 'r') as f:
                content = f.read()
            for old_func, new_func in funcs.items():
                content = content.replace(f'{old_func}(', f'{new_func}(')
                content = content.replace(f'{old_func} (', f'{new_func} (')
            with open(i, 'w') as f:
                f.write(content)

Функция processing
Продолжаем усложнять. Как и ранее с именами пакетов, замена потребуется в месте объявления функции, и в месте вызова функции.
Поэтому замену производим в 2 этапа.
Сначала вызываем функцию self.parse_funcs и собираем все объявления функций.
Затем рекурсивно обходим файлы проекта и в каждом go файле заменяем вызовы функций на новое имя функции.

При генерации имен, как водится, есть одна тонкость. В Golang из пакета можно вызвать только "глобальные" функции. Функция глобальная, если начинается с заглавной буквы.
"Локальные" функции могут использоваться только в пакете, где были объявлены. Если это правило нарушено, Golang выдаст ошибку во время компиляции.
Во время генерации новых имен функций необходимо сохранить область видимости функции. Для этого достаточно определить тип функции и добавить в начало имени функции заглавный или строчный символ. От этого длинна имени функции становится size+1.

Запуск обфускатора
Соберем все обфускаторы вместе. Создаем переменную с путем до нашего проекта и поочередно запускаем обфускатор имен файлов, имен пакетов, имен функций.
Python: Скопировать в буфер обмена
Код:
def main():
    src = "./src"
    FileNameObfuscator(src)
    PackageObfuscator(src)
    FunctionsObfuscator(src)


if __name__ == "__main__":
    main()

Внимание! Перед запуском pre_build.py следует копировать проект во временную папку. Иначе потеряете исходники!
Запустим скрипт: python3 main.py
Посмотрим на новое содержимое исходников.

Код с оформлением (BB-коды): Скопировать в буфер обмена
Код:
// ghzzbo.go
package main

import (
    "fmt"
    "main/ikqrxztkbh"
)

func main() {
    sol := ikqrxztkbh.Gtuezpes()
    fmt.Println("Solve", sol)
}

Код с оформлением (BB-коды): Скопировать в буфер обмена
Код:
// ikqrxztkbh/uzpjz.go
package ikqrxztkbh

import (
    "time"
)

func Gtuezpes() string {
    time.Sleep(8 * time.Second)
    return "12345678"
}

happy.jpeg


В исходнике теперь рандомные строки вместо имен пакетов, рандомные функции, рандомные файлы.
Есть что-то особое и приятное в работе обфускаторов. Напоминает карикатурно сложные машины Голдберга https://ru.wikipedia.org/wiki/Машина_Голдберга
Остались последние шаги: запустить сборку проекта и анализ.

Сборка проекта и повторный анализ бинарника
Собираем проект: "go build -o main ."
Запускаем: "./main"
И получаем вывод как до модификации проекта: "Solve 12345678"

Расчехляем strings и ищем артефакты. В этот раз наблюдаем большое количество случайных строк. Но настоящие артефакты больше не ищутся.
no-art.png



В бинарном файле добавилось больше случайных данных. Это действие должно отразиться на параметре энтропии. https://ru.wikipedia.org/wiki/Информационная_энтропия.
У меня получились следующие результаты вычисления энтропии по функции Шеннона.
Энтропия "чистого" бинарника: 6.013176
Энтропия после предварительной обфускации: 6.013218
Отличия в 4 знаке после точки. Размер бинарного файла скрывает наши изменения и они не оказывают влияния.

Выводы
Вот так за 2 статьи мы прикрыли чувствительную информацию в бинарниках от слишком любопытных глаз.
Я бы назвал 4 обфускатора (строки, файлы, пакеты, функции) джентльменским набором для Golang. В бинарнике есть другие артефакты, но в тот момент для меня они были не актуальны.
Обфускатор строк можно написать без использования маркеров. Это сильно упрощает процесс и делает код более читаемым.
Обфускатор должен работать для любого golang кода. Если возникнет ситуация, которую не способен решить обфускатор, то вы получите ошибку/исключение.

Для полного набора не хватает генератора мусора, но об этом в следующий раз
 
Сверху Снизу