D2
Администратор
- Регистрация
- 19 Фев 2025
- Сообщения
- 4,380
- Реакции
- 0
Автор: tenfield
Эксклюзивно для форума: xss.is
Введение
Привет любителям Golang. Привет любителям Python. Продолжим разговоры об обфускации бинарников Golang. Сегодня у меня для вас классы для обфускации имен пакетов, файлов, функций.
Содержание
- Go. Демо проект
- Схема работы обфускатора
- Пишем обфускатор на python
- Обфускатор имен файлов
- Обфускатор имен пакетов
- Обфускатор функций
- Запуск обфускатора
- Сборка проекта и повторный анализ бинарника
- Выводы
Go. Демо проект
Создадим папку project. Далее работать будем только в этой директории. Создадим файл main.go в директории с демо проектом.
Точка входа, функция main, вызывает функцию SolveUltimateQuestionOfLife из пакета xss.
Функция SolveUltimateQuestionOfLife просто ожидает несколько секунд и возвращает строку или сообщение.
Код с оформлением (BB-коды): Скопировать в буфер обмена
Код с оформлением (BB-коды): Скопировать в буфер обмена
И для сборки проекта необходимо создать файл go.mod. Это файл указывающий минимальную версию golang и зависимости проекта.
Код с оформлением (BB-коды): Скопировать в буфер обмена
Стандартная сборка проекта: "go build . -o main"
И запуск: "./main"
Вывод: "Solve 12345678"
Посмотрим что прячется внутри бинарника
Вот это сегодня мы будем скрывать.
Схема работы обфускатора
Как и ранее со строками, сделаем этап перед сборкой, скрывающий артефакты, сейчас это имена функций, пакеты и имена файлов
python3 pre_build.py go-project/ -> go build -> bin.exe без артефактов
Пишем обфускатор на python
Как писал DildoFagins в статье /threads/106900/, удобно организовывать обфускаторы с помощью классов. Полностью согласен.
Далее последовательно разберем несколько однотипных обфускаторов. Структура кода очень похожа и обфускаторы расположены от простого к более сложному.
Создадим класс Obfuscator принимающий путь до Go проекта.
В конструкторе проверяем что указанная директория существует и запускаем обработку (функция processing).
Обфускатор имен файлов
Python: Скопировать в буфер обмена
Функция processing
Функция рекурсивно обходит проект и, для каждого файла с расширением .go, генерирует новое имя.
Затем пытается переименовать файл, либо выводит сообщение об ошибке.
В отличие от обфускации пакетов (рассмотрим далее), здесь новые имена файлов не запоминаются.
Обфускатор имен пакетов
Python: Скопировать в буфер обмена
Функция processing
Для обфускации имен пакетов недостаточно заменить имена папок как мы делали это ранее с файлами.
Замена потребуется в месте объявления пакета, и в месте использования пакета.
Перед началом изменений, необходимо подготовить план изменений. План удобно хранить в dict, в формате {"старое_имя_пакета": "новое_имя_пакета"}.
После этого, по воле бога и согласно плану, переименовываем директории.
Далее необходимо в каждом .go файле из проекта заменить импорты, объявления, вызовы. Проще объяснить это на примере:
Пусть план в нашем примере содержит {'xss':'damagelab'}.
Так как пакет изменился, необходимо заменить объявление пакета (в начале файла, перед секцией импортов).
Код с оформлением (BB-коды): Скопировать в буфер обмена
Заменяем на
Код с оформлением (BB-коды): Скопировать в буфер обмена
Во всех файлах проекта, где содержится импорт из текущего пакета, необходимо обновить имя пакета.
Код с оформлением (BB-коды): Скопировать в буфер обмена
Заменяем на
Код с оформлением (BB-коды): Скопировать в буфер обмена
Во всех файла проекта, необходимо заменить вызовы функций из текущего проекта. Вызов функции в Go формируется следующим образом: "пакет.Функция(аргументы)". Обновляем пакет.
Код с оформлением (BB-коды): Скопировать в буфер обмена
Превращаем в
Код с оформлением (BB-коды): Скопировать в буфер обмена
И повторить каждый раз в каждом файле из проекта.
Обфускатор функций
Python: Скопировать в буфер обмена
Функция processing
Продолжаем усложнять. Как и ранее с именами пакетов, замена потребуется в месте объявления функции, и в месте вызова функции.
Поэтому замену производим в 2 этапа.
Сначала вызываем функцию self.parse_funcs и собираем все объявления функций.
Затем рекурсивно обходим файлы проекта и в каждом go файле заменяем вызовы функций на новое имя функции.
При генерации имен, как водится, есть одна тонкость. В Golang из пакета можно вызвать только "глобальные" функции. Функция глобальная, если начинается с заглавной буквы.
"Локальные" функции могут использоваться только в пакете, где были объявлены. Если это правило нарушено, Golang выдаст ошибку во время компиляции.
Во время генерации новых имен функций необходимо сохранить область видимости функции. Для этого достаточно определить тип функции и добавить в начало имени функции заглавный или строчный символ. От этого длинна имени функции становится size+1.
Запуск обфускатора
Соберем все обфускаторы вместе. Создаем переменную с путем до нашего проекта и поочередно запускаем обфускатор имен файлов, имен пакетов, имен функций.
Python: Скопировать в буфер обмена
Внимание! Перед запуском pre_build.py следует копировать проект во временную папку. Иначе потеряете исходники!
Запустим скрипт: python3 main.py
Посмотрим на новое содержимое исходников.
Код с оформлением (BB-коды): Скопировать в буфер обмена
Код с оформлением (BB-коды): Скопировать в буфер обмена
В исходнике теперь рандомные строки вместо имен пакетов, рандомные функции, рандомные файлы.
Есть что-то особое и приятное в работе обфускаторов. Напоминает карикатурно сложные машины Голдберга https://ru.wikipedia.org/wiki/Машина_Голдберга
Остались последние шаги: запустить сборку проекта и анализ.
Сборка проекта и повторный анализ бинарника
Собираем проект: "go build -o main ."
Запускаем: "./main"
И получаем вывод как до модификации проекта: "Solve 12345678"
Расчехляем strings и ищем артефакты. В этот раз наблюдаем большое количество случайных строк. Но настоящие артефакты больше не ищутся.
В бинарном файле добавилось больше случайных данных. Это действие должно отразиться на параметре энтропии. https://ru.wikipedia.org/wiki/Информационная_энтропия.
У меня получились следующие результаты вычисления энтропии по функции Шеннона.
Энтропия "чистого" бинарника: 6.013176
Энтропия после предварительной обфускации: 6.013218
Отличия в 4 знаке после точки. Размер бинарного файла скрывает наши изменения и они не оказывают влияния.
Выводы
Вот так за 2 статьи мы прикрыли чувствительную информацию в бинарниках от слишком любопытных глаз.
Я бы назвал 4 обфускатора (строки, файлы, пакеты, функции) джентльменским набором для Golang. В бинарнике есть другие артефакты, но в тот момент для меня они были не актуальны.
Обфускатор строк можно написать без использования маркеров. Это сильно упрощает процесс и делает код более читаемым.
Обфускатор должен работать для любого golang кода. Если возникнет ситуация, которую не способен решить обфускатор, то вы получите ошибку/исключение.
Для полного набора не хватает генератора мусора, но об этом в следующий раз
Эксклюзивно для форума: 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"
Посмотрим что прячется внутри бинарника
Вот это сегодня мы будем скрывать.
Схема работы обфускатора
Как и ранее со строками, сделаем этап перед сборкой, скрывающий артефакты, сейчас это имена функций, пакеты и имена файлов
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"
}
В исходнике теперь рандомные строки вместо имен пакетов, рандомные функции, рандомные файлы.
Есть что-то особое и приятное в работе обфускаторов. Напоминает карикатурно сложные машины Голдберга https://ru.wikipedia.org/wiki/Машина_Голдберга
Остались последние шаги: запустить сборку проекта и анализ.
Сборка проекта и повторный анализ бинарника
Собираем проект: "go build -o main ."
Запускаем: "./main"
И получаем вывод как до модификации проекта: "Solve 12345678"
Расчехляем strings и ищем артефакты. В этот раз наблюдаем большое количество случайных строк. Но настоящие артефакты больше не ищутся.
В бинарном файле добавилось больше случайных данных. Это действие должно отразиться на параметре энтропии. https://ru.wikipedia.org/wiki/Информационная_энтропия.
У меня получились следующие результаты вычисления энтропии по функции Шеннона.
Энтропия "чистого" бинарника: 6.013176
Энтропия после предварительной обфускации: 6.013218
Отличия в 4 знаке после точки. Размер бинарного файла скрывает наши изменения и они не оказывают влияния.
Выводы
Вот так за 2 статьи мы прикрыли чувствительную информацию в бинарниках от слишком любопытных глаз.
Я бы назвал 4 обфускатора (строки, файлы, пакеты, функции) джентльменским набором для Golang. В бинарнике есть другие артефакты, но в тот момент для меня они были не актуальны.
Обфускатор строк можно написать без использования маркеров. Это сильно упрощает процесс и делает код более читаемым.
Обфускатор должен работать для любого golang кода. Если возникнет ситуация, которую не способен решить обфускатор, то вы получите ошибку/исключение.
Для полного набора не хватает генератора мусора, но об этом в следующий раз