Hilda
Overview
Hilda is a debugger which combines both the power of LLDB and iPython for easier debugging.
The name originates from the TV show "Hilda", which is the best friend of
Frida. Both Frida and Hilda are meant for pretty much the same purpose, except Hilda takes the
more "
debugger-y" approach (based on LLDB).
Currently, the project is intended for iOS/OSX debugging, but in the future we will possibly add support for the
following platforms as well:
Since LLDB allows abstraction for both platform and architecture, it should be possible to make the necessary changes
without too many modifications.
Pull requests are more than welcome 😊.
If you need help or have an amazing idea you would like to suggest, feel free
to start a discussion 💬.
Installation
Requirements for remote iOS device (not required for debugging a local OSX process):
- Jailbroken iOS device
debugserver
in device's PATH
In order to install please run:
xcrun python3 -m pip install --user -U hilda
⚠️ Please note that Hilda is installed on top of XCode's python so LLDB will be able to use its features.
How to use
Starting a Hilda interactive shell
You can may start a Hilda interactive shell by invoking any of the subcommand:
hilda launch /path/to/executable
- Launch given executable on current host
hilda attach [-p pid] [-n process-name]
- Attach to an already running process on current host (specified by either
pid
or process-name
)
hilda remote HOSTNAME PORT
- Attach to an already running process on a target host (sepcified by
HOSTNAME PORT
)
hilda bare
-
Only start an LLDB shell and load Hilda as a plugin.
-
Please refer to the following help page if you require help on the command available to you within the lldb shell:
lldb command map.
As a cheatsheet, connecting to a remote platform like so:
platform connect connect://ip:port
... and attaching to a local process:
process attach -n proccess_name
process attach -p proccess_pid
When you are ready, just execute hilda
to move to Hilda's iPython shell.
Inside a Hilda shell
Upon starting Hilda shell, you are greeted with:
Hilda has been successfully loaded! 😎
Use the p global to access all features.
Have a nice flight ✈️! Starting an IPython shell...
Here is a gist of methods you can access from p
:
All these methods are available from the global p
within the newly created IPython shell. In addition, you may invoke any of the exported APIs described in the Python API
Magic functions
Sometimes accessing the Python API can be tiring, so we added some magic functions to help you out!
%objc <className>
- Equivalent to:
className = p.objc_get_class(className)
%fbp <filename> <addressInHex>
- Equivalent to:
p.file_symbol(addressInHex, filename).bp()
Key-bindings
- F7: Step Into
- F8: Step Over
- F9: Continue
- F10: Stop
Configurables
The global cfg
used to configure various settings for evaluation and monitoring.
These settings include:
evaluation_unwind_on_error
: Whether to unwind on error during evaluation. (Default: False
)evaluation_ignore_breakpoints
: Whether to ignore breakpoints during evaluation. (Default: False
)nsobject_exclusion
: Whether to exclude NSObject
during evaluation, reducing IPython autocomplete results. (
Default: False
)objc_verbose_monitor
: When set to True
, using monitor()
will automatically print Objective-C method arguments. (
Default: False
)
UI Configuration
Hilda contains a minimal UI for examining the target state.
The UI is divided into views:
- Registers
- Disassembly
- Stack
- Backtrace
![img.png](gifs/ui.png)
This UI can be displayed at any time be executing:
ui.show()
By default step_into
and step_over
will show this UI automatically.
You may disable this behaviour by executing:
ui.active = False
Attentively, if you want to display UI after hitting a breakpoint, you can register ui.show
as callback:
p.symbol(0x7ff7b97c21b0).bp(ui.show)
Try playing with the UI settings by yourself:
ui.views.stack.active = False
ui.views.stack.depth = 10
ui.views.backtrace.depth = 10
ui.views.disassembly.instruction_count = 5
ui.views.disassembly.flavor = 'att'
ui.views.registers.rtype = 'float'
ui.colors.address = 'red'
ui.color.title = 'green'
Python API
Hilda provides a comprehensive API wrappers to access LLDB capabilities.
This API may be used to access process memory, trigger functions, place breakpoints and much more!
Also, in addition to access this API using the Hilda shell, you may also use pure-python script using any of the create_hilda_client_using_*
APIs.
Consider the following snippet as an example of such usage:
from hilda.launch_lldb import create_hilda_client_using_attach_by_name
p = create_hilda_client_using_attach_by_name('sysmond')
print(p.symbols.malloc(10))
p.detach()
Please note this script must be executed using xcrun python3
in order for it to be able to access LLDB API.
Symbol objects
In Hilda, almost everything is wrapped using the Symbol
Object. Symbol is just a nicer way for referring to addresses
encapsulated with an object allowing to deref the memory inside, or use these addresses as functions.
In order to create a symbol from a given address, please use:
s = p.symbol(0x12345678)
True == isinstance(s, int)
print(s.file_address)
s = p.file_symbol(0x12345678)
print(s.peek(20))
s.poke('abc')
print(s.po())
print(s.po('char *'))
s(1, "string")
s.item_size = 1
s[0] = 1
s[1] = 1
s += 4
s.item_size = 8
s[0] = 1
s[0] = p.symbol(0x11223344)()
print(p.symbol(0x11223344).lldb_symbol)
s.monitor(bt=True)
s.monitor(regs={'x0': 'x'},
retval='po',
bt=True,
cmd=['thread list'],
)
s.monitor(force_return=0)
s.monitor?
def scripted_breakpoint(hilda, *args):
if hilda.registers.x0.peek(4) == b'\x11\x22\x33\x44':
hilda.registers.x0 = hilda.symbols.malloc(200)
hilda.registers.x0.poke(b'\x22' * 200)
hilda.cont()
s.bp(scripted_breakpoint)
p.bp('symbol_name')
p.bp('symbol_name', module_name='ModuleName')
Globalized symbols
Usually you would want/need to use the symbols already mapped into the currently running process. To do so, you can
access them using symbols.<symbol-name>
. The symbols
global object is of type SymbolsJar
, which is a wrapper
to dict
for accessing all exported symbols. For example, the following will generate a call to the exported
malloc
function with 20
as its only argument:
x = p.symbols.malloc(20)
You can also just write their name as if they already were in the global scope. Hilda will check if no name collision
exists, and if so, will perform the following lazily for you:
x = malloc(20)
malloc = p.symbols.malloc
x = malloc(20)
Searching for the right symbol
Sometimes you don't really know where to start your research. All you have is just theories of how your desired exported
symbol should be called (if any).
For that reason alone, we have the rebind_symbols()
command - to help you find the symbol you are looking for.
p.rebind_symbols()
jar = p.symbols.startswith('mem') - p.symbols.find('cpy')
jar = jar.code()
jar.monitor(regs={'x0': 'x'}, bt=True)
Objective-C Classes
The same as symbols applies to Objective-C classes name resolution. You can either:
d = NSDictionary.new()
NSDictionary = p.objc_get_class('NSDictionary')
d = NSDictionary.new()
%objc
NSDictionary
This is possible only since NSDictionary
is exported. In case it is not, you must call objc_get_class()
explicitly.
As you can see, you can directly access all the class' methods.
Please look what more stuff you can do as shown below:
print(NSDictionary.ivars)
print(NSDictionary.methods)
print(NSDictionary.properties)
print(NSDictionary.symbols_jar.startswith('-[NSDictionary init'))
NSDictionary.symbols_jar.startswith('-[NSDictionary init').monitior(retval='po')
NSDictionary.monitor()
NSDictionary.get_method('valueForKey:').address.monitor(force_return=4)
dictionary = NSDictionary.capture_self(True)
dictionary.show()
Objective-C Objects
In order to work with ObjC objects, each symbol contains a property called
objc_symbol
. After calling, you can work better with each object:
dict = NSDictionary.new().objc_symbol
dict.show()
print(dict.ivars)
print(dict._ivarName)
dict._ivarName = value
arr = dict.objectForKey_('keyContainingNSArray')
newDict = dict.dictionary()
print(arr.po())
Also, working with Objective-C objects like this can be somewhat exhausting, so we created the ns
and from_ns
commands so you are able to use complicated types when parsing values and passing as arguments:
import datetime
function_requiring_a_specfic_dictionary(ns({
'key1': 'string',
'key2': True,
'key3': b'1234',
'key4': datetime.datetime(2021, 1, 1)
}))
normal_python_dict = p.cf({
'key1': 'string',
'key2': True,
'key3': b'1234',
'key4': datetime.datetime(2021, 1, 1)
}).py()
On last resort, if the object is not serializable for this to work, you can just run pure Objective-C code:
abc_string = p.evaluate_expression('[NSString stringWithFormat:@"abc"]')
print(abc_string.po())
Using snippets
Snippets are extensions for normal functionality used as quick cookbooks for day-to-day tasks of a debugger.
They all use the following concept to use:
from hilda.snippets import snippet_name
snippet_name.do_domething()
For example, XPC sniffing can be done using:
from hilda.snippets import xpc
xpc.sniff_all()
This will monitor all XPC related traffic in the given process.
Contributing
Please run the tests as follows before submitting a PR:
xcrun python3 -m pytest