Will Reverse Engineer for Food: with GDB peda and IDA – Part I

I am an experienced Vulnerability Researcher and Security Architect with 16+ years of experience in various verticals and horizontals, be it consumer electronics, semiconductors, automotive or other. Having started in software engineer in low-level embedded devices from writing applications to kernel drivers on various operating systems and then moving to my real calling i.e. hacking. Love to stick to the older golden days of game hacking, BBS, shareware, phreaking, phrack, virus era, metal music, cheats and many more such cool stuff from the underground. I wear many hats from time to time as necessary - but I also love to help people and organizations to deal with the core cybersecurity issues and not provide them a checklist with a presentation. Opinions and posts on my site are purely my own and do not reflect my work.
I was searching for new work some 5 years ago, and one of an excellent company sent me a Reverse Engineering challenge for a Linux Binary. This post gives details of how I solved it.
Note: Ghidra was not available at the time and I wanted to do it raw using GDB and dynamic analysis just for fun by looking at assembly. Although I used IDA for understanding program logic better then work in GDB to conduct breakpoint analysis.
There are plenty of binaries out there for using techniques given here – I don’t want to share the one that this particular company sent me.
**Basic Analysis: **
Binary is completely stripped of any symbols or any debugging information – No Information other than strings inside the code is available
Binary has also been compiled with protection mechanism like Canaries, NX(Non-Executable Stack), PIE and RELRO is fully turned on so it is difficult to exploit, other than reverse engineering the password logic.
**Statically Analyzing results: **
Tried to find possible function names, system calls and API’s used by strings
Output of Strings:
#strings crckme-01
/lib64/ld-linux-x86-64.so.2
libc.so.6
strncmp
__stack_chk_fail
strlen
tcsetattr
read
malloc
tcgetattr
sleep
__cxa_finalize
__libc_start_main
write
GLIBC_2.4
GLIBC_2.2.5
_ITM_deregisterTMCloneTable
__gmon_start__
_ITM_registerTMCloneTable
AWAVI
AUATL
[]A\A]A^A_
Crack me!
Password:
+ Checking Password
OK.
Incorrect.
- Try again.
+ DONE
;*3$"
GCC: (Ubuntu 7.3.0-27ubuntu1~18.04) 7.3.0
All is all that can be seen about the binary.
Things we can infer: (First thoughts about the binary)
Does strncmp possibly of inputted password and some string generated inside the code
Read, may be used to read from user from console
Malloc – maybe we are allocating buffer for something, not sure at this point.
– Sleep – this was to insert some delay while password was verified.
– Write to write some data somewhere
– Crack me was the string when you started the program
– Password was the prompt to enter the password
– Tcgetattr – may be used to get current setting of some console/terminal
– +Checking Password, was outputted when password was being verified(May be)
– Tcsetattr – may be used to change tty setting for some terminal
– OK – This is what we are looking for
– Incorrect – This is what I got most of the times before this was solved
– DONE – Signalled end of program
Next this was to identify the program flow, meaning what the program actually did. **#strace ./crckme-01 **
#strace ./crckme-01
**Things we can infer about the flow of the program and general observations: **
execve("./crckme-01", ["./crckme-01"], 0x7ffe09c0a660 /* 43 vars */) = 0
brk(NULL) = 0x561a8b28f000
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=152566, ...}) = 0
mmap(NULL, 152566, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f7f3f24f000
close(3) = 0
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0000,\2\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=1808440, ...}) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f7f3f24d000 mmap(NULL, 1821408, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f7f3f090000
mmap(0x7f7f3f0b2000, 1335296, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x22000) = 0x7f7f3f0b2000 mmap(0x7f7f3f1f8000, 307200, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x168000) = 0x7f7f3f1f8000 mmap(0x7f7f3f243000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1b2000) = 0x7f7f3f243000 mmap(0x7f7f3f249000, 15072, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f7f3f249000 close(3) = 0
arch_prctl(ARCH_SET_FS, 0x7f7f3f24e4c0) = 0
mprotect(0x7f7f3f243000, 16384, PROT_READ) = 0
mprotect(0x561a8a8de000, 4096, PROT_READ) = 0
mprotect(0x7f7f3f29c000, 4096, PROT_READ) = 0
munmap(0x7f7f3f24f000, 152566) = 0
write(1, "Crack me!\n\n", 11Crack me!
) = 11
write(1, "Password:", 9Password:) = 9
brk(NULL) = 0x561a8b28f000
brk(0x561a8b2b0000) = 0x561a8b2b0000
ioctl(0, TCGETS, {B38400 opost isig icanon echo ...}) = 0
ioctl(0, TCGETS, {B38400 opost isig icanon echo ...}) = 0
ioctl(0, SNDCTL_TMR_CONTINUE or TCSETSF, {B38400 opost isig icanon -echo ...}) = 0
ioctl(0, TCGETS, {B38400 opost isig icanon -echo ...}) = 0
read(1, "khjasdasd\n", 32) = 10
ioctl(0, TCGETS, {B38400 opost isig icanon -echo ...}) = 0
ioctl(0, SNDCTL_TMR_CONTINUE or TCSETSF, {B38400 opost isig icanon echo ...}) = 0
ioctl(0, TCGETS, {B38400 opost isig icanon echo ...}) = 0
write(1, "\n+ Checking Password ", 21
+ Checking Password ) = 21
write(1, ".", 1.) = 1
nanosleep({tv_sec=1, tv_nsec=0}, 0x7ffc062d4430) = 0
write(1, ".", 1.) = 1
nanosleep({tv_sec=1, tv_nsec=0}, 0x7ffc062d4430) = 0
write(1, ".", 1.) = 1
nanosleep({tv_sec=1, tv_nsec=0}, 0x7ffc062d4430) = 0
write(1, ".", 1.) = 1
nanosleep({tv_sec=1, tv_nsec=0}, 0x7ffc062d4430) = 0
write(1, ".", 1.) = 1
nanosleep({tv_sec=1, tv_nsec=0}, 0x7ffc062d4430) = 0
write(1, "\n Incorrect.\n- Try again.\n", 26
Incorrect.
- Try again.
) = 26
write(1, "+ DONE\n", 7+ DONE
) = 7
exit_group(0) = ?
+++ exited with 0 +++
– Program starts by forking from bash shell process
– Prints “Crack Me” on stdout (1)
– Prints “Password:” as prompt for user to insert his password
– Write is the function that does so with “write(1, “”, strlen(string) + 1); (+1 for ‘\0’)
– We have an ioctl call as seen from above output, but as per investigation and strings output, no ioctl call is present in application, rather, tcgetattr is used with termios structure to be filled. Looks like it is taking stdin (0) line .
– Then the code blocks at read system call waiting for user input. ( read(1, “khjasdasd\n”, 32) – size of buffer to hold password entered by us is 32 – may be the malloc earlier was used to create this buffer dynamically ? )
– Tcsetattr is called with some change in line settings, like echo is turned off and is set for stdin (0)
– Then the program goes into verification of password by saying “+ Checking Password” , then prints a “.” Every 1 sec (nanosleep({tv_sec=1, tv_nsec=0}, 0x7ffc062d4430))
– Furthermore, the program jumps to some kind of password verification mechanism and then goes out print corresponding success “OK” or Incorrect – Try again on console.
**Diving into Program assembly code: **
First I tried to reverse engineer the program with just GDB and it was really painful without some kind of debugging information in the binary itself, all I could is try to reverse based on hex addresses and identifying function start and end addresses, classify or name them, identify points of interest. It was getting really difficult.
Some futile attempts can be seen here.
Then I said let’s bring out the machine gun → IDA Pro, and yeah.
**IDA – The Saviour **
After loading the program into IDA and number of things sorted out real fast.
– Program assembly addresses got converted into function names
– All functions were properly quantized, meaning, function start and end was easy to understand
– IDA assigned names to variables which made it easier to follow
– Program logic was evident
– Data and Text Sections were properly formatted.
Let’s see what I mean:
Main Function now looked like this:
As we predicted from strac flow, it seemed to match correctly with the graph from IDA. Now we needed to understand working of the other functions.
Password Verification logic in main: This routing verifies the password entered by user using a predefined value and jumps to corresponding conditions based on the outcome.
Write Function: This function writes the output to the console.
Termios Structure:
– This function defines a local variable of type struct termios and then passes it to tcgetattr function,which fills this structure with current tty values of stdin.
– Next it saves all the values into local variables of another function, changes the value of c_lflag and then does a tcsetattr
– Then it goes and asks the user to enter the password from read function call, and supplies the buffer allocated from the previous malloc call.
Termios Structure Contd :
Possible Encrytion Routine:
After running each and every instruction in GDB and then correlating their actions per the above graphs, the program flow was pretty evident.
Final Blow:
– It was found that there is a strncmp which compares two strings, s1 and s2, s2 is the string which never changes and is the value against which our password is compared, hence, and it must be our password.
– S1 is the string entered by us, over which some kind of logic is performed as seen from above diagrams. – As we cannot see the value of s2 in any of the code, it must be dynamically generated by some algorithm at runtime, which finally appears in the string s2 right before the comparison happens.
– Actually password cracking happened after adding a breakpoint at the correct instruction of comparison right before the strncmp call, which GDB peda shows the arguments to (check video)
–
– Now let’s see how it looks in GDB
– Argument 0 and argument 1 to the strncmp function shown above
– Arg[0] – Password Entered by us – char *s1 for strncmp function
– Arg[1] – Password Dynamically generated by some seed value inside the code – char *s2 for strncmp function
Now let’s see the working use case
Now both argv[0] and argv[1] match.
Initially I tried to reverse engineer the Encryption logic, but it was taking too much time.
Some points on the Encryption or Password generation routing:
– I observed these values for a long time
– All 32bits or 8bytes of the password had to match
– If I entered ‘A’ as the first character, it would map to 0x62 always, but if I entered it as a second character of the password it generated some new value
– It started me thinking that the encryption routing was something like the Enigma machine, as for every entered character it would generate different output if the position of entered character was different than what was in the previous run, this scared me. But after several trials. (It gave a feel like Enigma, but not really Enigma).
– I made relationship map of every character to its corresponding Hex character, for every position.
– After repeating this process, I could map all character to its corresponding hex value and solve the challenge. – So the logic to generate password may be some kind of rotational cipher which used a seed value and may be, is a function of current position. Couldn’t be sure about this.
Password was “ABCtEFG”
A Small video is available here for the bare bones approach.
Will Reverse Engineer for Food – Give me some GDB
I was asked to send response to this challenge in 48 hours – It was quite ecstatic when I was somehow able to do it under 12 hours.
Ghidra was Not available when i attempted this challenge.
The actual Algorithm – It was just a simple xor with some constant value
In Continuation to the Linux Reverse Engineering challenge – The Encryption Algorithm is nothing but an Xor operation between the Password in Hex Format, initialized and seen in Data Segment of the Binary.
Figure 1: Read-Only Initialized data segment of the program (unk_C78 from IDA Pro, show the password here)
The Encryption Algorithm: – Performs Xor Operation on characters entered by us from console and stores it in s1 (Figure 2)
Figure 2: Main Function Program Flow
– It takes the password from data section and performs an XOR with the password entered by user by taking 1bytes at a time, reversing it, shifting it to get the endianess and then send it for comparison.
– XOR operation can be seen below(Figure 3)
Figure 3: Rotation and Xor Logic (esi and ecx, contain our xor’ed seed value and user entered seed value)
Seed Value XOR Key Password
Figure 4: Formula
Hence,
Seed Value = 0x2389F2C26D3EC7B2 (As available in data section)
Little Endian = 0xB2C73E6DC2F28923
Seed Value when Xor’ed with password = 0xB2807828B6B1CB62 (Little Endian)
Hence the password should be, Password = Seed Value Xor’ed with 0xB2807828B6B1CB62 (As seen in Figure 5 – retrieved via Dynamic Analysis)
Figure 5: Final BreakPoint Output
Therefore, password = 0xB2C73E6DC2F28923 XOR 0xB2807828B6B1CB62 = 0x47464574434241 (Little Endian) So if we convert this into Ascii then we get “GFEtCBA”, now we reverse this to get into console format, as data is stored in reverse order for X86 systems, we get the PASSWORD = “ABCtEFG”.
It was fun to do this before the allocated time.
Unfortunately, I was offered the job but due a hiring freeze all the offers at the time were taken back. I stayed in touch with the technical manager for 4-5 months, then he also left that position so this did not materialise.
I’m Still hungry and foolish now \m/




