ISC-Nothing出题思路及WriteUp

2022-07-15
#re #wp

出题思路

在看到通过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>()&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_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

image-20220627174136776

参考通过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参数编译,再通过逆向工具反编译,得到的结果如下

image-20220728232623292

观察这四段表达式,可以发现他们都在根据R12的低8位进行选择不同的功能。可以列得下表(具体数据每个附件不一样):

R12&0xffR12R13R14R15
01R13+11448659141604722329R14R15
11R13R14^(r13+6754348585825892801)^(r13>>30)^(r15<<24)R15
21R13R15R14
31R13R14^5050961714944838468R15^17385842772293075248
4R12>>8R13R14R15
50R13R14R15
6r13 == 14610338879684656475 ? (r12 >> 8) : 2R13R14R15
71r14!=9785931681412827268 | r15!=6634550174467950632R14R15

根据逆向结果,发现每次都会取v10+i的地方的内容放入寄存器r12中,因此v10里面存的是是操作码。每次执行完后又会i+=r12,因此返回的r12是对应下一个操作码与当前的操作码的偏移。结合上表,4号功能是无条件转移,5号功能是结束运行,6号功能是条件跳转(条件是r13 == 14610338879684656475)。

image-20220627174024519

根据这些信息,可以把加密过程写出如下代码。注意:每个不同的附件的下面常量都是不一样的。

#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

参考资料

  1. 通过DWARF Expression将代码隐藏在栈展开过程中

  2. DWARF 5 Standard

  3. CFI directives

  4. Debugging information