0xf00's-Shiftr
In this reverse engineering challenge we will see how we can idenitfy the behavior of functions, resolving dependencies and communication between processes.
Link to the Challenge: https://crackmes.one/crackme/675ad67560fa67152406b81c
My Linkedin Profile: https://www.linkedin.com/in/deepak-bhardwaj-aa8543143/
My crackme’s profile: https://crackmes.one/user/anon786
This challenge was also fine, we need to make sure we understand what the underlying function does and why exactly it is getting used, if we observe carefully we would get to know the inner workings of it by analyzing the decompiled binary code and looking into the flow of the code, so lets start analyzing the functions we will use ghidra for getting the decompiled version of the functions:
Observe that its an ELF binary, with the symbols as stripped.
Let’s run the binary and see what it expects, observe that it expects us to enter a string if incorrect it prompts for 3 times and it also expects only 8 characters on input string:
So, when we look at the decompiled code, we would observed that we are in entry point of the binary, where the helper function “_libc_start_main” invokes the “main” function, in our case the main function is “FUN_00101d9b”.
# Analysis of “FUN_00101d9b”:
Lets analyze the working of the function “FUN_00101d9b”:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
undefined8 FUN_00101d9b(void)
{
int iVar1;
undefined4 uVar2;
int iVar3;
char *pcVar4;
size_t sVar5;
size_t sVar6;
undefined8 uVar7;
long in_FS_OFFSET;
int local_d0;
int local_cc;
char local_a8 [32];
char local_88 [104];
long local_20;
local_20 = *(long *)(in_FS_OFFSET + 0x28);
FUN_00101480();
iVar1 = FUN_00101be6();
if (iVar1 == 0) {
FUN_00101b5c(local_a8,8);
uVar2 = FUN_00101d51(local_a8);
FUN_00101976(local_a8,8,uVar2);
FUN_00101480();
local_cc = 0;
while (local_cc < 3) {
printf("$: ");
pcVar4 = fgets(local_88,100,stdin);
if (pcVar4 == (char *)0x0) {
uVar7 = 1;
goto LAB_001020f1;
}
sVar5 = strcspn(local_88,"\n");
local_88[sVar5] = '\0';
sVar5 = strlen(local_88);
sVar6 = strlen(local_a8);
if (sVar6 < sVar5) {
fwrite("Too long! Try again.\n",1,0x15,stderr);
FUN_00101469();
}
else {
iVar1 = FUN_00101c7c(local_88,local_a8);
if (iVar1 != 0) {
puts("Nicely done! ");
FUN_00101d37();
uVar7 = 0;
goto LAB_001020f1;
}
local_cc = local_cc + 1;
printf("Wrong! %d left.\n",(ulong)(3 - local_cc));
sleep(1);
}
}
uVar7 = 1;
}
else {
FUN_00101b5c(local_a8,8);
uVar2 = FUN_00101d51(local_a8);
FUN_00101536(local_a8,8,uVar2);
local_d0 = 0;
while (local_d0 < 3) {
printf("$: ");
pcVar4 = fgets(local_88,100,stdin);
if (pcVar4 == (char *)0x0) {
uVar7 = 1;
goto LAB_001020f1;
}
sVar5 = strcspn(local_88,"\n");
local_88[sVar5] = '\0';
sVar5 = strlen(local_88);
sVar6 = strlen(local_a8);
if (sVar6 < sVar5) {
fwrite("Too long! No buffer overflow here.\n",1,0x23,stderr);
FUN_00101469();
}
else {
iVar1 = FUN_00101d51(local_88);
iVar3 = FUN_00101d51(local_a8);
if (iVar1 == iVar3) {
sleep(0x32);
puts("is it real? Nah");
uVar7 = 0;
goto LAB_001020f1;
}
local_d0 = local_d0 + 1;
printf("Wrong! %d left.\n",(ulong)(3 - local_d0));
sleep(1);
}
}
uVar7 = 1;
}
LAB_001020f1:
if (local_20 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return uVar7;
}
So, we could see that first function call is to this function “FUN_00101480()”, if we would go through this function it can be observed that this function is just printing the strings on the standard output.
It can be observed that next function call is to this function “FUN_00101be6”: iVar1 = FUN_00101be6();
If we go through the next function call “FUN_00101be6()” we would observe that it is dynamically loading a library “lib.so.6” using the dlopen() function and further using the dlsym() function it is loading the address of the symbol(function) in that library, in this case it is loading the address of the “ptrace” function from the library “lib.so.6”, and further using that function address it is trying to attach the “ptrace” to the current executing program by calling it, to determine that if the “ptrace” is already attached to the running program or not. If the “ptrace” is already attached to the executing program by the user, the executing code will not be able to attach it to itself and hence -1 would be returned and if the running process succeeds in attaching the ptrace to the itself 0 gets returned (this is technique to determine if “ptrace” was attached during execution by the user).
So, if the program is not being debugged (if “ptrace” is not attached to this program by the user), execution will go into the first if statement, so till now the analysis is something like this:
1
2
3
4
5
6
7
8
9
10
11
12
undefined8 FUN_00101d9b(void)
{
FUN_00101480();
iVar1 = FUN_00101be6();
if (iVar1 == 0) {
// execution will continue from here inside "if" statement if "ptrace" is not attached already into the program.
}
else
{
// execution will continue inside this "else" statement, if "ptrace" is attached by user into this program.
}
}
If we observe that “else” condition after some function calls using the “local_a8” array as argument (this might be the array to which the user input might get compared), the user input is being taken from the “stdin”, and getting stored in the “local_88” array.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
else {
FUN_00101b5c(local_a8,8);
uVar2 = FUN_00101d51(local_a8);
FUN_00101536(local_a8,8,uVar2);
local_d0 = 0;
while (local_d0 < 3) {
printf("$: ");
pcVar4 = fgets(local_88,100,stdin);
if (pcVar4 == (char *)0x0) {
uVar7 = 1;
goto LAB_001020f1;
}
sVar5 = strcspn(local_88,"\n");
local_88[sVar5] = '\0';
sVar5 = strlen(local_88);
sVar6 = strlen(local_a8);
if (sVar6 < sVar5) {
fwrite("Too long! No buffer overflow here.\n",1,0x23,stderr);
FUN_00101469();
}
else {
iVar1 = FUN_00101d51(local_88);
iVar3 = FUN_00101d51(local_a8);
if (iVar1 == iVar3) {
sleep(0x32);
puts("is it real? Nah");
uVar7 = 0;
goto LAB_001020f1;
}
local_d0 = local_d0 + 1;
printf("Wrong! %d left.\n",(ulong)(3 - local_d0));
sleep(1);
}
}
uVar7 = 1;
}
Observe that later after taking user input, both the array’s length gets compared and if the user passed input is less than or equal to the “local_a8” array then both array’s gets passed to a function “FUN_00101d51” which just creates an integer value based on the characters present in the array which was passed as input to this function:
1
2
3
4
5
6
7
8
iVar1 = FUN_00101d51(local_88);
iVar3 = FUN_00101d51(local_a8);
if (iVar1 == iVar3) {
sleep(0x32);
puts("is it real? Nah");
uVar7 = 0;
goto LAB_001020f1; // This is exit label
}
But if we see that even after both the passed array as input to function “FUN_00101d51” returns same result than as well it just jumps to the exit condition which is return from the main function. So, when the user attaches debugger to this program, this challenge will not be solved.
So, lets switch our focus only inside “if” condition to solve this challenge:
1
2
3
4
5
if (iVar1 == 0) {
FUN_00101b5c(local_a8,8);
uVar2 = FUN_00101d51(local_a8);
FUN_00101976(local_a8,8,uVar2);
FUN_00101480();
It could be seen that first function call after the “if” condition is “FUN_00101b5c”, lets analyze this function: FUN_00101b5c(local_a8,8);
# Analysis of “FUN_00101b5c”:
It can be observed that current system time is being fetched into the variable “tVar3” using the time() function. And using the current “pid” of the executing program as seed for the random function, the rand value is stored in “iVar2” variable which is further used to take any random value between 0 to 99.
Using this line the output is getting stored in “param_1”, at least 8 characters hex value of the current system time and then 2 integer values, and once the “local_a8” array is populated with these values, function call is returned back to main.
snprintf(param_1,param_2 + 1,"%08lx%02d",tVar3,(ulong)(uint)(iVar2 % 100));
After this function call which is inside “if” statement, another function call happens to the function “FUN_00101d51”: uVar2 = FUN_00101d51(local_a8);
If we observe the function “FUN_00101d51” it just returns an integer that is calculated based on the characters in the “local_a8” array, the returned integer gets stored in the “uVar2” variable.
The next function call is to the function “FUN_00101976”, where “local_a8” and “uVar2” were passed as arguments: FUN_00101976(local_a8,8,uVar2);
Let’s analyze the function “FUN_00101976”.
# Analysis of “FUN_00101976”:
The decompiled version of the function from ghidra is:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
void FUN_00101976(long param_1,ulong param_2,uint param_3)
{
char cVar1;
int iVar2;
int iVar3;
ushort **ppuVar4;
uint uVar5;
char cVar6;
ulong local_20;
for (local_20 = 0; local_20 < param_2; local_20 = local_20 + 1) {
iVar2 = (int)(param_3 + local_20) + (int)((param_3 + local_20) / 0x5e) * -0x5e;
cVar1 = *(char *)(local_20 + param_1);
ppuVar4 = __ctype_b_loc();
if (((*ppuVar4)[cVar1] & 0x800) == 0) {
ppuVar4 = __ctype_b_loc();
if (((*ppuVar4)[cVar1] & 0x400) == 0) {
uVar5 = (iVar2 + cVar1) - 0x21;
*(char *)(local_20 + param_1) = (char)uVar5 + (char)(uVar5 / 0x5e) * -0x5e + '!';
}
else {
ppuVar4 = __ctype_b_loc();
if (((*ppuVar4)[cVar1] & 0x200) == 0) {
cVar6 = 'A';
}
else {
cVar6 = 'a';
}
ppuVar4 = __ctype_b_loc();
if (((*ppuVar4)[cVar1] & 0x200) == 0) {
iVar3 = 0x41;
}
else {
iVar3 = 0x61;
}
uVar5 = (cVar1 - iVar3) + iVar2;
*(char *)(local_20 + param_1) = cVar6 + (char)uVar5 + (char)(uVar5 / 0x1a) * -0x1a;
}
}
else {
uVar5 = (iVar2 + cVar1) - 0x30;
*(char *)(local_20 + param_1) = (char)uVar5 + (char)(uVar5 / 10) * -10 + '0';
}
}
return;
}
It can be observed this function is performing some operation on the characters of the array passed “local_a8”, this function is using “__ctype_b_loc()” this function returns a pointer to a table for character classifications (e.g., whether the character is a digit, letter, etc.). Depending on whether the character is a letter (uppercase or lowercase) or a digit, it applies different transformations. The end result is a modified version of the input buffer where each character has been adjusted according to its classification (letter, digit, etc.) and the calculated transformations. The resulted string is modified/obfuscated.
Now, if we look at the “if” condition again, user input is getting stored in the array “local_88” after taking input from “stdin”, the program asks for input 3 times, if the input string is incorrect as observed in the while loop.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
while (local_cc < 3) {
printf("$: ");
pcVar4 = fgets(local_88,100,stdin);
if (pcVar4 == (char *)0x0) {
uVar7 = 1;
goto LAB_001020f1;
}
sVar5 = strcspn(local_88,"\n");
local_88[sVar5] = '\0';
sVar5 = strlen(local_88);
sVar6 = strlen(local_a8);
if (sVar6 < sVar5) {
fwrite("Too long! Try again.\n",1,0x15,stderr);
FUN_00101469();
}
else {
iVar1 = FUN_00101c7c(local_88,local_a8);
if (iVar1 != 0) {
puts("Nicely done! ");
FUN_00101d37();
uVar7 = 0;
goto LAB_001020f1;
}
local_cc = local_cc + 1;
printf("Wrong! %d left.\n",(ulong)(3 - local_cc));
sleep(1);
}
}
uVar7 = 1;
And after getting the input string from user using the function “FUN_00101c7c” it is getting compared with the string stored in array “local_a8” if both the strings match then this challenge will be solved.
1
2
3
4
5
6
7
8
else {
iVar1 = FUN_00101c7c(local_88,local_a8);
if (iVar1 != 0) {
puts("Nicely done! ");
FUN_00101d37();
uVar7 = 0;
goto LAB_001020f1;
}
Decompiled version of function “FUN_00101c7c”:
So, if we will be able to make the array contents same as “local_a8”, as we understood how it is getting created and we paas the contents of our string same as “local_a8” then we can solve this challenge.
Note: But the string “local_a8” consists of the current time of the system as hex as observed in the function “FUN_00101b5c”. To make sure both the strings match (which this binary will generate and which we pass as the input). So we need to make sure to generate the input string at the same system time when this program gets created. There might be a slight delay sometimes to reach that execution point at the same time (for this challenge binary and the solution program that we will create), but sometimes it might hit at the same time. So let’s write the solution program to generate the array the user input string with same logic which was used to generate “local_a8” array.
# Creation of Solution Program
This is the solution program using the same logic that was used to generate “local_a8”. One thing to note there is that we have used “extern” keyword to provide context to the linker that this function definition “unsigned short int **__ctype_b_loc(void);” exist somewhere else as it is part of the standard library, so at the linking phase linker will resolve this dependency.
This program is named as “test.c”, we can compile and make a final executable using:
gcc test.c -o test
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
#include<stdio.h>
#include<time.h>
#include<stdbool.h>
extern unsigned short int **__ctype_b_loc(void);
typedef unsigned int uint;
typedef unsigned long ulong;
typedef unsigned short int ushort;
void fun3(char* param_1,ulong param_2,uint param_3)
{
char cVar1;
int iVar2;
int iVar3;
unsigned short int **ppuVar4;
uint uVar5;
char cVar6;
ulong local_20;
for (local_20 = 0; local_20 < param_2; local_20 = local_20 + 1) {
iVar2 = (int)(param_3 + local_20) + (int)((param_3 + local_20) / 0x5e) * -0x5e;
cVar1 = param_1[local_20];
ppuVar4 = __ctype_b_loc();
if (((*ppuVar4)[cVar1] & 0x800) == 0) {
//printf("test2");
ppuVar4 = __ctype_b_loc();
// printf("test");
if (((*ppuVar4)[cVar1] & 0x400) == 0) {
uVar5 = (iVar2 + cVar1) - 0x21;
param_1[local_20] = (char)uVar5 + (char)(uVar5 / 0x5e) * -0x5e + '!';
printf("%c\n",param_1[local_20]);
}
else {
ppuVar4 = __ctype_b_loc();
if (((*ppuVar4)[cVar1] & 0x200) == 0) {
cVar6 = 'A';
}
else {
cVar6 = 'a';
}
ppuVar4 = __ctype_b_loc();
if (((*ppuVar4)[cVar1] & 0x200) == 0) {
iVar3 = 0x41;
}
else {
iVar3 = 0x61;
}
uVar5 = (cVar1 - iVar3) + iVar2;
param_1[local_20] = cVar6 + (char)uVar5 + (char)(uVar5 / 0x1a) * -0x1a;
}
}
else {
uVar5 = (iVar2 + cVar1) - 0x30;
param_1[local_20] = (char)uVar5 + (char)(uVar5 / 10) * -10 + '0';
}
}
printf("%s\n", param_1);
return;
}
int fun2(char *param_1)
{
char *local_20;
int local_10;
local_10 = 0x1505;
local_20 = param_1;
while( true ) {
if (*local_20 == 0) break;
local_10 = (int)*local_20 + local_10 * 0x21;
local_20 = local_20 + 1;
}
return local_10;
}
void main()
{
char param_1[32];
int param_2 = 8;
unsigned int uVar1;
int iVar2;
time_t tVar3;
tVar3 = time((time_t *)0x0);
uVar1 = getpid();
srand(uVar1 ^ (uint)tVar3);
iVar2 = rand();
snprintf(param_1,param_2 + 1,"%08lx%02d",tVar3,(ulong)(uint)(iVar2 % 100));
// printf("%s\n", param_1);
int val = fun2(param_1);
//printf("%d\n", val);
fun3(param_1, 8, val);
return;
}
There is a way to run these programs at the same time as both needs to generate the same string, we can use the process synchronization using the PIPE concept (which is one of the IPC concept in *nix systems).
Note: We can’t guarantee that two processes will start at the exact same time even if we attempt to Synchronize them at the same system time. In practical scenario there will always be some minute differences because of the inherent delays in process scheduling and OS-level context switching.
So, this is an attempt to achieve same, the execution flow will be something like:
A parent process will start which will spawn/fork 2 child processes:
- These 2 child processes will execute our binaries.
- 1st child process will execute our solution binary that will generate the string.
- 2nd child process will execute the challenge binary which will receive input from 1st child process through PIPE.
- But there is a condition:
- Both the process will start those 2 binaries at the approx. same current system time (There might be a slight delay (from the OS level as it depends on various factors while scheduling of a process) or other factors like time difference to reach that execution point at the same time where “time” function is invoked (for this challenge binary and the solution binary that we will create)).
- There will be a function sync_time which will run in both child process, this function will hold the execution for 1,2 seconds. This function is intended to make both child processes wait until the system time reaches a specific target (target_time). It repeatedly checks the system time and only proceeds when the time is equal to or greater than target_time.
- But after the execution of function sync_time gets completes then time will get synchronized and both processes will execute their respective binaries.
- Then when the solution binary will get executed the string that will get generated from the “solution” binary will be piped to the read end of the pipe which is the challenge binary as it is expecting the input string.
- If there will be no delay and both (the challenge and the solution binary) started at the approx. same current system time, then we solved the challenge and we should receive message “Nicely done! “ from challenge binary.
# Process Synchronization Using PIPE:
Description on working of the program (Step by Step execution):
The program uses the PIPE IPC mechanism to exchange the data and Synchronize between the running processes. The program opens the pipe file descriptors “pfiledes[2]” where:
- pfiledes[0] : This is the read end of the pipe
- pfiledes[1] : This is the write end of the pipe
Once the child processes were forked they will wait for the sync_time() function to synchronize child process binary invoking time,
- Before invoking our binaries, child processes will close the unused pipes:
- Child process-1 (which will execute solution binary that will generate a string) will close the read-end of the pipe which is “pfiledes[0]” (as read-end is not required for this child process-1) and using the function “dup2” it redirects the standard output to the pipe (write end of the pipe), once redirection is established it will close the write end of pipe as well, so that the solution binary will now write in established pipe instead of the standard output.
- Similarly, Child process-2 (which will execute the challenge binary which expects user input) will close the write-end of the pipe which is “pfiledes[1]” (as write-end is not required for this child process-2) and using the function “dup2” it redirects the standard input to the pipe (read end of the pipe). once redirection is established it will close the read end of the pipe as well, so the challenge binary will now read from the established pipe instead of standard input.
- 1st child process at the approx. synchronized time will execute our solution binary “test” that will generate the string and write solution string into established PIPE.
- 2nd child process at the approx. synchronized time will execute the challenge binary “foo” which will receive input from 1st child process through established PIPE.
- Then based on the string received by the challenge binary if the processes were synchronized and “time()” function was invoked approx. at the same time, then strings “local_a8” and “local_88” would be same and the challange will be solved.
This code is saved in file named: final1.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <time.h>
void sync_time(time_t target_time) { // this function synchronize the time between two programs
time_t current_time;
do {
current_time = time(NULL);
} while (current_time < target_time);
}
int main() {
int pfiledes[2]; // creating pipe file descriptors
pid_t pid1, pid2;
time_t target_time;
target_time = time(NULL) + 1;
if (pipe(pfiledes) == -1) {
perror("pipe");
exit(1);
}
pid1 = fork(); // forking process1
if (pid1 == -1) {
perror("fork");
exit(1);
}
if (pid1 == 0) {
sync_time(target_time);
close(pfiledes[0]);
dup2(pfiledes[1], STDOUT_FILENO); // redirecting the stdout to the write-end of pipe.
close(pfiledes[1]);
execlp("./test", "test", (char *)NULL); // invoking the "test" solution binary.
perror("execlp test failed");
exit(1);
}
pid2 = fork(); // forking process2
if (pid2 == -1) {
perror("fork");
exit(1);
}
if (pid2 == 0) {
sync_time(target_time);
close(pfiledes[1]);
dup2(pfiledes[0], STDIN_FILENO); // redirecting the stdin to the read-end of pipe.
close(pfiledes[0]);
execlp("./foo", "foo", (char *)NULL); // invoking the "foo" challenge binary.
perror("execlp foo failed");
exit(1);
}
close(pfiledes[0]);
close(pfiledes[1]);
waitpid(pid1, NULL, 0);
waitpid(pid2, NULL, 0);
return 0;
}
Let’s Compile this code and create a final executable using the following command:
gcc -o final_program final1.c
Let’s run this program as this program will execute the challenge and the solution binary at approx. the same time and we can solve our challenge. Sometimes there might be some latency so we might see error but if we keep executing our final binary, at some point “time” function will be invoked at same epoch time on both programs this challenge will get solved.
Note: There might be other simpler way to solve this challenge. Like I observed that when we are using this pipe to send the output of the solution program “test” to “foo” then also sometimes the challenge is getting solved, but i wanted to try some different approach.
./test | ./foo
We have successfully solved the challenge. I hope it was fun.
Let’s get back with the next challenge or learning.