workname: Делаем директории в IDA и автоматизируем это.
# Пишем простой скрипт: Распределение функций C++ по директориям в IDA.
Задача: Есть проект, где в каждом модуле есть куча классов с сопутствующими функциями, которые не организованы. Нужно написать скрипт, который парсит функции и распределяет их по директориям (новая фича IDA 7.7).
Цели:
- Распарсить функции
- Применить демангл
- Организовать директории по классам
Ссылки:
- AlphaGo: [categorize_go_folders.py](https://github.com/SentineLabs/AlphaGolang/blob/main/3.categorize_go_folders.py)
- Док-я IDA (SDKDOC) #1 : [Ссылка](https://hex-rays.com/products/ida/support/sdkdoc/dirtree_8hpp.html)
- Док-я IDA (IDAPYTHONDOC) #2: [Ссылка](https://www.hex-rays.com/products/ida/support/idapython_docs/ida_dirtree.html)
# Заметки
- Функции в демангл-версии выглядят как `ModuleName::FuncName(arg)`
- Функция для получения списка функций в IDB: `idautils.Functions()`
- Функция для получения имени функции из объекта функции: `idc.get_func_name(func)`
- Функция для демангла имени: `string demangle_name(string name, long disable_mask);`
- `ida_dirtree.get_std_dirtree(ida_dirtree.DIRTREE_FUNCS)` - судя по всему корень дерева директорий, по умолчанию функции кладутся туда. (root node/корневой узел-директория)
- dirtree_t.resolve_path(dirname:str) - возвращает direntry_t. из нее можно вытащить .idx - номер директории
- get_entry_name(idx) - наоборот, возвращает только имя директории
- idx = 0 - походу это всегда './'
- root_node.get_dir_size(0) - вернуть размер корня.
# Проблемы
- Функции, сгенерированный по шаблонам. Имеют чуть другой mangling алгоритм. **UPD: не проблема, в имя модуля уходит весь шаблон.**
- Останется ряд функций вида sub_XXXXXXXX, которые, частично, составляют vtable различных модулей. Мб попробовать их как-то научится парсить?
- Не все функции деманглятся. Хз почему, наверное алгоритм в компиляторе изначально чуть другой?
- Некоторые названия модулей образуются с помощью префикса/суффикса. Можно ли как-то мерджить директории?
- Если переименовать функцию, то она по умолчанию останется в той же директории, что и была. TODO: при старте получать список existed директорий и удалять их все. И соответственно снова наполнять с уже обновленными именами.
- апхахпахпахп я не знаю почему но findnext возвращает False раньше чем закончатся файлы в директории. **UPD** Это происходило из-за того что я перемещал файл сразу после того, как находил. Это почему-то ломало курсор кекв.
TODO: Пофиксить ошибку, если в функе нет неймспеса, а в параме есть ( verify(std::string) -> verify(std )
# Фичи для реализации
- Сортировка по префиксу-подстроке
- Сделать Favorite по ПКМ.
- Сделайть отдельный GUI
- Сделать Favorite-Auto по регексу
- Сделать для Rust
- Сделать для Golang
# Алгоритм
- Чтобы увидеть директории: Functions -> ПКМ -> Show Folders
- Получаем список функций при помощи `for func in idautils.Functions():`
- Они будут иметь 'оригинальное' имя вида `AfxThrowMemoryException@@YGXXZ`
- Парсим имя при помощи `demangle_name(str, disable_mask)`
- str - имя функции
- disable_mask - режим по которому раскукоживается название функции. Имеет два режима:
- get_inf_attr(INF_LONG_DN). Раскукожить полностью, т.е получить полную сигнатуру функции. Имеем на выходе названия вида:
- `public: virtual unsigned int __thiscall CFile::Read(void *, unsigned int)`
- get_inf_attr(INF_SHORT_DN). Раскукожить только модуль, имя и аргументы. Имеем на выходе:
- `CFile::Read(void *,uint)`
- В наших целях нам будет достаточно только 'короткого' имени функции. Сплитаем по `::` и получаем имя модуля через [0].
- WARNING: это некорректный алгоритм, так как прегенерированные шаблонные функции имеют чуть другой demangled name. Надо обработать эту ветку.
- По результату демангла функции можно поделить на следующие категории:
- Модульные - те, которые имеют :: в названии (?)
- Сишные - те, которые деманглятся, но имеют обычное сишное имя (без ::)
- ~~У которых произошла ошибка с деманглом и остальные символы (требуют ручного переноса в директории или изменить имя функции вручаную)~~
- Unrecognized - стандартные sub_XXXXXXXX.
- Для работы с директориями функций в иду добавили следующее API:
# Алгоритм Создания/Удаления новых нод
# Алгоритм Обхода нод дерева.
# Алгоритм поиска директорий
Все пути можно получить в динамике. Но почитав документацию можно предположить, что должен быть механизм поиска потомков ноды dirent_t файловой системы
# Готовые скрипты
Простой скрипт поиска и переименования
```python!=
import idautils
import ida_dirtree
import idc
print("="*20)
modules = {}
for fun in idautils.Functions():
name = idc.get_func_name(fun)
name_dmngl_long = demangle_name(name, get_inf_attr(INF_LONG_DN))
name_dmngl_short = demangle_name(name, get_inf_attr(INF_SHORT_DN))
if name_dmngl_short:
if "::" in name_dmngl_short:
module_name = '[' + name_dmngl_short.split("::")[0] + ']'
modules.setdefault(module_name, []).append(fun)
else:
modules.setdefault("Unrecognized", []).append(fun)
else:
if name.startswith("sub_"):
modules.setdefault("sub_", []).append(fun)
elif '::' in name:
module_name = '[' + name.split("::")[0] + ']'
modules.setdefault(module_name, []).append(fun)
else:
modules.setdefault("Other", []).append(fun)
for module in modules.keys():
ida_dirtree.get_std_dirtree(ida_dirtree.DIRTREE_FUNCS).mkdir(module)
print('[D] Created folder for ', module)
for fun in modules[module]:
name = idc.get_func_name(fun)
ida_dirtree.get_std_dirtree(ida_dirtree.DIRTREE_FUNCS).rename(name, module+"/"+name)
print("="*20)
```
IN_DEVELOPMENT:
```python=
import idautils
import ida_dirtree
import idc
#######################################################################################
# doc = https://www.hex-rays.com/products/ida/support/idapython_docs/ida_dirtree.html #
#######################################################################################
print("="*20)
modules = {}
for fun in idautils.Functions():
name = idc.get_func_name(fun)
name_dmngl_long = demangle_name(name, get_inf_attr(INF_LONG_DN))
name_dmngl_short = demangle_name(name, get_inf_attr(INF_SHORT_DN))
if name_dmngl_short:
if "::" in name_dmngl_short:
module_name = '[' + name_dmngl_short.split("::")[0] + ']'
modules.setdefault(module_name, []).append(fun)
else:
modules.setdefault("Unrecognized", []).append(fun)
else:
if name.startswith("sub_"):
modules.setdefault("sub_", []).append(fun)
elif '::' in name:
module_name = '[' + name.split("::")[0] + ']'
#print(module_name, name)
modules.setdefault(module_name, []).append(fun)
else:
modules.setdefault("Other", []).append(fun)
# delete previosly created module dirs
# 1. Получаем direntry_t на нашу директорию
a = ida_dirtree.get_std_dirtree(ida_dirtree.DIRTREE_FUNCS).resolve_path('[std]')
print(a.idx)
# 2. Делаем на основе нее курсор для обхода, пихаем в idx значение нашей direntry_t
dt = ida_dirtree.dirtree_cursor_t()
dt.parent = a.idx
print(ida_dirtree.get_std_dirtree(ida_dirtree.DIRTREE_FUNCS).get_entry_name(a)) # self-check
# 3. Делаем итератор на основании курсора.
ite = ida_dirtree.dirtree_iterator_t()
ite.cursor = dt
# 4. Обходим при помощи findnext и получаем полный путь при помощи get_abspath(iter)
print(ida_dirtree.get_std_dirtree(ida_dirtree.DIRTREE_FUNCS).findnext(ite))
print(ida_dirtree.get_std_dirtree(ida_dirtree.DIRTREE_FUNCS).get_abspath(ite.cursor))
print(ida_dirtree.get_std_dirtree(ida_dirtree.DIRTREE_FUNCS).findnext(ite))
print(ida_dirtree.get_std_dirtree(ida_dirtree.DIRTREE_FUNCS).get_abspath(ite.cursor))
# Make folders with funcs
for module in modules.keys():
ida_dirtree.get_std_dirtree(ida_dirtree.DIRTREE_FUNCS).mkdir(module)
#print('[D] Created folder for', module, "\n Functions:", len(modules[module]))
for fun in modules[module]:
name = idc.get_func_name(fun)
#print("\t", name)
ida_dirtree.get_std_dirtree(ida_dirtree.DIRTREE_FUNCS).rename(name, module+"/"+name)
print("="*20)
```
```python=
import ida_dirtree
import idautils
import idc
print('-'*10)
func_dir_root = ida_dirtree.get_std_dirtree(ida_dirtree.DIRTREE_FUNCS)
def harvest_modules_names():
modules_list = []
for func in Functions():
name = idc.get_func_name(func)
demangled_short = demangle_name(name, get_inf_attr(INF_SHORT_DN))
if demangled_short:
if '::' in demangled_short:
nm = demangled_short.split('::')[0]
modules_list.append(nm)
return set(modules_list)
ml = harvest_modules_names()
for i in ml:
module_idx= func_dir_root.resolve_path(i).idx
if module_idx != 0xFFFFFFFF:
print(i, module_idx)
```
Function window sorting for C++:
```python=
import ida_dirtree
import idc
import re
print('-'*20)
root_node: ida_dirtree.dirtree_t
root_node = ida_dirtree.get_std_dirtree(ida_dirtree.DIRTREE_FUNCS) # root tree instance for Functions window
# (Folder Attribute Label) Append number of functions in module to module folder name
FLAG_ADD_COUNT_TO_MODULE_NAME = True
# Align module folder name. For readable attributes labels.
FLAG_ALIGNED_MODULE_NAMES = True
# Create prefix for matched names of folders. Like Favorite and for sorting. Hack: Use whitespace symbols.
# TODO: FIX: Can be nested match that will create nested prefix. Like [C... and [CFG...
FLAG_CURSED_FAVORITE = True
CURSED_PREFIX_MATCH = {
"\[Rt[\d\w]+\]" : "\u2606",
"C[A-Z]{1}[a-z]{1}[\d\w]+" : "\u2730",
"\[RtT[\d\w]+<[\d\w,\*\s]+>\]" : "\u2726"
}
def create_functions_modules_dict() -> dict:
'''
Create dictionary with modules as keys and functions names as values.
Return - dict.
'''
modules = {}
#iterate over functions in IDB
for func in Functions():
#get function name from IDB
name = idc.get_func_name(func)
#it will not be demangled. demangle it:
dmngl_name = demangle_name(name, get_inf_attr(INF_SHORT_DN)) #it return NULL on error
module_name = ""
if dmngl_name:
# we have smth that looks like C/C++ name mangling.
# it can have :: - namespace delimeter. Split by it and get module name:
if '::' in dmngl_name:
module_name = '[{}]'.format(dmngl_name.split('::')[0])
# if not - it some generic function (or without module namespace)
else:
module_name = "\u3000 Unrecognized"
else:
# we have some error with demangling...
# ... because sometimes IDA put already demangled name to IDB¯\_(ツ)_/¯
# ... or with functions already renamed by hand.
if '::' in name:
module_name = '[{}]'.format(name.split('::')[0])
# ... with standard unrecognized funcs
elif name.startswith("sub_"):
module_name = "\u3000 sub_"
# ... when it smth other. "_start" as example
else:
module_name = "\u3000 Unrecognized"
# and add original name to specific's module list
modules.setdefault(module_name, []).append(name)
return modules
def generate_functions_folders(modules: dict):
'''
Generate folders in Function window with ida_dirtree API
based on modules dictionary (use create_modules_dict()).
'''
format_for_attrs = '{}'
# Check comments at start of file for flags explanation
if FLAG_ALIGNED_MODULE_NAMES:
max_len_of_module_name = max([len(i) for i in modules.keys()])
format_for_attrs = "{{:<{}}}".format(max_len_of_module_name+2)
print("Generated folders (Functions): [Module Namespace] [number of f:unctions]")
#iterate over module names
for module in modules.keys():
module_folder = format_for_attrs.format(module)
# Check comments at start of file for flags explanation
if FLAG_ADD_COUNT_TO_MODULE_NAME:
module_folder += "[f: {}]".format(len(modules[module]))
# Check comments at start of file for flags explanation
if FLAG_CURSED_FAVORITE:
for pref in CURSED_PREFIX_MATCH.keys():
#if module.startswith(pref):
if re.match(pref, module):
module_folder = CURSED_PREFIX_MATCH[pref] + module_folder
print(module_folder)
# using ida_dirtree api for creating folder at root of Functions window
err = root_node.mkdir(module_folder)
if err:
print(err, module, root_node.errstr(err))
continue
for function in modules[module]:
# little cursed ida_dirtree API: moving files in folders with "tree.rename"
err = root_node.rename(function, module_folder+'/'+function)
if err:
print(err, function, root_node.errstr(err))
continue
def delist_functions_folders(start_path):
'''
Delete all folders at start_path in Functions windows and move functions to root directory node.
CTRL+Z works, if you need to revert changes.
'''
# create standard iterator
node_iter = ida_dirtree.dirtree_iterator_t()
# find first node in direntry_t. V - pattern used for search, so start_path also prefix for nested folders
ok = root_node.findfirst(node_iter, start_path+'/*')
paths = []
dirs = []
while ok:
# getting absolute path for node. direntry_t doesn't have any "get childs" functions?
dirent_path = root_node.get_abspath(node_iter.cursor)
if root_node.isfile(dirent_path):
paths.append(dirent_path) # if node is file - adding it to files list
elif root_node.isdir(dirent_path):
dirs.append(dirent_path) # if node is dir - recursive fall to this dir
delist_functions_folders(dirent_path) # and add for deleting list in future
else:
print("wtf") # never must be branched
raise Exception('wtf')
ok = root_node.findnext(node_iter)
for p in paths:
root_path = '/'+p.split('/')[-1]
err = root_node.rename(p, root_path) # move all found files to root directory
if err:
print(err, root_node.errstr(err))
for d in dirs:
err = root_node.rmdir(d) # and delete all folders
if err:
print(err, root_node.errstr(err))
delist_functions_folders('/')
modules = create_functions_modules_dict()
generate_functions_folders( modules )
```