Package: hilda
DescriptionInstallationHow to useStarting a Hilda shellBare modeRemote modeUsageSymbol objectsGlobalized symbolsSearching for the right symbolObjective-C ClassesObjective-C ObjectsUsing snippetsContributingDescriptionHilda 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 ofFrida. 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:LinuxAndroidSince 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
tostart a discussion 💬.InstallationRequirements for remote iOS device (not required for debugging a local OSX process):Jailbroken iOS devicedebugserverin device's PATHYou can use this tool in order to obtain the binaryAfter re-signing with new entitlements, you can put the binary in the following path:/usr/bin/debugserverIn order to install please run:xcrunpython3-mpipinstall--user-Uhilda⚠️ Please note that Hilda is installed on top of XCode's python so LLDB will be able to use its features.How to useStarting a Hilda shellAttach modeUse the attach sub-command in order to start an LLDB shell attached to given process.hildaattach[-ppid][-nprocess-name]After attaching, simply executehildacommand to enter the hilda shell.Bare modeUse "Bare mode" to get a "bare-bones" lldb shell, whereas hilda plugin is already loaded and ready to start. This mode
is useful when you need to have custom commands for attaching to the target process (for example when debugging OSX
processes).To start this mode simply use:hildabarePlease 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:platformconnectconnect://ip:port... and attaching to a local process:processattach-nproccess_name
processattach-pproccess_pidWhen you are ready, just executehildato move to Hilda's iPython shell.Remote modeThis mode will auto-connect to the remote device and attach to your target process assuming you are trying to debug a
remote jailbroken iOS device.Please note the following:script assumes the connected device alreadyhas a running ssh server, which doesn't require a password (you can
usessh-copy-idto achieve this).From this point the flow diverges into 2 flows:The connected device is connected via networkRun the following command:hildaremoteHOSTNAMEPORTUsageUpon 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 fromp:hdPrint an hexdump of given bufferlsofGet dictionary of all open FDsbtPrint an improved backtrace.disable_jetsam_memory_checksDisable jetsam memory checks, prevent raising:error: Execution was interrupted, reason: EXC_RESOURCE RESOURCE_TYPE_MEMORY (limit=15 MB, unused=0x0).when evaluating expression.symbolGet symbol object for a given addressobjc_symbolGet objc symbol wrapper for given addressinjectInject a single library into currently running processrebind_symbolsReparse all loaded images symbolspokeWrite data at given addresspeekRead data at given addresspeek_strPeek a buffer till null terminationstopStop process.contContinue process.detachDetach from process.
Useful in order to exit gracefully so process doesn't get killed
while you exitdisassPrint disassembly from a given addressfile_symbolCalculate symbol address without ASLRget_registerGet value for register by its nameset_registerSet value for register by its nameobjc_callSimulate a call to an objc selectorcallCall function at given address with given parametersmonitorMonitor every time a given address is called
The following options are available:regs={reg1: format}
will print register values

         Available formats:
             x: hex
             s: string
             cf: use CFCopyDescription() to get more informative description of the object
             po: use LLDB po command
             User defined function, will be called like `format_function(hilda_client, value)`.

         For example:
             regs={'x0': 'x'} -> x0 will be printed in HEX format
     expr={lldb_expression: format}
         lldb_expression can be for example '$x0' or '$arg1'
         format behaves just like 'regs' option
     retval=format
         Print function's return value. The format is the same as regs format.
     stop=True
         force a stop at every hit
     bt=True
         print backtrace
     cmd=[cmd1, cmd2]
         run several LLDB commands, one by another
     force_return=value
         force a return from function with the specified value
     name=some_value
         use `some_name` instead of the symbol name automatically extracted from the calling frame
     override=True
         override previous break point at same locationshow_current_sourceprint current source code if possiblefinishRun current frame till its end.step_intoStep into current instruction.step_overStep over current instruction.remove_all_hilda_breakpointsRemove all breakpoints created by Hildaremove_hilda_breakpointRemove a single breakpoint placed by Hildaforce_returnPrematurely return from a stack frame, short-circuiting exection of newer frames and optionally
yielding a specified value.proc_infoPrint information about currently running mapped process.print_proc_entitlementsGet the plist embedded inside the process' __LINKEDIT section.bpAdd a breakpointshow_hilda_breakpointsShow existing breakpoints created by Hilda.show_commandsShow available commands.saveSave loaded symbols map (for loading later using the load() command)loadLoad an existing symbols map (previously saved by the save() command)poPrint given object using LLDB's po command
Can also run big chunks of native code:po('NSMutableString *s = [NSMutableString string]; [s appendString:@"abc"]; [s description]')globalize_symbolsMake all symbols in python's global scopejumpjump to given symbollldb_handle_commandExecute an LLDB command
For example:
lldb_handle_command('register read')objc_get_classGet ObjC class objectCFSTRCreate CFStringRef object from given stringnsCreate NSObject from given datafrom_nsCreate python object from NS object.evaluate_expressionWrapper for LLDB's EvaluateExpression.
Used for quick code snippets.Feel free to use local variables inside the expression using format string.
For example:
currentDevice = objc_get_class('UIDevice').currentDevice
evaluate_expression(f'[[{currentDevice} systemName] hasPrefix:@"2"]')import_moduleImport & reload given python module (intended mainly for external snippets)set_evaluation_unwindSet whether LLDB will attempt to unwind the stack whenever an expression evaluation error occurs.
Use unwind() to restore when an error is raised in this case.get_evaluation_unwindGet evaluation unwind state.
When this value is True, LLDB will attempt unwinding the stack on evaluation errors.
Otherwise, the stack frame will remain the same on errors to help you investigate the error.set_evaluation_ignore_breakpointsSet whether to ignore breakpoints while evaluating expressionsget_evaluation_ignore_breakpointsGet evaluation "ignore-breakpoints" state.unwindUnwind the stack (useful when get_evaluation_unwind() == False)UI ConfigurationHilda contains minimal UI for examining the target state.
The UI is divided into views:RegistersDisassemblyStackBacktraceThis UI can be displayed at any time be executing:ui.show()By defaultstep_intoandstep_overwill show this UI automatically.
You may disable this behaviour by executing:ui.active=FalseAttentively, if you want to display UI after hitting a breakpoint, you can registerui.showas callback:p.symbol(0x7ff7b97c21b0).bp(ui.show)Try playing with the UI settings by yourself:# Disable stack viewui.views.stack.active=False# View words from the stackui.views.stack.depth=10# View last 10 framesui.views.backtrace.depth=10# Disassemble 5 instructionsui.views.disassembly.instruction_count=5# Change disassembly syntax to AT&Tui.views.disassembly.flavor='att'# View floating point registersui.views.registers.rtype='float'# Change addresses print colorui.colors.address='red'# Change titles colorui.color.title='green'Symbol objectsIn Hilda, almost everything is wrapped using theSymbolObject. 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)# the Symbol object extends `int`True==isinstance(s,int)# print the un-shifted file address# (calculating the ASLR shift for you, so you can just view it in IDA)print(s.file_address)# or.. if you know the file address, but don't wanna mess# with ASLR calculationss=p.file_symbol(0x12345678)# peek(/read) 20 bytes of memoryprint(s.peek(20))# write into this memorys.poke('abc')# let LLDB print-object (it should guess the type automatically# based on its memory layout)print(s.po())# or you can help LLDB with telling it its type manuallyprint(s.po('char *'))# jump to `s` as a function, passing (1, "string") as its argss(1,"string")# change the size of each item_size inside `s` for derefss.item_size=1# *(char *)s = 1s[0]=1# *(((char *)s)+1) = 1s[1]=1# symbol inherits from int, so all int operations applys+=4# change s item size back to 8 to store pointerss.item_size=8# *(intptr_t *)s = 1s[0]=1# storing the return value of the function executed at `0x11223344`# into `*s`s[0]=p.symbol(0x11223344)()# calling symbols also returns symbols# attempt to resolve symbol's nameprint(p.symbol(0x11223344).lldb_symbol)# monitor each time a symbol is called into console and print its backtrace (`bt` option)# this will create a scripted breakpoint which prints your desired data and continues.monitor(bt=True)# you can also:#   bt -> view the backtrace#   regs -> view registers upon each call in your desired format#   retval -> view the return value upon each call in your desired format#   cmd -> execute a list of LLDB commands on each hits.monitor(regs={'x0':'x'},# print `x0` in HEX formretval='po',# use LLDB's `po` for printing the returned valuebt=True,# view backtrace (will also resolve ASLR addresses for you)cmd=['thread list'],# show thread list)# we can also just `force_return` with a hard-coded value to practically disable# a specific functionalitys.monitor(force_return=0)# cause the function to always return `0`# as for everything, if you need help understanding each such feature,# simply execute the following to view its help (many such features even contain examples)s.monitor?# create a scripted_breakpoint manuallydefscripted_breakpoint(hilda,*args):# like everything in hilda, registers are also# just simple `Symbol` objects, so feel free to# use them to your heart's content :)ifhilda.registers.x0.peek(4)==b'\x11\x22\x33\x44':hilda.registers.x0=hilda.symbols.malloc(200)hilda.registers.x0.poke(b'\x22'*200)# just continue the processhilda.cont()s.bp(scripted_breakpoint)# Place a breakpoint at a symbol not yet loaded by it's namep.bp('symbol_name')# In case you need to specify a specific library it's loaded fromp.bp('symbol_name',module_name='ModuleName')Globalized symbolsUsually you would want/need to use the symbols already mapped into the currently running process. To do so, you can
access them usingsymbols.<symbol-name>. Thesymbolsglobal object is of typeSymbolsJar, which is a wrapper
todictfor accessing all exported symbols. For example, the following will generate a call to the exportedmallocfunction with20as 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)# is equivalent to:malloc=p.symbols.mallocx=malloc(20)Searching for the right symbolSometimes 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 therebind_symbols()command - to help you find the symbol you are looking for.p.rebind_symbols()# this might take some time# find all symbols prefixed as `mem*` AND don't have `cpy`# in their namejar=p.symbols.startswith('mem')-p.symbols.find('cpy')# filter only symbols of type "code" (removing data global for example)jar=jar.code()# monitor every time each one is called, print its `x0` in HEX# form and show the backtracejar.monitor(regs={'x0':'x'},bt=True)Objective-C ClassesThe same as symbols applies to Objective-C classes name resolution. You can either:d=NSDictionary.new()# call its `new` selector# which is equivalent to:NSDictionary=p.objc_get_class('NSDictionary')d=NSDictionary.new()This is possible only sinceNSDictionaryis exported. In case it is not, you must callobjc_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:# show the class' ivarsprint(NSDictionary.ivars)# show the class' methodsprint(NSDictionary.methods)# show the class' proprtiesprint(NSDictionary.properties)# view class' selectors which are prefixed with 'init'print(NSDictionary.symbols_jar.startswith('-[NSDictionary init'))# you can of course use any of `SymbolsJar` over them, for example:# this will `po` (print object) all those selectors returned valueNSDictionary.symbols_jar.startswith('-[NSDictionary init').monitior(retval='po')# monitor each time any selector in NSDictionary is calledNSDictionary.monitor()# `force_return` for some specific selector with a hard-coded value (4)NSDictionary.get_method('valueForKey:').address.monitor(force_return=4)# capture the `self` object at the first hit of any selector# `True` for busy-wait for object to be captureddictionary=NSDictionary.capture_self(True)# print a colored and formatted version for class layoutdictionary.show()Objective-C ObjectsIn order to work with ObjC objects, each symbol contains a property calledobjc_symbol. After calling, you can work better with each object:dict=NSDictionary.new().objc_symboldict.show()# print object layout# just like class, you can access its ivars, method, etc...print(dict.ivars)# except now they have values you can viewprint(dict._ivarName)# or editdict._ivarName=value# and of course you can call the object's methods# hilda will checks if the method returned an ObjC object:#   - if so, call `objc_symbol` upon it for you#   - otherwise, leave it as a simple `Symbol` objectarr=dict.objectForKey_('keyContainingNSArray')# you can also call class-methods# hilda will call it using either the instance object,# or the class object respectively of the usenewDict=dict.dictionary()# print the retrieved objectprint(arr.po())Also, working with Objective-C objects like this can be somewhat exhausting, so we created thensandfrom_nscommands so you are able to use complicated types when parsing values and passing as arguments:importdatetime# using the `ns` command we can just pass a python-native dictionaryfunction_requiring_a_specfic_dictionary(ns({'key1':'string',# will convert to NSString'key2':True,# will convert to NSNumber'key3':b'1234',# will convert to NSData'key4':datetime.datetime(2021,1,1)# will convert to NSDate}))# and also parse onenormal_python_dict=p.cf({'key1':'string',# will convert to NSString'key2':True,# will convert to NSNumber'key3':b'1234',# will convert to NSData'key4':datetime.datetime(2021,1,1)# will convert to NSDate}).py()On last resort, if the object is not serializable for this to work, you can just run pure Objective-C code:# let LLDB compile and execute the expressionabc_string=p.evaluate_expression('[NSString stringWithFormat:@"abc"]')# will print "abc"print(abc_string.po())Using snippetsSnippets 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:fromhilda.snippetsimportsnippet_namesnippet_name.do_domething()For example, XPC sniffing can be done using:fromhilda.snippetsimportxpcxpc.sniff_all()This will monitor all XPC related traffic in the given process.ContributingPlease run the tests as follows before submitting a PR:xcrunpython3-mtestsaggregated# wait for lldb shell promptrun_tests