ISC-Nothing出题思路及WriteUp
出题思路
在看到通过DWARF Expression将代码隐藏在栈展开过程中这篇博客后,原本是想用DWRAF特性做一个VM的。但是DWRAF恢复状态时只能恢复寄存器,但是可以用的寄存器有比较少,因此用这仅有的几个寄存器做了半个虚拟机。
工具选择
首先碰到的第一个问题就是怎样写DWRAF Expression比较方便。在经过一份搜寻后,发现一个比较好用的rust库gimli
。但是他无法只生成DWRAF Expression,因此稍稍魔改了一下,添加了一个函数,这样方便得生成Expression了
let mut exp=Expression::new();
exp.op_reg(gimli::X86_64::R12);
...
exp.op(gimli::DW_OP_and);
exp.op(gimli::DW_OP_gt);
exp.op(gimli::DW_OP_swap);
let cf=CallFrameInstruction::ValExpression(gimli::X86_64::R14, exp);
cf.simple_write(&mut w,gimli::Encoding { address_size: 8, format: gimli::Format::Dwarf64, version: 5 }).unwrap();
条件逻辑
在DWRAF Expression中没有条件转移的命令,那么意味着如果想要实现类似条件语句,就要转化成数学表达式,在该题中普遍运用了下面表达式
( ((exp!=1)-1)&num1 ) | ( ((exp!=0)-1)&num2 )
来表示exp?num1:num2
的逻辑。
常量混淆
如果这道题只有上面这些,那么就没什么难度了。因此加了一个简单的常量混淆,这样就必须要写自动化脚本来分析这段DWRAF Expression。构造常量混淆的代码如下
fn exp_constu(exp:&mut Expression, rng:&mut ThreadRng,num:u64,depth:u64){
if depth==0 {
exp.op_constu(num);
return;
}
let ty:usize=rng.gen_range(0..9);
match ty{
0=>{
let xor_num:u64=rng.gen();
exp_constu(exp, rng, xor_num, depth-1);
exp_constu(exp, rng, xor_num^num, depth-1);
exp.op(gimli::DW_OP_xor);
}
1=>{
let add_num:u64=rng.gen();
exp_constu(exp, rng, add_num.wrapping_add(num), depth-1);
exp_constu(exp, rng, add_num, depth-1);
exp.op(gimli::DW_OP_minus);
}
2=>{
let add_num:u64=rng.gen();
exp_constu(exp, rng, num.wrapping_sub(add_num), depth-1);
exp_constu(exp, rng, add_num, depth-1);
exp.op(gimli::DW_OP_plus);
}
3=>{
let num1:u64=rng.gen::<u64>()#
let mut num2:u64=rng.gen::<u64>()#
num2|=num&!(num1|num2);
assert_eq!(num1|num2,num);
exp_constu(exp, rng, num1, depth-1);
exp_constu(exp, rng, num2, depth-1);
exp.op(gimli::DW_OP_or);
}
4=>{
let num1:u64=rng.gen::<u64>()&!num;
let mut num2:u64=rng.gen::<u64>()&!num;
num2|=!num&!(num1|num2);
assert_eq!(!num1&!num2,num);
exp_constu(exp, rng, !num1, depth-1);
exp_constu(exp, rng, !num2, depth-1);
exp.op(gimli::DW_OP_and);
}
5=>{
exp_constu(exp, rng, !num, depth-1);
exp.op(gimli::DW_OP_not);
}
6=>{
exp.op(gimli::DW_OP_dup);
exp_constu(exp, rng, num, depth-1);
exp.op(gimli::DW_OP_swap);
exp.op(gimli::DW_OP_drop);
}
7=>{
let rand_num:u64=rng.gen();
exp_constu(exp, rng, rand_num, depth-1);
exp_constu(exp, rng, num, depth-1);
exp.op(gimli::DW_OP_swap);
exp.op(gimli::DW_OP_drop);
}
8=>{
let rand_num1:u64=rng.gen();
let rand_num2:u64=rng.gen();
exp_constu(exp, rng, rand_num1, depth-1);
exp_constu(exp, rng, rand_num2, depth-1);
exp_constu(exp, rng, num, depth-1);
exp.op(gimli::DW_OP_rot);
exp.op(gimli::DW_OP_drop);
exp.op(gimli::DW_OP_drop);
}
_=>{}
}
}
WriteUp
通过IDA反编译发现程序必定会输出Success! Your flag is flag{%016lx%016lx}\n
,但是通过运行程序发现输出的是Error
。研究一项输出Error
的条件,发现是r13
不为0时输出,那么通过调试可以发现是在throw-catch
的过程中修改了r13
。
参考通过DWARF Expression将代码隐藏在栈展开过程中这篇文章,可以得知有可能是在DWRAF中恢复寄存器的表达式中做了手脚。通过readelf -wf nothing
查看得到大量信息。其中有4段DW_CFA_val_expression
非常长就是恢复寄存器所运行的表达式,具体指令含义请参考文档DWARF 5 Standard(2.5 DWARF Expressions)。显然这种长度手工分析已经不可能了,因此这里写了一个简单的分析器,他可以把这些栈操作变成c代码,然后通过编译器的-O3
优化,可以得到较为简单的表达式。
分析器代码如下:
// [dependencies]
// gimli = "0.26.1"
// object = "0.29.0"
use std::{collections::HashMap, fs, fmt::Display, io::Write};
use std::process::Command;
use gimli::UnwindSection;
use object::{Object, ObjectSection};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut arg = std::env::args();
if arg.len() != 2 {
panic!("Argument Error!")
}
let bin_data = fs::read(arg.nth(1).unwrap())?;
let obj_file = object::File::parse(&*bin_data)?;
let data = obj_file.section_by_name(".eh_frame").unwrap();
let eh_frame = gimli::read::EhFrame::new(data.data()?, gimli::LittleEndian);
let bases = gimli::BaseAddresses::default().set_eh_frame(data.address());
let mut entries = eh_frame.entries(&bases);
let mut file = fs::OpenOptions::new().append(false).truncate(true).write(true).create(true).open("./output.c")?;
writeln!(file, "#include <stdint.h>")?;
let mut cies = HashMap::new();
while let Some(entry) = entries.next()? {
if let gimli::CieOrFde::Fde(partial) = entry {
let fde = partial.parse(|_, bases, o| {
cies.entry(o)
.or_insert_with(|| eh_frame.cie_from_offset(bases, o))
.clone()
})?;
// 通过长度过滤出我们想要的
if fde.entry_len() < 100 {
continue;
}
let mut instructions = fde.instructions(&eh_frame, &bases);
use gimli::CallFrameInstruction::*;
loop {
match instructions.next() {
Err(e) => {
println!("Failed to decode CFI instruction: {}", e);
break;
}
Ok(Some(ValExpression {
register,
expression,
})) => {
println!(
"DW_CFA_val_expression ({}, ...)",
gimli::X86_64::register_name(register).unwrap_or("{unknown}")
);
display_val_expression(register, expression, &mut file)?;
}
Ok(None) => {
break;
}
_ => {}
}
}
}
}
file.flush()?;
Command::new("gcc")
.arg("-O3")
.arg("./output.c")
.arg("-c")
.spawn()?;
Ok(())
}
#[derive(Clone, Copy)]
struct Val {
id: u64,
}
impl Val {
fn new(id: u64) -> Self {
Val { id }
}
}
struct ValGenerator {
id: u64,
}
impl ValGenerator {
fn new() -> Self {
Self { id: 0 }
}
fn next(&mut self) -> Val {
self.id += 1;
Val::new(self.id - 1)
}
}
impl Display for Val {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "v{}", self.id)
}
}
fn display_val_expression<R>(target_reg: gimli::Register, exp: gimli::Expression<R>, w: &mut dyn Write) -> Result<(), Box<dyn std::error::Error>>
where
R: gimli::Reader,
{
let mut val_generator = ValGenerator::new();
let mut ops = exp.operations(gimli::Encoding { address_size: 8, format: gimli::Format::Dwarf64, version: 5 });
let mut stack: Vec<Val> = Vec::new();
writeln!(w, "uint64_t cal_{}(uint64_t r12, uint64_t r13, uint64_t r14, uint64_t r15){{", gimli::X86_64::register_name(target_reg).unwrap())?;
writeln!(w, " uint64_t rax=0,rbx=0;")?;
loop {
if let Ok(Some(op)) = ops.next() {
match op {
gimli::Operation::Drop => {
stack.pop();
}
gimli::Operation::Pick { index } => {
let val1 = stack.get(stack.len() - 1 - index as usize).unwrap();
let new_val = val_generator.next();
writeln!(w, " uint64_t {}={};", new_val, val1)?;
stack.push(new_val);
}
gimli::Operation::Swap => {
let val1 = stack.pop().unwrap();
let val2 = stack.pop().unwrap();
stack.push(val1);
stack.push(val2);
}
gimli::Operation::Rot => {
let val1 = stack.pop().unwrap();
let val2 = stack.pop().unwrap();
let val3 = stack.pop().unwrap();
stack.push(val1);
stack.push(val3);
stack.push(val2);
}
gimli::Operation::And => {
let val1 = stack.pop().unwrap();
let val2 = stack.pop().unwrap();
let new_val = val_generator.next();
writeln!(w, " uint64_t {}={}&{};", new_val, val2, val1)?;
stack.push(new_val);
}
gimli::Operation::Minus => {
let val1 = stack.pop().unwrap();
let val2 = stack.pop().unwrap();
let new_val = val_generator.next();
writeln!(w, " uint64_t {}={}-{};", new_val, val2, val1)?;
stack.push(new_val);
}
gimli::Operation::Neg => {
let val = stack.get(stack.len() - 1).unwrap();
writeln!(w, " {}=-{};", val, val)?;
}
gimli::Operation::Not => {
let val = stack.get(stack.len() - 1).unwrap();
writeln!(w, " {}=~{};", val, val)?;
}
gimli::Operation::Or => {
let val1 = stack.pop().unwrap();
let val2 = stack.pop().unwrap();
let new_val = val_generator.next();
writeln!(w, " uint64_t {}={}|{};", new_val, val2, val1)?;
stack.push(new_val);
}
gimli::Operation::Plus => {
let val1 = stack.pop().unwrap();
let val2 = stack.pop().unwrap();
let new_val = val_generator.next();
writeln!(w, " uint64_t {}={}+{};", new_val, val2, val1)?;
stack.push(new_val);
}
gimli::Operation::PlusConstant { value } => {
let val = stack.get(stack.len() - 1).unwrap();
writeln!(w, " {}+={}ull;", val, value)?;
}
gimli::Operation::Shl => {
let val1 = stack.pop().unwrap();
let val2 = stack.pop().unwrap();
let new_val = val_generator.next();
writeln!(w, " uint64_t {}={}<<{};", new_val, val2, val1)?;
stack.push(new_val);
}
gimli::Operation::Shr => {
let val1 = stack.pop().unwrap();
let val2 = stack.pop().unwrap();
let new_val = val_generator.next();
writeln!(w, " uint64_t {}={}>>{};", new_val, val2, val1)?;
stack.push(new_val);
}
gimli::Operation::Shra => {
let val1 = stack.pop().unwrap();
let val2 = stack.pop().unwrap();
let new_val = val_generator.next();
writeln!(w, " uint64_t {}=(uint64_t)((int64_t){}>>(int64_t){});", new_val, val2, val1)?;
stack.push(new_val);
}
gimli::Operation::Xor => {
let val1 = stack.pop().unwrap();
let val2 = stack.pop().unwrap();
let new_val = val_generator.next();
writeln!(w, " uint64_t {}={}^{};", new_val, val2, val1)?;
stack.push(new_val);
}
gimli::Operation::Eq => {
let val1 = stack.pop().unwrap();
let val2 = stack.pop().unwrap();
let new_val = val_generator.next();
writeln!(w, " uint64_t {}= {}=={}?1:0;", new_val, val2, val1)?;
stack.push(new_val);
}
gimli::Operation::Ge => {
let val1 = stack.pop().unwrap();
let val2 = stack.pop().unwrap();
let new_val = val_generator.next();
writeln!(w, " uint64_t {}={}>={}?1:0;", new_val, val2, val1)?;
stack.push(new_val);
}
gimli::Operation::Gt => {
let val1 = stack.pop().unwrap();
let val2 = stack.pop().unwrap();
let new_val = val_generator.next();
writeln!(w, " uint64_t {}={}>{}?1:0;", new_val, val2, val1)?;
stack.push(new_val);
}
gimli::Operation::Le => {
let val1 = stack.pop().unwrap();
let val2 = stack.pop().unwrap();
let new_val = val_generator.next();
writeln!(w, " uint64_t {}={}<={}?1:0;", new_val, val2, val1)?;
stack.push(new_val);
}
gimli::Operation::Lt => {
let val1 = stack.pop().unwrap();
let val2 = stack.pop().unwrap();
let new_val = val_generator.next();
writeln!(w, " uint64_t {}={}<{}?1:0;", new_val, val2, val1)?;
stack.push(new_val);
}
gimli::Operation::Ne => {
let val1 = stack.pop().unwrap();
let val2 = stack.pop().unwrap();
let new_val = val_generator.next();
writeln!(w, " uint64_t {}={}!={}?1:0;", new_val, val2, val1)?;
stack.push(new_val);
}
gimli::Operation::UnsignedConstant { value } => {
let new_val = val_generator.next();
writeln!(w, " uint64_t {}={}ull;", new_val, value)?;
stack.push(new_val);
}
gimli::Operation::SignedConstant { value } => {
let new_val = val_generator.next();
writeln!(w, " uint64_t {}=(uint64_t){}ll;", new_val, value)?;
stack.push(new_val);
}
gimli::Operation::Register { register } => {
let new_val = val_generator.next();
writeln!(w, " uint64_t {}={};", new_val, gimli::X86_64::register_name(register).unwrap_or("{error}"))?;
stack.push(new_val);
}
_ => todo!("{:?}", op)
}
} else {
break;
}
}
assert_eq!(stack.len(), 1);
writeln!(w, " return {};", stack.pop().unwrap())?;
writeln!(w, "}}\n")?;
Ok(())
}
生成的c文件:
#include <stdint.h>
uint64_t cal_r12(uint64_t r12, uint64_t r13, uint64_t r14, uint64_t r15){
uint64_t rax=0,rbx=0;
uint64_t v0=r12;
uint64_t v1=10841219284134096893ull;
uint64_t v2=11129291835405172697ull;
uint64_t v3=v1&v2;
uint64_t v4=7670811485375801638ull;
uint64_t v5=9042264754201427834ull;
uint64_t v6=13551094804195048963ull;
uint64_t v7=v5&v6;
uint64_t v8=11953879495213237442ull;
v8=~v8;
v8=~v8;
uint64_t v9=6024959385228738588ull;
uint64_t v10=10110448519209335383ull;
uint64_t v11=v9^v10;
uint64_t v12=18357748223165525310ull;
uint64_t v13=v11&v12;
uint64_t v14=v8^v13;
v14=~v14;
uint64_t v15=288301881189795841ull;
v15=~v15;
uint64_t v16=8268870611111702486ull;
uint64_t v17=3955754985819470506ull;
uint64_t v18=v16-v17;
uint64_t v19=13807121416608770802ull;
uint64_t v20=v15&v19;
uint64_t v21=12157606309567720355ull;
uint64_t v22=5738683437199689134ull;
uint64_t v23=9508833155140800093ull;
uint64_t v24=v22+v23;
uint64_t v25=2318908854934060614ull;
uint64_t v26=15520974619213676469ull;
uint64_t v27=v25+v26;
uint64_t v28=11008240194628928020ull;
v28=~v28;
uint64_t v29=v27&v28;
uint64_t v30=v24-v29;
uint64_t v31=13784270564178122628ull;
uint64_t v32=1297940605539369143ull;
uint64_t v33=10325368571782404983ull;
uint64_t v34=v32^v33;
uint64_t v35=13072036865594709985ull;
uint64_t v36=14411389371001456874ull;
uint64_t v37=1733989214560846096ull;
uint64_t v38=3680308739910074691ull;
...
通过gcc
加上-O3
参数编译,再通过逆向工具反编译,得到的结果如下
观察这四段表达式,可以发现他们都在根据R12
的低8位进行选择不同的功能。可以列得下表(具体数据每个附件不一样):
R12&0xff | R12 | R13 | R14 | R15 |
---|---|---|---|---|
0 | 1 | R13+11448659141604722329 | R14 | R15 |
1 | 1 | R13 | R14^(r13+6754348585825892801)^(r13>>30)^(r15<<24) | R15 |
2 | 1 | R13 | R15 | R14 |
3 | 1 | R13 | R14^5050961714944838468 | R15^17385842772293075248 |
4 | R12>>8 | R13 | R14 | R15 |
5 | 0 | R13 | R14 | R15 |
6 | r13 == 14610338879684656475 ? (r12 >> 8) : 2 | R13 | R14 | R15 |
7 | 1 | r14!=9785931681412827268 | r15!=6634550174467950632 | R14 | R15 |
根据逆向结果,发现每次都会取v10+i
的地方的内容放入寄存器r12
中,因此v10
里面存的是是操作码。每次执行完后又会i+=r12
,因此返回的r12
是对应下一个操作码与当前的操作码的偏移。结合上表,4号功能是无条件转移,5号功能是结束运行,6号功能是条件跳转(条件是r13 == 14610338879684656475
)。
根据这些信息,可以把加密过程写出如下代码。注意:每个不同的附件的下面常量都是不一样的。
#include <stdint.h>
#define XOR_NUM_A 5050961714944838468ull
#define XOR_NUM_B 17385842772293075248ull
#define DELTA_END 14610338879684656475ull
#define DETLE 11448659141604722329ull
#define HASH_NUM 6754348585825892801ull
#define RESULT_A 9785931681412827268ull
#define RESULT_B 6634550174467950632ull
uint64_t enc(uint64_t num_a, uint64_t num_b){
uint64_t r13=0, r14=num_a, r15=num_b;
r14^=XOR_NUM_A;
r15^=XOR_NUM_B;
while(r13!=DELTA_END) {
r13+=DETLE;
r14^=(r13+HASH_NUM)^(r13>>30)^(r15<<24);
uint64_t tmp=r14;
r14=r15;
r15=tmp;
}
r14^=XOR_NUM_A;
r15^=XOR_NUM_B;
return r14!=RESULT_A || r15!=RESULT_B;
}
可以看出这是Feistel cipher,根据该密码的特点,可以写得解密函数:
#include <stdint.h>
#define XOR_NUM_A 5050961714944838468ull
#define XOR_NUM_B 17385842772293075248ull
#define DELTA_END 14610338879684656475ull
#define DETLE 11448659141604722329ull
#define HASH_NUM 6754348585825892801ull
#define RESULT_A 9785931681412827268ull
#define RESULT_B 6634550174467950632ull
void dec(uint64_t* flag_a, uint64_t* flag_b){
uint64_t r13=DELTA_END, r14=RESULT_A, r15=RESULT_B;
r14^=XOR_NUM_A;
r15^=XOR_NUM_B;
while(r13) {
uint64_t tmp=r14;
r14=r15;
r15=tmp;
r14^=(r13+HASH_NUM)^(r13>>30)^(r15<<24);
r13-=DETLE;
}
r14^=XOR_NUM_A;
r15^=XOR_NUM_B;
*flag_a=r14;
*flag_b=r15;
}
int main(){
uint64_t flag_a,flag_b;
dec(&flag_a,&flag_b);
printf("flag{%016llx%016llx}",flag_a,flag_b);
return 0;
}
项目源码及wp源码 ri-char/nothing-ctf