diff --git a/Cargo.lock b/Cargo.lock index 1331a3f..9f066ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -50,12 +50,20 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "anyhow" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" + [[package]] name = "cattleya" version = "0.1.0" dependencies = [ + "anyhow", "clap", "memmap2", + "thiserror", ] [[package]] @@ -160,6 +168,26 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "thiserror" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "unicode-ident" version = "1.0.12" diff --git a/Cargo.toml b/Cargo.toml index fc45fe9..398b547 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,5 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +anyhow = "1.0.86" clap = { version = "4.5.3", features = ["derive"] } memmap2 = "0.9.4" +thiserror = "1.0.63" diff --git a/README.md b/README.md index c254b0b..2f5df1a 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,9 @@ Options: --comment nullify comment section in the ELF --section
nullify section in the ELF [default: ] -r, --recursive recursive [default: ] + -g, --got perform GOT overwrite + --got-l GOT overwrite target library function name [default: ] + --got-f GOT overwrite target function name [default: ] -h, --help Print help -V, --version Print version ``` @@ -134,6 +137,36 @@ Hex dump of section '.comment': 0x00000020 00000000 00000000 000000 ........... ``` +## GOT overwrite + +Overwrites the GOT section with a specified value + +``` +$ cattleya -i bin/got --got --got-l system --got-f secret -o bin/res_got +$ ./bin/res_got +secret function called +``` + +As shown below, only the system function is called in the main function as far as disassembly is concerned: + +``` +$ objdump -d bin/res_got +... +00000000004011d2
: + 4011d2: f3 0f 1e fa endbr64 + 4011d6: 55 push %rbp + 4011d7: 48 89 e5 mov %rsp,%rbp + 4011da: 48 83 ec 10 sub $0x10,%rsp + 4011de: 48 8d 05 36 0e 00 00 lea 0xe36(%rip),%rax # 40201b <_IO_stdin_used+0x1b> + 4011e5: 48 89 c7 mov %rax,%rdi + 4011e8: e8 73 fe ff ff call 401060 + 4011ed: 89 45 fc mov %eax,-0x4(%rbp) + 4011f0: b8 00 00 00 00 mov $0x0,%eax + 4011f5: c9 leave + 4011f6: c3 ret +... +``` + # Recursive option By specifying the directory name in the recursive option, the same obfuscation can be applied to all ELF files in that directory: diff --git a/bin/got b/bin/got new file mode 100755 index 0000000..8c5f99a Binary files /dev/null and b/bin/got differ diff --git a/bin/got.c b/bin/got.c new file mode 100644 index 0000000..46d61f1 --- /dev/null +++ b/bin/got.c @@ -0,0 +1,14 @@ +// gcc got.c -no-pie -o got +#include +#include + +int secret(char* s) { + if (s[0] == 's' && s[1] == 'e' && s[2] == 'c' && s[3] == 'r' && s[4] == 'e' && s[5] == 't') { + puts("secret function called"); + } + return 0; +} + +int main() { + int x=system("secret"); +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..80e4b6b --- /dev/null +++ b/src/error.rs @@ -0,0 +1,28 @@ +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("invalid option: {0}")] + InvalidOption(&'static str), + + #[error("failed to open file: {0}")] + OpenFile(std::io::Error), + + #[error("failed to create file: {0}")] + CreateFile(std::io::Error), + + #[error("invalid ELF magic")] + InvalidMagic, + + #[error("failed to mmap: {0}")] + Mmap(std::io::Error), + + #[error("failed in I/O operation: {0}")] + Io(std::io::Error), + + #[error("failed to process obfuscation: {0}")] + Obfuscation(&'static str), + + #[error("not found: {0}")] + NotFound(String), +} + +pub type Result = std::result::Result; diff --git a/src/main.rs b/src/main.rs index 24067f6..5ac4e46 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +mod error; mod obfus; mod util; @@ -45,15 +46,27 @@ struct Args { section: String, #[arg(short, long, help = "recursive", default_value = "")] recursive: String, + #[arg(short, long, help = "perform GOT overwrite", default_value = "false")] + got: bool, + #[arg( + long, + help = "GOT overwrite target library function name", + default_value = "" + )] + got_l: String, + #[arg(long, help = "GOT overwrite target function name", default_value = "")] + got_f: String, } -fn main() { +fn main() -> crate::error::Result<()> { use clap::Parser as _; let args = Args::parse(); if args.recursive.is_empty() { if args.input.is_empty() { - panic!("input file name is required"); + return Err(crate::error::Error::InvalidOption( + "input file name is required", + )); } let output_path = if !args.output.is_empty() { @@ -62,13 +75,15 @@ fn main() { "obfuscated" }; - exec_obfus(&args.input, output_path, &args).unwrap_or(()); + exec_obfus(&args.input, output_path, &args).unwrap(); } else { if !args.input.is_empty() { - panic!("both input file name and recursive option are not allowed"); + return Err(crate::error::Error::InvalidOption( + "both input file name and recursive option are not allowed", + )); } if !args.output.is_empty() { - println!("output file name will be ignored"); + eprintln!("output file name will be ignored"); } let entries = util::RecursiveDir::new(&args.recursive) @@ -83,16 +98,18 @@ fn main() { std::fs::create_dir_all(dir).unwrap(); std::fs::File::create(&output_path).unwrap(); - exec_obfus(entry.to_str().unwrap(), &output_path, &args).unwrap_or(()); + exec_obfus(entry.to_str().unwrap(), &output_path, &args).unwrap(); } } + + Ok(()) } -fn exec_obfus(input_path: &str, output_path: &str, args: &Args) -> std::io::Result<()> { +fn exec_obfus(input_path: &str, output_path: &str, args: &Args) -> crate::error::Result<()> { let loader = obfus::Obfuscator::open(input_path, output_path); let mut obfuscator = loader.unwrap(); - match obfuscator.is_elf() && obfuscator.is_64bit() { + match obfuscator.is_elf() { true => { println!("start obfuscating {}...", input_path); @@ -106,21 +123,28 @@ fn exec_obfus(input_path: &str, output_path: &str, args: &Args) -> std::io::Resu obfuscator.nullify_sec_hdr(); } if args.symbol { - obfuscator.nullify_section(".strtab"); + obfuscator.nullify_section(".strtab")?; } if args.comment { - obfuscator.nullify_section(".comment"); + obfuscator.nullify_section(".comment")?; } if !args.section.is_empty() { - obfuscator.nullify_section(&args.section); + obfuscator.nullify_section(&args.section)?; + } + if args.got { + if args.got_l.is_empty() || args.got_f.is_empty() { + return Err(crate::error::Error::InvalidOption( + "both library and function names are required", + )); + } + + obfuscator.got_overwrite(&args.got_l, &args.got_f)?; } println!("obfuscation done!"); Ok(()) } - false => { - panic!("not a valid ELF file: {}", args.input); - } + false => Err(crate::error::Error::InvalidMagic), } } @@ -170,7 +194,7 @@ mod tests { fn null_symbol_name() { let loader = crate::obfus::Obfuscator::open("bin/test_64bit", "bin/res_symbol"); let mut obfuscator = loader.unwrap(); - obfuscator.nullify_section(".strtab"); + obfuscator.nullify_section(".strtab").unwrap(); let output = std::process::Command::new("readelf") .args(["-x29", "bin/res_symbol"]) .output() @@ -190,7 +214,7 @@ mod tests { fn null_comment() { let loader = crate::obfus::Obfuscator::open("bin/test_64bit", "bin/res_comment"); let mut obfuscator = loader.unwrap(); - obfuscator.nullify_section(".comment"); + obfuscator.nullify_section(".comment").unwrap(); let output = std::process::Command::new("readelf") .args(["-x27", "bin/res_comment"]) .output() diff --git a/src/obfus.rs b/src/obfus.rs index 02b6f28..e800d62 100644 --- a/src/obfus.rs +++ b/src/obfus.rs @@ -29,14 +29,16 @@ pub struct Obfuscator { sec_hdr_size: u64, sec_hdr_offset: u64, sec_table: u64, + dyn_strings: String, + string_table: String, } impl Obfuscator { - pub fn open(input_path: &str, output_path: &str) -> std::io::Result { + pub fn open(input_path: &str, output_path: &str) -> crate::error::Result { let file = match std::fs::OpenOptions::new().read(true).open(input_path) { Ok(file) => file, Err(e) => { - panic!("failed to open file: {}", e); + return Err(crate::error::Error::OpenFile(e)); } }; @@ -49,18 +51,23 @@ impl Obfuscator { { Ok(file) => file, Err(e) => { - panic!("failed to create file: {}", e); + return Err(crate::error::Error::CreateFile(e)); } }; let mut input_contents = Vec::new(); - file.try_clone()? + file.try_clone() + .map_err(crate::error::Error::Io)? .take(usize::MAX as u64) - .read_to_end(&mut input_contents)?; - output_file.write_all(&input_contents)?; + .read_to_end(&mut input_contents) + .map_err(crate::error::Error::Io)?; + output_file + .write_all(&input_contents) + .map_err(crate::error::Error::Io)?; - let input = unsafe { memmap2::Mmap::map(&file)? }; - let output = unsafe { memmap2::MmapMut::map_mut(&output_file)? }; + let input = unsafe { memmap2::Mmap::map(&file).map_err(crate::error::Error::Mmap)? }; + let output = + unsafe { memmap2::MmapMut::map_mut(&output_file).map_err(crate::error::Error::Mmap)? }; let elf_hdr: ElfHeader = unsafe { std::ptr::read(input.as_ptr() as *const ElfHeader) }; @@ -106,7 +113,7 @@ impl Obfuscator { let sec_hdr = String::from_utf8_lossy(&data_copy).to_string(); - Ok(Obfuscator { + let mut obfus = Obfuscator { input, output, sec_hdr, @@ -114,7 +121,21 @@ impl Obfuscator { sec_hdr_size, sec_hdr_offset, sec_table, - }) + dyn_strings: String::new(), + string_table: String::new(), + }; + + let (section_addr, section_size, _, _) = obfus.get_section(".dynstr").unwrap(); + obfus.dyn_strings = + String::from_utf8_lossy(&obfus.input[section_addr..section_addr + section_size]) + .to_string(); + + let (section_addr, section_size, _, _) = obfus.get_section(".strtab").unwrap(); + obfus.string_table = + String::from_utf8_lossy(&obfus.input[section_addr..section_addr + section_size]) + .to_string(); + + Ok(obfus) } pub fn is_elf(&self) -> bool { @@ -125,10 +146,25 @@ impl Obfuscator { self.input[4] == 2 } - fn get_section(&self, section: &str) -> (usize, usize) { + fn is_enable_pie(&self) -> bool { + self.input[16] != 2 + } + + fn is_stripped(&self) -> bool { + self.get_section(".symtab").unwrap().0 == 0 + } + + fn v2p(&self, virtual_addr: usize, section: &str) -> usize { + let (section_addr, _, _, vaddr) = self.get_section(section).unwrap(); + + section_addr + virtual_addr - vaddr + } + + // (section_addr, section_size, entry_size, vaddr) + fn get_section(&self, section: &str) -> crate::error::Result<(usize, usize, usize, usize)> { let searched_idx = self.sec_hdr.find(section).unwrap_or(usize::MAX); if searched_idx == usize::MAX { - panic!("section not found"); + return Err(crate::error::Error::InvalidOption("section not found")); } for i in 0..self.sec_hdr_num { @@ -137,14 +173,26 @@ impl Obfuscator { .to_vec(); let string_offset = u32::from_le_bytes(sec_hdr[0..4].try_into().unwrap()); if string_offset == searched_idx as u32 { - return ( - u64::from_le_bytes(sec_hdr[24..32].try_into().unwrap()) as usize, - u64::from_le_bytes(sec_hdr[32..40].try_into().unwrap()) as usize, - ); + if self.is_64bit() { + return Ok(( + u64::from_le_bytes(sec_hdr[24..32].try_into().unwrap()) as usize, + u64::from_le_bytes(sec_hdr[32..40].try_into().unwrap()) as usize, + u64::from_le_bytes(sec_hdr[56..64].try_into().unwrap()) as usize, + u64::from_le_bytes(sec_hdr[16..24].try_into().unwrap()) as usize, + )); + } else { + return Ok(( + u32::from_le_bytes(sec_hdr[16..20].try_into().unwrap()) as usize, + u32::from_le_bytes(sec_hdr[20..24].try_into().unwrap()) as usize, + u32::from_le_bytes(sec_hdr[36..40].try_into().unwrap()) as usize, + u32::from_le_bytes(sec_hdr[12..16].try_into().unwrap()) as usize, + )); + } } } - (usize::MAX, usize::MAX) + // section not found + Err(crate::error::Error::InvalidOption("section not found")) } pub fn change_class(&mut self) { @@ -164,14 +212,108 @@ impl Obfuscator { } } - pub fn nullify_section(&mut self, section: &str) { - let (section_addr, section_size) = self.get_section(section); + pub fn nullify_section(&mut self, section: &str) -> crate::error::Result<()> { + let (section_addr, section_size, _, _) = self.get_section(section).unwrap(); if section_addr == usize::MAX { - panic!("section not found"); + return Err(crate::error::Error::InvalidOption("section not found")); } for i in section_addr..section_addr + section_size { self.output[i] = 0; } + + Ok(()) + } + + fn get_dyn_func_id(&self, function: &str) -> u64 { + let idx = self.dyn_strings.find(function).unwrap(); + let (section_addr, section_size, entry_size, _) = self.get_section(".dynsym").unwrap(); + + let dynsym_section = &self.input[section_addr..section_addr + section_size]; + + for i in 0..section_size / entry_size { + let entry = &dynsym_section[i * entry_size..(i + 1) * entry_size]; + let name_offset = u32::from_le_bytes(entry[0..4].try_into().unwrap()); + if name_offset == idx as u32 { + return i as u64; + } + } + + 0 + } + + fn get_func_addr_by_name(&self, function: &str) -> crate::error::Result { + let idx = self.string_table.find(function).unwrap(); + let (section_addr, section_size, entry_size, _) = self.get_section(".symtab").unwrap(); + + let dynsym_section = &self.input[section_addr..section_addr + section_size]; + + for i in 0..section_size / entry_size { + let entry = &dynsym_section[i * entry_size..(i + 1) * entry_size]; + if self.is_64bit() { + if u32::from_le_bytes(entry[0..4].try_into().unwrap()) == idx as u32 { + return Ok(u64::from_le_bytes(entry[8..16].try_into().unwrap())); + } + } else if u32::from_le_bytes(entry[0..4].try_into().unwrap()) == idx as u32 { + return Ok(u32::from_le_bytes(entry[4..8].try_into().unwrap()) as u64); + } + } + + Err(crate::error::Error::NotFound( + "function not found".to_owned() + function, + )) + } + + pub fn got_overwrite( + &mut self, + function: &str, + new_func_addr: &str, + ) -> crate::error::Result<()> { + if self.is_enable_pie() { + return Err(crate::error::Error::InvalidOption( + "replacing GOT get will no effect with PIE enabled", + )); + } else if self.is_stripped() { + return Err(crate::error::Error::InvalidOption( + "cannot overwrite GOT with stripped binary", + )); + } + + let id = self.get_dyn_func_id(function); + + if self.is_64bit() { + let (section_addr, section_size, entry_size, _) = + self.get_section(".rela.plt").unwrap(); + for i in 0..section_size / entry_size { + let entry = &self.input[section_addr..section_addr + section_size] + [i * entry_size..(i + 1) * entry_size]; + let info = u64::from_le_bytes(entry[8..16].try_into().unwrap()) >> 32; + if info == id { + let offset = u64::from_le_bytes(entry[0..8].try_into().unwrap()); + let addr = self.v2p(offset as usize, ".got.plt"); + let new_func_addr = self.get_func_addr_by_name(new_func_addr); + self.output[addr..addr + 8] + .copy_from_slice(&new_func_addr.unwrap().to_le_bytes()); + return Ok(()); + } + } + } else { + let (section_addr, section_size, entry_size, _) = self.get_section(".rel.plt").unwrap(); + for i in 0..section_size / entry_size { + let entry = &self.input[section_addr..section_addr + section_size] + [i * entry_size..(i + 1) * entry_size]; + let info = (u32::from_le_bytes(entry[8..16].try_into().unwrap()) >> 8) as u64; + if info == id { + let offset = u32::from_le_bytes(entry[0..4].try_into().unwrap()); + let addr = self.v2p(offset as usize, ".got.plt"); + let new_func_addr = self.get_func_addr_by_name(new_func_addr); + self.output[addr..addr + 4] + .copy_from_slice(&new_func_addr.unwrap().to_le_bytes()); + return Ok(()); + } + } + } + + Err(crate::error::Error::Obfuscation("failed to overwrite GOT")) } }