7

So I am on a Linux box with an executable.Vague - I know. This is what happens when I try and run it with an argument/parameter:

me@there:~$ ./theFile theOption -a
./theFile : option requires an argument -- 'a'
usage: theOption [parameters]

My colleague said just use objdump to reverse theFile first part of the main function.

My question is whether it's possible to find what parameters can be passed to the program. I have the name of some functions like do_this and do_that, but insure how to pass these to the executable.

closed as off-topic by usr2564301, perror, jvoisin, Vitaly Osipov, NirIzr Jan 22 '17 at 15:32

This question appears to be off-topic. The users who voted to close gave this specific reason:

  • "Questions asking for help reverse-engineering a specific system are off-topic unless they demonstrate an understanding of the concepts involved and clearly identify a specific problem." – usr2564301, perror, jvoisin, Vitaly Osipov, NirIzr
If this question can be reworded to fit the rules in the help center, please edit the question.

12

It is possible to determine what command line arguments or options can be passed to a Linux executable. Of course, how this can be done will depend on the type and design of the program and on factors such as obfuscation, encryption, compression, etc.

Hardcoded Documentation

Linux executables designed to be easily usable by humans and whose behavior changes depending on what command line arguments they receive, such as ls, cat, grep etc., are typically ELF binaries and have documentation of their usage hardcoded in the .rodata section. This data can be examined using readelf -x .rodata ELF_BINARY_NAME.

Example:

  $ readelf -x .rodata /bin/ls | less
                               .
                         <lots of data>
                               .
  0x00413e90 20202d61 2c202d2d 616c6c20 20202020   -a, --all     
  0x00413ea0 20202020 20202020 20202020 20646f20              do 
  0x00413eb0 6e6f7420 69676e6f 72652065 6e747269 not ignore entri
  0x00413ec0 65732073 74617274 696e6720 77697468 es starting with
  0x00413ed0 202e0a20 202d412c 202d2d61 6c6d6f73  ..  -A, --almos
  0x00413ee0 742d616c 6c202020 20202020 20202020 t-all           
  0x00413ef0 646f206e 6f74206c 69737420 696d706c do not list impl
  0x00413f00 69656420 2e20616e 64202e2e 0a202020 ied . and ...   
                               .
                        <even more data>

This means that even if there is no man page for the program on your system, no README files, and no source code, you can still look at how it is documented internally manually.

This pertains specifically to the example provided in the question:

This is what happens when I try and run it with an argument/parameter:

me@there:~$ ./theFile theOption -a
./theFile : option requires an argument -- 'a'
usage: theOption [parameters]

What is happening in this example is that some command line option is passed to the executable and the executable then prints some strings about proper usage to STDOUT. The implication of this in the context of deducing valid command line arguments is that there are hardcoded strings in the executable that document proper usage that can be located using static analysis.

No Hardcoded Documentation

Part 1: Static Analysis

We can take advantage of program design considerations made when designing Linux userspace applications that accept command line arguments to identify what arguments the program expects when analyzing the binary. For example, what mechanisms exist to parse n-many command line arguments passed to the program when executing it? How is a program designed such that it can successfully determine what arguments are valid and what arguments are invalid? What if 1,000 arguments are passed? The design of programs that process command line arguments must reflect the fact that such input can be arbitrary.

A common design pattern employed in a Linux environment to address this is using a combination of the GNU C library function getopt() and a switch construct inside a loop. getopt() parses the command-line arguments and the parsed argument is then evaluated by the switch construct. This can be used as a heuristic when analyzing the binary.

To illustrate this, we can analyze a "mystery" ELF binary that processes command line arguments.

$ file mystery_program 
mystery_program: ELF 64-bit LSB  executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=3286c11349d82c58257a14e4d174b9ec5f313714, stripped

This program is dynamically linked, which means that even though it is stripped it still contains information about what libraries are linked and loaded at runtime and also what functions in these libraries the program uses. Since getopt() is a glibc (shared library or .so*) function, if our dynamically-linked binary uses it then an entry in the .dynsym section will include information about it. Entry 21 in the output of readelf --dyn-syms mystery_program confirms that getopt() is indeed used in the program:

 $ readelf --dyn-syms mystery_program 

Symbol table '.dynsym' contains 79 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __uflow@GLIBC_2.2.5 (2)
     2: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND getenv@GLIBC_2.2.5 (2)
     3: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND free@GLIBC_2.2.5 (2)
     4: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND abort@GLIBC_2.2.5 (2)
     5: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __errno_location@GLIBC_2.2.5 (2)
     6: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND strncmp@GLIBC_2.2.5 (2)
     7: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND _exit@GLIBC_2.2.5 (2)
     8: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND strcpy@GLIBC_2.2.5 (2)
     9: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __fpending@GLIBC_2.2.5 (2)
    10: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND iconv@GLIBC_2.2.5 (2)
    11: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND iswcntrl@GLIBC_2.2.5 (2)
    12: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND write@GLIBC_2.2.5 (2)
    13: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND textdomain@GLIBC_2.2.5 (2)
    14: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND fclose@GLIBC_2.2.5 (2)
    15: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND bindtextdomain@GLIBC_2.2.5 (2)
    16: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND stpcpy@GLIBC_2.2.5 (2)
    17: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND dcgettext@GLIBC_2.2.5 (2)
    18: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __ctype_get_mb_cur_max@GLIBC_2.2.5 (2)
    19: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND strlen@GLIBC_2.2.5 (2)
    20: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __stack_chk_fail@GLIBC_2.4 (3)
==> 21: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND getopt_long@GLIBC_2.2.5 (2)
    22: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND mbrtowc@GLIBC_2.2.5 (2)
    23: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND strchr@GLIBC_2.2.5 (2)
    24: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND strrchr@GLIBC_2.2.5 (2)
<output snipped>

The version of getopt() being used in this program is getopt_long(). To see in what context getopt_long() is called, we can use objdump:

$ objdump -dj .text mystery_program | grep getopt -B10 -A25
  401aa6:   c6 84 24 80 00 00 00    movb   $0x0,0x80(%rsp)
  401aad:   00 
  401aae:   c6 84 24 84 00 00 00    movb   $0x0,0x84(%rsp)
  401ab5:   00 
  401ab6:   c6 44 24 57 00          movb   $0x0,0x57(%rsp)
  401abb:   8b 7c 24 58             mov    0x58(%rsp),%edi
  401abf:   45 31 c0                xor    %r8d,%r8d
  401ac2:   b9 00 90 40 00          mov    $0x409000,%ecx
  401ac7:   ba 23 8f 40 00          mov    $0x408f23,%edx
  401acc:   48 89 de                mov    %rbx,%rsi
  401acf:   e8 3c fc ff ff          callq  401710 <getopt_long@plt>          <===
  401ad4:   83 f8 ff                cmp    $0xffffffff,%eax
  401ad7:   0f 84 10 01 00 00       je     401bed <__sprintf_chk@plt+0x1bd>
  401add:   83 f8 62                cmp    $0x62,%eax
  401ae0:   0f 84 f3 00 00 00       je     401bd9 <__sprintf_chk@plt+0x1a9>
  401ae6:   7e 2c                   jle    401b14 <__sprintf_chk@plt+0xe4>
  401ae8:   83 f8 73                cmp    $0x73,%eax
  401aeb:   0f 1f 44 00 00          nopl   0x0(%rax,%rax,1)
  401af0:   0f 84 d6 00 00 00       je     401bcc <__sprintf_chk@plt+0x19c>
  401af6:   7e 6a                   jle    401b62 <__sprintf_chk@plt+0x132>
  401af8:   83 f8 75                cmp    $0x75,%eax
  401afb:   0f 1f 44 00 00          nopl   0x0(%rax,%rax,1)
  401b00:   74 b9                   je     401abb <__sprintf_chk@plt+0x8b>
  401b02:   7c 72                   jl     401b76 <__sprintf_chk@plt+0x146>
  401b04:   83 f8 76                cmp    $0x76,%eax
  401b07:   0f 85 d6 00 00 00       jne    401be3 <__sprintf_chk@plt+0x1b3>
  401b0d:   c6 44 24 53 01          movb   $0x1,0x53(%rsp)
  401b12:   eb a7                   jmp    401abb <__sprintf_chk@plt+0x8b>
  401b14:   83 f8 41                cmp    $0x41,%eax
  401b17:   74 34                   je     401b4d <__sprintf_chk@plt+0x11d>
  401b19:   7f 19                   jg     401b34 <__sprintf_chk@plt+0x104>
  401b1b:   3d 7d ff ff ff          cmp    $0xffffff7d,%eax
  401b20:   74 64                   je     401b86 <__sprintf_chk@plt+0x156>
  401b22:   3d 7e ff ff ff          cmp    $0xffffff7e,%eax
  401b27:   0f 85 b6 00 00 00       jne    401be3 <__sprintf_chk@plt+0x1b3>
  401b2d:   31 ff                   xor    %edi,%ed

The call to getopt_long() at address 401acf is followed by disassembly of a jump table, indicative of the presence of a switch sequence. The jump table is much longer but what is displayed here is enough to get the idea. If we look more closely at the jump table we see that hex values that look like ASCII codes are compared to %eax, which by convention holds the return value of getopt_long():

  .
  .
  401add:   83 f8 62                cmp    $0x62,%eax       # 0x62 is '>'
  .
  .
  401ae8:   83 f8 73                cmp    $0x73,%eax       # 0x73 is 'I'
  .
  .  
  401af8:   83 f8 75                cmp    $0x75,%eax       # 0x75 is 'K'
  .
  .
  401b04:   83 f8 76                cmp    $0x76,%eax       # 0x76 is 'L'
  .
  .
  401b14:   83 f8 41                cmp    $0x41,%eax       # 0x41 is ')'
  .
  .
  401b34:   83 f8 45                cmp    $0x45,%eax       # 0x45 is '-'
  .
  .
  401b39:   83 f8 54                cmp    $0x54,%eax       # 0x54 is '6'
  .
  .
<more comparisons>

This program is parsing command line arguments and its execution path branches based on whatever ASCII characters it has specific conditionals for. While this is not going to immediately yield what arguments are valid and what arguments are not valid, it is a start. Static analysis with Linux binutils (and my limited knowledge and skills) can only get us so far.

Part 2: Dynamic Analysis

There are two factors that are quite helpful in dynamically analyzing this "mystery" binary in order to determine what the valid command line arguments are.

The first is that even though this binary is stripped, getopt_long() is in a shared library, and in order to be dynamically linked to the binary some symbol information about getopt_long() must be preserved. This is why when objdump was used to statically analyze some dissasembled code from this stripped binary getopt_long() was still referred to by name. This means that we have the exact address in the binary where getopt_long() is called, which allows us to set a break point at that address without having to rummage around in the binary for hours and hours. In other words, we do not need to know in what function getopt_long() is called, whether in main() or some other function; we can just let the program execute until the break point at that address is hit. As a quick reminder, getopt_long() was called at address 401acf:

 $ objdump -dj .text mystery_program | grep getopt
   401acf:  e8 3c fc ff ff          callq  401710 <getopt_long@plt>

The second is that a string composed of all valid command line options is passed to getopt() when it is called, as we see in its function prototypes. From the synopsis in its man page:

#include <unistd.h>

int getopt(int argc, char * const argv[],
           const char *optstring);

extern char *optarg;
extern int optind, opterr, optopt;

#include <getopt.h>

int getopt_long(int argc, char * const argv[],
                const char *optstring,
                const struct option *longopts, int *longindex);

int getopt_long_only(int argc, char * const argv[],
                     const char *optstring,
                     const struct option *longopts, int *longindex);

Of these three prototypes, the prototype for getopt_long() is the one of interest in this particular case. According to the prototype, getopt_long() is passed a pointer to the options string as well as a pointer to a struct holding long options. This means that both the single-character options as well as option strings can be discovered by investigating these pointers at the point in the program getopt_long() is called. gdb can be used to perform the analysis:

$ gdb -q mystery_program
Reading symbols from mystery_program...(no debugging symbols found)...done.
(gdb) break *0x401acf
Breakpoint 1 at 0x401acf
(gdb) run
Starting program: mystery_program 

Breakpoint 1, 0x0000000000401acf in ?? ()
(gdb) x/i $rip
=> 0x401acf:    callq  0x401710 <getopt_long@plt>
(gdb)

As is demonstrated here, it is not necessary to know where main() is or even what the program entry point is.

This is the point at which getopt_long() is called. Time to investigate the registers.

(gdb) info registers 
rax            0x0  0
rbx            0x7fffffffe138   140737488347448
rcx            0x409000 4231168
rdx            0x408f23 4230947
rsi            0x7fffffffe138   140737488347448
rdi            0x1  1
rbp            0x1000   0x1000
rsp            0x7fffffffdee0   0x7fffffffdee0
r8             0x0  0
r9             0x2  2
r10            0x7fffffffdca0   140737488346272
r11            0x7ffff7a51410   140737348178960
r12            0x402602 4204034
r13            0x7fffffffe130   140737488347440
r14            0x0  0
r15            0x0  0
rip            0x401acf 0x401acf
eflags         0x246    [ PF ZF IF ]

%rcx and %rdx look interesting.

(gdb) x/s $rdx
0x408f23:   "benstuvAET"

That's the option character string, composed of all character options that the program uses. The options are -b, -e, -n, -s, etc.

What about the long options?

(gdb) x/s $rcx
0x409000:   "\225\217@"        # oops, that is not a string
(gdb) x/xg $rcx
0x409000:   0x0000000000408f95
(gdb) x/s 0x0000000000408f95
0x408f95:   "number-nonblank"
(gdb)

So one long option is "--number-nonblank".

(gdb) x/s 0x0000000000408f95+64
0x408fd5:   "show-tabs"
(gdb) 

Another is "--show-tabs".

And so on.

Final Thoughts

If you have reached this point and have not died of boredom, congrats. It turns out that this "mystery" program is actually cat, a well-known Linux utility. In this particular case it was possible to discover what command line options the program was expecting.

It is important to note that throughout the entire course of this example analysis the arguments/options passed to mystery_program/cat were never examined. When a user passes options x, y and z to some program and then analyzes it, it is not useful to then go to main() and then look at the strings in *argv[] since they are already known: argv[0] points to the program name, argv[1] points to the first argument after the program name, x, etc.

My colleague said just use objdump to reverse theFile first part of the main function

Investigating main() is done to recover what arguments were passed to the program when it was executed, not to determine what options a program will actually use. As was illustrated in the example, that information lies elsewhere in the program.

1

As I don't know much about your executable, I would first run strings with your program to see if anything comes up: this work if the binary has a description.
(I'm stuck on a Windows now, but strings seem to work in the same exact way it does on Linux)

{ ~ }  » strings.exe /bin/ls.exe  
...
Usage: %s [OPTION]... [FILE]...
List information about the FILEs (the current directory by default).
Sort entries alphabetically if none of -cftuvSUX nor --sort is specified.
Mandatory arguments to long options are mandatory for short options too.
  -a, --all                  do not ignore entries starting with .
  -A, --almost-all           do not list implied . and ..
      --author               with -l, print the author of each file
  -b, --escape               print C-style escapes for nongraphic characters
      --block-size=SIZE      scale sizes by SIZE before printing them; e.g.,
                               '--block-size=M' prints sizes in units of
                               1,048,576 bytes; see SIZE format below
  -B, --ignore-backups       do not list implied entries ending with ~
...

In this case those strings are part of the binary and are used when parameters are not valid or missing. If that fails, can't you just run it with some random values and run ltrace (or strace if you want to take a closer look) and guess what the possible parameters are ?

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