BFS Ekoparty 2022 Kernel exploitation challenge 分析
任意门 发表于 上海 二进制安全 452浏览 · 2024-05-26 17:08

BFS Ekoparty 2022 Kernel exploitation challenge
题目描述


它题目主要说了几个点,一个是利用脚本python编写的话不能用external libraries,然后最终完成的效果是在目标上运行calc.exe。运行的环境是在win10或者win11上都行
程序分析

它的样本程序其实并不是很大,先跑起来看看效果如何


从提示的字符串来说其实蛮明显的就是本地跑起来的这个其实是一个服务端的监听程序,然后等待客户端连接。
那么我们可控的应该就是客户端去主动连接的过程了,顺手看了一下调用的依赖这样后续ida看函数的话心中有个大概。像这种能跑起来并且持续运行的程序配合动态的行为分析可以更直观的先了解到其一个大概的逻辑。


ida 看了一下它里边函数大概有哪些, 我对其中的函数简单命名了一下(因为其中的函数逻辑也并不复杂)其中的init_Winsock->函数分别实现了初始化Winsock;listen_socket->函数创建并设置一个监听套接字然后初始化套接字,绑定到指定端口,并设置为监听状态,以便接收来自客户端的连接请求;其中还有一个函数mecopy_a2_a1 实现的逻辑跟memcpy差不多从源数组 a2 复制数据到目标数组 a1,如果遇到(43或51)则在目标数组对应位置写入0,否则直接复制。Accept_pacp函数主要处理从网络接收的数据。


先来看一下main函数的整体逻辑

int __cdecl main(int argc, const char **argv, const char **envp)
{
  SOCKET s; // [rsp+20h] [rbp-58h] BYREF
  int addrlen[4]; // [rsp+28h] [rbp-50h] BYREF
  struct sockaddr addr; // [rsp+38h] [rbp-40h] BYREF
  SOCKET v7; // [rsp+58h] [rbp-20h]

  printf("*** Ekoparty 2022 - BFS challenge ***\n");
  buf = (char *)VirtualAlloc((LPVOID)0x10000000, 0x1000ui64, 0x3000u, 0x40u);
  if ( buf )
  {
    if ( (unsigned int)init_Winsock() )
    {
      if ( (unsigned int)listen_socket((__int64)a0000, 0x7AB7u, &s) )
      {
        printf("[+] Server listening\n");
        while ( 1 )
        {
          while ( 1 )
          {
            printf("[+] Waiting for new client connections\n");
            addrlen[0] = 16;
            v7 = accept(s, &addr, addrlen);
            if ( v7 != -1i64 )
              break;
            printf(" [-] Client socket error\n");
          }
          printf(" [+] New connection accepted\n");
          addrlen[0] = recv(v7, buf, 4096, 0);
          if ( addrlen[0] == -1 )
          {
            printf(" [-] Client data error\n");
            closesocket(v7);
          }
          else
          {
            printf(" [+] Data received: %i bytes\n", (unsigned int)addrlen[0]);
            if ( strncmp_s((unsigned int)addrlen[0], buf) )
            {
              printf(" [+] Handshake accepted\n");
              addrlen[0] = send(v7, aHi, 3, 0);
              if ( addrlen[0] == -1 )
              {
                printf(" [-] Client data error\n");
              }
              else
              {
                printf(" [+] Waiting for request\n");
                sub_140001240(v7);
                printf(" [+] Closing connection\n");
              }
              closesocket(v7);
            }
            else
            {
              printf(" [-] Error: Invalid handshake\n");
              closesocket(v7);
            }
          }
        }
      }
      printf("[-] It was not possible to bind: %s:%i\n", a0000_0, 31415i64);
    }
    else
    {
      printf("[-] Socket support version error\n");
    }
  }
  else
  {
    printf("[-] It was not possible to allocate memory\n");
  }
  return 0;
}

main函数中首先就是 VirtualAlloc 函数分配了一个4096byte大小而且这块内存权限给的是PAGE_EXECUTE_READWRITE。再往下就是判断初始化Winsock库是否成功,成功就接着开始创建监听套接字
然后绑定到指定的端口0x7AB7也就是31415,这样服务器的监听就启动起来了。它还贴心了有的了printf告诉我们逻辑走到了哪一步


在往下就是开始通过 while ( 1 ) 循环不断的使用accept函数 去等待客户端连接


accept返回的套接字v7,会到下边的recv来接受服务器接收来自客户端的数据(最多 4096 字节)
addrlen[0]用来判断Cilent数据是否出错出错逻辑就会走到closesocket函数这里了。
在往下看的话正常步骤的话逻辑会继续运行到 addrlen[0] = send(v7, aHi, 3, 0);
然后addrlen[0]的值会被修改->send(v7, aHi, 3, 0),这段代码就是服务器发送一个Hi\0 给客户端
然后逻辑就走到了Accept_pacp函数中去,最开始介绍了这个函数接收并处理来自客户端的数据包
从我们分析main函数来看,main函数主要是负责函数逻辑的处理顺序,所以真正的漏洞点应该就是在Accept_pacp中了,因为Accept_pacp中的a1 参数是来自客户端的数据,也就是由我们可控的参数点。所以在下图逻辑中我们要紧跟一下a1经历了哪些行为


在最开始此函数为先前分配的堆缓冲区填充了 0x5050505050505050 和 0xCF58585858585858 。
然后使用 recv 从 a1 接收 11 字节数据到 buf。然后就是一系列的去check;去检查接收的长度;去检查 buf 中的数据是否包含cookie值(0x323230326F6B45(Eko2022));
去检查字段v9是否为84(然后检查cookie之后的第一个字节是否存在该 T 字符。用于确定数据包的类型);检测 v10 (这里代表packet_len)数据包数据的大小必须小于 0xF00也就是是否超过3840字节。
根据数据包v10,去获取数据到全局缓冲区 ::buf中去,然后就走到了mecopy_a2_a1函数,此函数接受了三个参数CmdLine ::buf lena,其实就是处理 ::buf 中的数据,并将数据存储到 CmdLine。如果v7为 84,就将处理后的 CmdLine 发回给客户端。
不知道是否注意到一个点,此函数中的recv在接受数据包的时候,这个数据包就用于不同的验证,就如我们上述分析的,而且其中关键的数据点packet_len 必须要小于0x0F00


对于比较大小的话,尤其是要去绕过一个数据比定义的数据要小,这时候无疑是无符号的整数溢出最好了
unsigned int16 packet_len,而且packet_len类型正好是unsigned,所以可以将数据设为0xFFFF就会变成
0x0000FFFF 这就自然比0x0F00小了而且也达到了溢出的效果。然后我们的0x0000FFFF就会被复制到堆中的buff里去了。
我们在来回顾一下mecopy_a2_a1函数,此函数将堆中的数据 packet_data 复制到大小3840 字节 CmdLine的堆栈缓冲区中。在复制数据时它会将所有出现的字节 0x2B 替换 0x33 为空字节。


所以大致的漏洞点我们已经确定了从 raw assembly 来直观看一下,packet_len会通过MOVSX (MOVSX :将源操作数的内容复制到目标操作数,并对扩展值进行符号扩展。在 64 位模式下,指令的默认操作大小为 32 位。)被加载到EAX寄存器中去。所以发送 packet_len 为0xFFFF 大小,它将被符号扩展为 0xFFFFFFFF 被视为负值并绕过长度检查。


当mecopy_a2_a1被调用的时候 packet_header和packet_len中的指定长度将从堆缓冲区复制到CmdLine[3840] 缓冲区造成溢出。
EXP分析
在知道了漏洞点在哪里后,我们就要去触发执行shellcode了,而且还需要绕过 Stack canary 所以要想一个好一点的办法。这里参考了tin-z的python脚本,然我们来看一下代码的具体实现


先是尝试创建一个套接字并连接到指定的主机和端口,通过flush(t_sleep=1)控制发送消息的速率这块也是编写利用脚本的小细节,recv_until(msg)从套接字接收数据,直到在接收的数据中检测到需要的msg。
而在get_header()函数中定义了 opcode_1 用于告诉服务端如何处理接收到的数据包。

这里使用 struct.pack("<Q", 0x323230326F6B45) 来打包操作码,其中 <Q 表示使用小端字节序格式化一个无符号长整数。数值 0x323230326F6B45在上述分析中提到了为了过前边的检查也就是cookie要包含Eko2022


前边函数都是建立套接字连接处理连接过程的细节,最后的get_shellcode函数才是关键,
在解析此利用脚本之前还需要了解一下IRETD
IRETD 是一个汇编语言指令,用于在 x86 架构的计算机中处理中断返回操作。在此操作期间,处理器将返回指令指针、返回码段选择器和 EFLAGS 映像从堆栈分别弹出 EIP 到 、 CS 和 EFLAGS 寄存器,然后继续执行中断的程序或过程。
这条指令的作用是从栈中恢复多个寄存器的值,并将程序的控制返回给中断或异常之前的代码流。
IRETD 是 32 位操作系统中使用的指令,而在 64 位系统中,相应的指令是 IRETQ。
还记得分析过程中的逻辑我们从客户端发送的请求保存在地址0x10000000中,然后每个字节 0x33和0x2B正文内部都设置为零。但在利用过程中我们使用了是 iretd 指令,在调用 iretd 之前,堆栈上应存在以下EIP、CS、EFLAGS、SP、SS。而x64 进程的默认 CS 值是 "0x33" 和 SS "0x23" ,但这两个会被设置为0,很明显出题者也考虑到这一块故意设置的。那我们就不能在发送的请求中插入这些值。
但是可以设置 CS 为0x23 因为它用于x86进程,并且代码地址空间低于32 位,随后的 SS 可以设置为有效的段选择器我们只需要找到一个类型为 Data, RW 的值。遍历了所有选择器最终就得到 0x53。
CS: 0x23 代码段选择器;SS: 0x53 堆栈段选择器。
使用 iretd 指令。在执行调用之前客户端的输入将保存在堆栈中。然后堆栈指针比缓冲区要低 0x38 个字节。这样通过执行 pop 指令,我们将能够制作 iretd 的参数。
这样最后跳转到 shellcode 后iretd 再次恢复 x64 位地址空间完成攻击过程。

回过头来看代码的buff部分的组成:首先szp是根据传入的 is_64 参数决定<Q 表示64位<I 表示32位。
ret_shellcode, cs, eflags, sp, ss: 这些变量分别包含了返回地址、代码段寄存器、EFLAGS 寄存器、堆栈指针和堆栈段寄存器的值。cs设置为上述计算好的0x23,
g0, g1_1, g1_2, g2: 这些字节字符串包含了具体的汇编指令,用于调整 CPU 状态或者跳转到特定的代码执行路径。具体的汇编指令就是下边这些这里就不过多阐述,比较关键的位置主要就是将EAX的值移动到段寄存器SS;将EAX加上0x25; IRETD: 从中断返回(并恢复标志寄存器),最后恢复堆栈。这几处在上文分析都有提及 buff就是正常计算器的shellcode了。

"MOV ESP, ECX"
"XOR EAX, EAX; INC EAX; OR EAX, 0x2A; MOV SS, EAX; MOV ESI, EAX; XOR EAX, 0x1B; OR EAX, 0x3; MOV EDI, EAX"
"SUB ECX, 0x38; POP EAX; POP EBX; POP EBX; POP EDX; POP EDX; PUSH ESI; PUSH ECX; PUSH EBX; PUSH EDI; ADD EAX, 0x25; PUSH EAX; IRETD"
"MOV RSP, RCX"

其实最关键的点在于将堆栈地址恢复为其原始值,修复SS, CS寄存器的值
我们不能直接设置CS寄存器,但是可以再次调用iretd通过irretd将RSP设置为256对齐的地址。
后边的内容就是构造正常的通信包了好让我们的shellcode能正常走到溢出的位置完成利用,比如发送消息 b"Hello\0\0\0\0",然后接收服务器数据直到收到 "Hi" 字节序列,并打印收到的内容等。


看一下它跑起来监听的端口就是31415 跟我们在最开始的静态分析是一样的。

然后直接运行攻击脚本python ip 31415 完成最终利用

漏洞点虽然不能看出,但在windows的利用过程包括寄存器的使用和cs还有ss的获取,还有此处利用过程一些body和header的构造还是蛮需要细心调试的,的而且这种socket接受数据的模式在客户端中也是蛮常见的,还是要经常温故知新。

0 条评论
某人
表情
可输入 255
目录