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