I have GDB but the binary I want reverse engineer dynamically has no symbols, that is when I run the file utility it shows me stripped:

ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.18, stripped

What options do I have if the environment in which this runs doesn't allow a remote IDA Pro instance to connect to gdbserver? In short: the environment you have is limited in what it allows you to do, but you do have trusty old gdb and a binary to reverse engineer.

  • 3
    unstrip is a tool that can try to restore lost symbol names of known library calls. – Mellowcandle Apr 27 '13 at 5:29
  • @Mellowcandle: thanks for pointing it out. But isn't it mostly useless, because you'll see those names resolved already if the loader was able to resolve them and you have symbols for the libraries themselves? What am I missing? I mean in the described scenario, I can see scenarios where it makes perfect sense. – 0xC0000022L Apr 27 '13 at 13:42
  • 1
    Actually I posted it before I read your answer which is awesome by the way. – Mellowcandle Apr 27 '13 at 14:21
up vote 127 down vote accepted

Conventions used plus preliminary remarks

I am trimming the output of GDB for brevity since it usually shows the copyright and other information at the beginning of ever session. When I reproduce the output I'll start at the first (gdb) prompt line, or in case or auto-executed commands from the first genuine output line.

In order to distinguish commands entered at the GDB prompt, these will have a leading (gdb) just like in the real world. For shell command this will either be no prefix at all or $ as it appears to be the convention on most unixoid systems.

When I use a particular command, such as vim as my editor, you are free to use your own favorite editor of course. Be it emacs or nano, I won't judge ya ;)

Getting started

This section is about setting up your gdb environment and starting the process. I'll also include a few tidbits for the complete newcomers.

Tricks you should know

GDB has a nice prompt at which your cursor will stop after the program breaks or whenever you are stepping or some such.

  • Pressing RETURN (aka ENTER) after you ran a GDB command will run the same command again. This is useful when you are stepping through code with step or next and simply want to continue one by one.
  • Commands can be abbreviated as long as they are unambiguous. For some oft used commands a particular shorthand exists that takes precedence despite ambiguity:
    • b for break (despite bt and backtrace)
    • c or cont for continue (despite catch, call and so on)
    • n for next (despite ni and nexti)
  • You can call actual library functions or even functions from within the debugged program using the command call. This means you can try out behavior or force behavior.
  • You can start GDB with gdbtui or gdb -tui to get a - supposedly more convenient - more visual text user interface. It shows the source code at the top and the (gdb) prompt below. You can also switch to this layout by executing the command layout src at the (gdb) prompt.
  • GDB has a command line completion feature much like many shells, so use Tab to your advantage and also make sure to use help or help [keyword|command] whenever you are in need of help.
  • shell allows you to execute commands in the shell, so that you can run commands from within your GDB session. During development an example would be shell make.
  • print, examine and display know various formats (/FMT) which you can use to make the output more readable.
  • When source-level debugging you can use C type casts to display values. Imagine a C string behind a void * (which GDB knows thanks to the symbols in such case). Simply cast to (char*) and print it: print (char*)variable.

Getting the process to run

Since we want to dynamically analyze the binary, we need to start it first.

Command line

We can do that straight from the command line by passing not only the path to the binary, but also the arguments we want to start it with. The whole process then looks like this:

$ gdb --args ./exe argument1 argument2

Easy enough. From the (gdb) prompt you can then issue the run command (shorthand r) to run ./exe with the parameters given on the command line. I prefer this method, but your mileage may vary.

Starting a process using the described method

GDB prompt

Fire up GDB and at the (gdb) prompt use the file command to load the binary and then use the run command to start it with the arguments you want to pass:

$ gdb
(gdb) file exe
(gdb) run argument1 argument2

an alternative to the above would be the use of set args like this:

$ gdb
(gdb) file exe
(gdb) set args argument1 argument2
(gdb) run

Starting a process using the described method

You can also see in any case which arguments run would pass to the started process by issuing a:

(gdb) show args

btw: if you were wondering about environment variables, use GDB's built-in help command as help set and help show. Pointers: set environment VARNAME=VALUE and show environment [VARNAME] and unset environment VARNAME.

Phew, but why does the program stop with a SIGSEGV (segment fault)?

Well, we don't know yet, but it looks like this little beasty wants the proper treatment. Since we practice defensive computing, we don't want to run anything we don't know much about, right? So let's start over. If this would have been malware we'd have to flush the machine and reinstall or restore a snapshot if it's a VM guest.

First we'll want to run the info command as follows:

(gdb) info file

Observe:

info file command in GDB

There are two important pieces of information, the most relevant for us being the line stating:

Entry point: 0x400710

alright, so we can set a break point on that one and then run the process with the arguments we fancy.

.gdbinit for the win

But wait, this is getting tedious already. No easy method to automate these steps in some way? As a matter of fact there is. A file named .gdbinit can be used to issue commands to GDB upon startup. You can also pass a file with GDB commands using the -x argument on the (shell) command line. If I have a number of projects, usually they are in subfolders with a .gdbinit file each.

Side-note: -nx prevents the .gdbinit contents from being executed upon startup.

So we know which arguments we want to pass and we know the address of the break point, this translates to the following .gdbinit file:

file exe
break *0x400710
run argument1 argument2

The output I get when I start gdb without any other arguments is:

Breakpoint 1 at 0x400710

Breakpoint 1, 0x0000000000400710 in ?? ()
(gdb) 

Nice! But this looks different ...

Assembly and GDB

So you're used to see the next line you're going to execute and then your trusty old (gdb) prompt. But no such thing. We have no source for this binary and furthermore symbols. Doh! So we contemplate the blinking caret at the (gdb) prompt and wonder what to do. Don't fret, GDB can also handle assembly code. Only problem, it defaults to the - in my opinion - inconvenient AT&T assembly syntax. I prefer the Intel flavor and the following command tells GDB to do just that:

(gdb) set disassembly-flavor intel

Showings the assembly code

And how is it going to show us the assembly code? Well, similar to the TUI mode (check the tag wiki for ) by using the following command:

(gdb) layout asm

and if you are so inclined, also:

(gdb) layout regs

which will also show you the contents of the registers in an overview.

Let's run it again

So we end up with the following .gdbinit for our purposes:

file exe
break *0x400710
set disassembly-flavor intel
layout asm
layout regs
run argument1 argument2

And when we start gdb without arguments we end up with this:

GDB with asm and regs layout

Sweet. So we can see the disassembly while we step through the code. We could conclude this here, but of course there are more tricks to be learned, so why not go a little further.

NOTE: the registers with white/gray background show that a value has changed. Not too meaningful when we just started the program, but mighty useful when stepping through code later on.

btw, if you prefer to save screen estate

... and have it less visual, starting with GDB 7.0 you can use:

set disassemble-next-line on

on GDB versions before that you can emulate the behavior by settings an automatic display:

display/i $pc

or shorter disp/i $pc where /i is the format, you can remember it best by thinking "instruction" and $pc being the instruction pointer, also known as program counter - hence pc.

Also good to know

Sometimes when stepping through assembly the regs and asm views will get borked. Simply execute the respective layout commands again to restore them to their old glory:

(gdb) layout asm
(gdb) layout regs

"Debugging" at assembly level

Turns out when you are in assembly mode some of the commands you are used to from source-level debugging simply won't work. That makes sense, because a single source line means usually a dozen instructions or more. The next and step commands, however, have instruction-level counterparts:

  • nexti (shorthand ni ... anyone else thinking of shrubbery?)
  • stepi (shorthand si)

From our disassembly above we know:

0x40072d        mov    rdi,0x40f961

and for all practical purposes this is the main function. Of course if you were to reverse engineer malware you should be more careful, but in this case it is. So let's add a breakpoint to this address (0x40f961) instead of the entry point:

break *0x40f961

If we examine (shorthand x) the code at which we are currently, we can see:

(gdb) x/5i $pc
x/5i $pc
=> 0x40f961:    push   rbp
   0x40f962:    mov    rbp,rsp
   0x40f965:    mov    eax,0x0
   0x40f96a:    call   0x40911f
   0x40f96f:    pop    rbp

Okay, the call is what we want to follow, so let's step inside of it using si. We get to see another call immediately at the instruction pointer when entering the function:

(gdb) x/5i $pc
x/5i $pc
=> 0x40911f:    call   0x400b8c
   0x409124:    push   rbp
   0x409125:    mov    rbp,rsp
   0x409128:    push   r10
   0x40912a:    push   r11

The call leads us to a function which calls ptrace(PTRACE_TRACEME, ...), now why would it do that?

0x400bab        call   0x4006b8 <ptrace@plt>

Well, it's an old anti-debugger trick which Mellowcandle has described in another Q&A here:

But how do we get around it? We'd have to overwrite the call to the function which calls ptrace() with nop or something along those lines.

This is where GDB becomes a little unwieldy. But we can use set so do the magic for us. Let's first inspect the instruction bytes:

(gdb) x/10b $pc
x/10b $pc
0x40911f:       0xe8    0x68    0x7a    0xff    0xff    0x55    0x48    0x89
0x409127:       0xe5    0x41

The 0xe8 is a call instruction and we know now that it is 5 bytes long. So let's nop this out. (x/10b $pc means examine 10 bytes at program counter - default format is hex already).

So we do while stopped at 0x40911f:

(gdb) set write
(gdb) set {unsigned int}$pc = 0x90909090
(gdb) set {unsigned char}($pc+4) = 0x90
(gdb) set write off

and verify the patched location:

(gdb) x/10i $pc
x/10i $pc
=> 0x40911f:    nop
   0x409120:    nop
   0x409121:    nop
   0x409122:    nop
   0x409123:    nop
   0x409124:    push   rbp
   0x409125:    mov    rbp,rsp
   0x409128:    push   r10
   0x40912a:    push   r11
   0x40912c:    push   rbx

Excellent. We can now execute it.

Alternatives to the given method

  1. alternative for patching: set {unsigned int}0x40911f = 0x90909090 followed by set {unsigned char}0x409123 = 0x90
  2. Manipulate the program counter (instruction pointer) instead:
    • set $pc+=5 or the more explicit set $pc=$pc+5
    • jump *$pc+5

Better ways yet to manipulate/patch the running program

There are alternative (and way superior) methods like this one by Tavis Ormandy. I'm reproducing the assemble macro below (in case it goes offline from the other place):

define assemble
 # dont enter routine again if user hits enter
 dont-repeat
 if ($argc)
  if (*$arg0 = *$arg0)
    # check if we have a valid address by dereferencing it,
    # if we havnt, this will cause the routine to exit.
  end
  printf "Instructions will be written to %#x.\n", $arg0
 else
  printf "Instructions will be written to stdout.\n"
 end
 printf "Type instructions, one per line.\n"
 printf "End with a line saying just \"end\".\n"
 if ($argc)
  # argument specified, assemble instructions into memory
  # at address specified.
  shell nasm -f bin -o /dev/stdout /dev/stdin \
    <<< "$( echo "BITS 32"; while read -ep '>' r && test "$r" != end; \
                do echo -E "$r"; done )" | hexdump -ve \
        '1/1 "set *((unsigned char *) $arg0 + %#2_ax) = %#02x\n"' \
            > ~/.gdbassemble
  # load the file containing set instructions
  source ~/.gdbassemble
  # all done.
  shell rm -f ~/.gdbassemble
 else
  # no argument, assemble instructions to stdout
  shell nasm -f bin -o /dev/stdout /dev/stdin \
    <<< "$( echo "BITS 32"; while read -ep '>' r && test "$r" != end; \
                do echo -E "$r"; done )" | ndisasm -i -b32 /dev/stdin
 end
end
document assemble
Assemble instructions using nasm.
Type a line containing "end" to indicate the end.
If an address is specified, insert instructions at that address.
If no address is specified, assembled instructions are printed to stdout.
Use the pseudo instruction "org ADDR" to set the base address.
end

Again the above script snippet wasn't written by me, but by Tavis Ormandy - see the link above.

This concludes this little Q&A.

  • 6
    great writeup! just one note: -q as a cmd arg gets rid of the copyright stuff – 0xea Apr 27 '13 at 8:02
  • 9
    I though I knew gdb pretty well, but I learned a lot. Thanks ! – perror Apr 27 '13 at 9:16
  • 6
    This AWESOME answer saved my life more than once. Thank you, and thanks again. – Hackndo Sep 7 '15 at 8:00

Your Answer

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.