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.