Using gdb to Call Random Functions!

By Ron Bowes

Sometimes reverse engineering is graceful and purposeful, where you thread the needle just right to figure out some obscure, undocumented function and how it can be used to the best of your ability. This article isn’t about that.

In this post, we’ll look at how we can find hidden functionality by jumping to random functions in-memory! This is normally a good way to crash the program, but who knows? You might find a gem!

This technique is useful for finding hidden functionality, but it’s somewhat limited: it’ll only work for applications that you’re capable of debugging. With few exceptions (I’ve used a technique like this to break out of an improperly implemented sandbox before), this technique is primarily for analysis, not for exploitation or privilege escalation.

Creating a Test Binary

Let’s start by writing a simple toy program. You can download this program as a 32- and 64-bit Linux binary, as well as the source code and Makefile, here

Here’s the full code:


#include <stdio.h>

void random_function() {
  printf("You called me!\n");
}

int main(int argc, char *argv[]) {
  printf("Can you call random_function()?\n");
  printf("Press <enter> to continue\n");
  getchar();

  printf("Good bye!\n");
}

Put that in a file called jumpdemo.c and compile with the following command:

gcc -g -O0 -o jumpdemo jumpdemo.c

We add -O0 to the command line to prevent the compiler from performing optimizations such as deleting unused functions under the guise of “helping”. If you grabbed our source code, you can simply run make after extracting it.

Finding Interesting Functions

Let’s assume for the purposes of this post that the binaries are compiled with symbols. That means that you can see the function names! My favorite tool for analyzing binaries is IDA, but for our purposes, the nm command is more than sufficient:


$ nm ./jumpdemo
0000000000601040 B __bss_start
0000000000601030 D __data_start
0000000000601030 W data_start
0000000000601038 D __dso_handle
0000000000601040 D _edata
0000000000601048 B _end
0000000000400624 T _fini
                 U getchar@@GLIBC_2.2.5
                 w __gmon_start__
0000000000400400 T _init
0000000000400630 R _IO_stdin_used
                 w _ITM_deregisterTMCloneTable
                 w _ITM_registerTMCloneTable
                 w _Jv_RegisterClasses
0000000000400620 T __libc_csu_fini
00000000004005b0 T __libc_csu_init
                 U __libc_start_main@@GLIBC_2.2.5
0000000000400577 T main
                 U puts@@GLIBC_2.2.5
0000000000400566 T random_function
0000000000400470 T _start
0000000000601040 D __TMC_END__

Everything you see here is a symbol, and the ones with T in front are ones that we can actually call, but the ones that start with an underscore (‘_’) are built-in stuff that we can just ignore (in a “real” situation, you shouldn’t discount something simply because the name starts with an underscore, of course).

The two functions that might be interesting are “main” and “random_function”, so that’s what we’re going to target!

Load in gdb

Before we can call one of these functions, we need to run the project in gdb — the GNU Project Debugger. On the command line (from the directory containing the compiled jumpdemo binary), run ./jumpdemo in gdb:


$ gdb -q ./jumpdemo
Reading symbols from ./jumpdemo...(no debugging symbols found)...done.
(gdb)

The -q flag is simply to disable unnecessary output. After you get to the (gdb) prompt, the jumpdemo application is loaded and ready to run, but it hasn’t actually been started yet. You can verify that by trying to run a command such as continue:


(gdb) continue
The program is not being run.

gdb is an extremely powerful tool, with a ton of different commands. You can enter help into the prompt to learn more, and you can also use help <command> on any of the commands we use (such as help break) to get more details. Give it a try!

Simple Case: Just Call It!

Now that the program is ready to go in gdb, we can run it with the run command (don’t forget to try help run!). You’ll see the same output as you would if you’d run it directly until it ends, at which point we’re back in gdb. You can run it over and over if you desire, but that’s not really going to get you anywhere.

In order to modify the application at runtime, it is necessary to run the program and then stop it again before it finishes cleanly. The most common way is to use a breakpoint (help break) on main:


$ gdb -q ./jumpdemo
Reading symbols from ./jumpdemo...(no debugging symbols found)...done.
(gdb) break main
Breakpoint 1 at 0x40057b
(gdb) 

Then run the binary and watch what happens:


(gdb) run
Starting program: /home/ron/blogs/jumpdemo

Breakpoint 1, 0x000000000040057b in main ()
(gdb) 

Now we have control of the application in the running (but paused) state! We can view/edit memory, modify registers, continue execution, jump to another part of the code, and much much more!

In our case, as I’m sure you’ve guessed by now, we’re going to move the program’s execution to another part of the program. Specifically, we’re just going to use gdb‘s jump command (help jump!) to resume execution at the start of random_function():


$ gdb -q ./jumpdemo
Reading symbols from ./jumpdemo...(no debugging symbols found)...done.
(gdb) break main
Breakpoint 1 at 0x40057b
(gdb) run
Starting program: /home/ron/blogs/jumpdemo 

Breakpoint 1, 0x000000000040057b in main ()
(gdb) help jump
Continue program being debugged at specified line or address.
Usage: jump <location>
Give as argument either LINENUM or *ADDR, where ADDR is an expression
for an address to start at.
(gdb) jump random_function
Continuing at 0x40056a.
You called me!
[Inferior 1 (process 11391) exited with code 017]

We did it! The program printed, “You called me!”, which means we successfully ran random_function()! Exit code 017 means the process didn’t exit cleanly, but that is to be expected. We just ran an unexpected function with absolutely no context!

If for some reason you can’t get breakpoints to work (maybe the program was compiled without symbols and you don’t actually know where main is), you can do the same thing without breakpoints by pressing ctrl-c while the binary is running:


(gdb) run
Starting program: /home/ron/blogs/jumpdemo 
Can you call random_function()?
Press <enter> to continue
^C
Program received signal SIGINT, Interrupt.
0x00007ffff7b04260 in __read_nocancel () at ../sysdeps/unix/syscall-template.S:84
84      ../sysdeps/unix/syscall-template.S: No such file or directory.
(gdb) jump random_function
Continuing at 0x40056a.
You called me!
Program received signal SIGSEGV, Segmentation fault.
0x0000000000000001 in ?? ()

Don’t worry about the segmentation fault, like the 017 exit code above, it’s happening because the program has no idea what to do after it’s finished running random_function.

Another mildly interesting thing that you can do is jump back to main() to make the program think it’s starting over:


(gdb) run
Starting program: /home/ron/blogs/jumpdemo 
Can you call random_function()?
Press <enter> to continue
^C
Program received signal SIGINT, Interrupt.
0x00007ffff7b04260 in __read_nocancel () at ../sysdeps/unix/syscall-template.S:84
84      ../sysdeps/unix/syscall-template.S: No such file or directory.
(gdb) jump main
Continuing at 0x40057b.
Can you call random_function()?
Press <enter> to continue

I use jump-back-to-main a lot while doing actual exploit development to check whether or not I actually have code execution without trying to develop working shellcode. If the program appears to start over, you’ve changed the current instruction!

Real-world Example

For a quick example of this technique doing something visible against a “real” application, I took a look through my tools/ folder for a simple command line Linux application, and found THC-Hydra. I compiled it the standard way — ./configure && make — and ran nm against it:


$ nm ./hydra
0000000000657abc b A
0000000000657cb4 B accntFlag
                 U alarm@@GLIBC_2.2.5
000000000043baf0 T alarming
0000000000657ae4 B alarm_went_off
00000000004266f0 T analyze_server_response
0000000000654200 B apop_challenge
                 U ASN1_OBJECT_free@@OPENSSL_1.0.0
                 U __assert_fail@@GLIBC_2.2.5
0000000000655c00 B auth_flag
0000000000657ab8 b B
0000000000408b60 T bail
000000000044b760 r base64digits
000000000044b6e0 r base64val
0000000000433450 T bf_get_pcount
…

Turns out, there are over 700 exported symbols. Wow! We can narrow it down by grepping for T, which refers to a symbol in the code section, but there are still a lot of those. I manually looked over the list to find ones that might work (ie, functions that might print output without having any valid context / parameters).

I started by running the application with a “normal” set of arguments to make note of the “normal” output:


$ gdb -q --args ./hydra -l john -p doe localhost http-head
Reading symbols from ./hydra...(no debugging symbols found)...done.
(gdb) run
Starting program: /home/ron/tools/hydra-7.1-src/hydra -l john -p doe localhost http-head
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Hydra v7.1 (c)2011 by van Hauser/THC & David Maciejak - for legal purposes only

Hydra (http://www.thc.org/thc-hydra) starting at 2018-10-15 14:47:12
Error: You must supply the web page as an additional option or via -m
[Inferior 1 (process 3619) exited with code 0377]
(gdb)

The only new thing here is --args which is simply a signal to gdb that there are going to be command line arguments to the binary.

After I determined what the output is supposed to look like, I set a breakpoint on main, like before, and jumped to help() after breaking:

$ gdb -q --args ./hydra -l john -p doe localhost http-head 
Reading symbols from ./hydra...(no debugging symbols found)...done. 
(gdb) break main 
Breakpoint 1 at 0x403bf0 
(gdb) run
Starting program: /home/ron/tools/hydra-7.1-src/hydra -l john -p doe localhost http-head [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

Breakpoint 1, 0x0000000000403bf0 in main () 
(gdb) jump help 
Continuing at 0x408420. Syntax: (null) [[[-l LOGIN|-L FILE] [-p PASS|-P FILE]] | [-C FILE]] [-e nsr] [-o FILE] [-t TASKS] [-M FILE [-T TASKS]] [-w TIME] [-W TIME] [-f] [-s PORT] [-x MIN:MAX:CHARSET] [-SuvV46] [server service [OPT]]|[service://server[:PORT][/OPT]]

Options: -R restore a previous aborted/crashed session -S perform an SSL connect ..

I tried help_bfg as well:


(gdb) jump help_bfg
Continuing at 0x408580.
Hydra bruteforce password generation option usage:

  -x MIN:MAX:CHARSET

     MIN     is the minimum number of characters in the password
     MAX     is the maximum number of characters in the password
     CHARSET is a specification of the characters to use in the generation
             valid CHARSET values are: 'a' for lowercase letters,
             'A' for uppercase letters, '1' for numbers, and for all others,
             just add their real representation.

Examples:
   -x 3:5:a  generate passwords from length 3 to 5 with all lowercase letters
   -x 5:8:A1 generate passwords from length 5 to 8 with uppercase and numbers
   -x 1:3:/  generate passwords from length 1 to 3 containing only slashes
   -x 5:5:/%,.-  generate passwords with length 5 which consists only of /%,.-

The bruteforce mode was made by Jan Dlabal, http://houbysoft.com/bfg/
[Inferior 1 (process 9980) exited with code 0377]

Neat! Another help output! Although these are easy to get to legitimately, it’s really neat to see them called when they aren’t expected to be called! Stuff “just works”, kinda.

Speaking of kinda working, I also jumped to hydra_debug(), which had slightly more interesting results:


(gdb) jump hydra_debug
Continuing at 0x408b40.
[DEBUG] Code: ����   Time: 1539640228
[DEBUG] Options: mode 0  ssl 0  restore 0  showAttempt 0  tasks 0  max_use 0 tnp 0  tpsal 0  tprl 0  exit_found 0  miscptr (null)  service (null)
[DEBUG] Brains: active 0  targets 0  finished 0  todo_all 0  todo 0  sent 0  found 0  countlogin 0  sizelogin 0  countpass 0  sizepass 0
[Inferior 1 (process 7761) exited normally]

It does its best to print out the statistics, but because it didn’t expect that function to be called, it just prints nonsense.

Every other function I tried either does nothing or simply crashes the application. When you call a function that expects certain parameters without any such parameters, it normally tries to access invalid memory, so it’s pretty unsurprising. It’s actually surprising that it works at all! It’s pure luck that the string it tried to print is an invalid string rather than invalid memory (which would crash).

Conclusion

This may not seem like much, but it’s actually a very simple and straightforward reverse engineering technique that sometimes works shockingly well. Grab some open source applications, run nm on them, and try calling some functions. You never know what you’ll figure out!

–Ron Bowes
@iagox86