[2023数字网络安全人才挑战赛]Rev-WP

考察内容其实很简单,分析部分偏废话。

1 Game

1.1 附件

game的附件

1.15 考点

C++的文件读取、写入的反汇编分析

1.2 解题思路

1.根据调试过程很容易发现,很大一部分代码是游戏代码的一部分。在判断出这一点之后,我们跳过游戏代码对下面这一部分代码进行分析和理解。

   if ( dword_40E880 == dword_40E888 )
    {
      sub_401120(v23, v26);           //1
      memset(v30, 0, sizeof(v30));
      sub_4038B0(0, 0);
      v32 = 0;
      memset(v29, 0, sizeof(v29));
      sub_4038B0(0, 0);
      LOBYTE(v32) = 1;
      craeteWindow(900, 60, 0);
      sub_403810((int)v30, "flag.png", 0, 0, 0);
      sub_403840(0, 0, v30, 13369376);
      system("pause");
      sub_4037B0();
      Sleep(0x540BE3FFu);
      sub_403C50(v24, v27);
      v32 = -1;
      sub_403C50(v25, v28);
    }

2.从游戏代码中可以看出,每当吃掉一个豆豆,dword_B0E880的值就会加1。同时,dword_B0E888的初始值为0x280,绕过if判断,可以eax的值改成了0x280

if ( dword_B0E890[0] >= dword_B0F830 - dword_B0F838
  && dword_B0E890[0] <= dword_B0F838 + dword_B0F830
  && dword_B0E894[0] >= dword_B0F834 - dword_B0F838
  && dword_B0E894[0] <= dword_B0F838 + dword_B0F834 )
{
  ++dword_B0E880; //吃掉豆豆+1
  byte_B0F83C = 0;

if ( dword_40E880 == dword_40E888 )
{
.text:00B01BB7 mov     eax, dword_B0E880 
.text:00B01BBC cmp     eax, dword_B0E888  // dword_B0E888   = 0x280
.text:00B01BC2 jnz     loc_B01900

3.进入sub_B01120,创建了一个名为v16数组,开始进行了一系列的初始化操作,实际v16为std::ifstream对象,并打开了指定的文件进行读取。

  memset(v16, 0, sizeof(v16)); //
  v16[0] = (int)&unk_B0AB20;   //全局未初始化变量
  std::ios::ios(&v16[28]);     //初始化v16数组,包括设置 iostate 为0(即正常状态)和设置 flags 为0。
  v20 = 0;
  v15 = 1;
  std::istream::istream(v16, &v16[4], 0, 0); //初始化缓冲区
  v20 = 1;
  *(int *)((char *)v16 + *(_DWORD *)(v16[0] + 4)) = (int)&std::ifstream::`vftable';
  *(int *)((char *)&v16[-1] + *(_DWORD *)(v16[0] + 4)) = *(_DWORD *)(v16[0] + 4) - 112;
  std::streambuf::streambuf(&v16[4]);//设置 v16 数组的一部分为 std::filebuf 对象的虚函数表指针
  LOBYTE(v20) = 2;
  v16[4] = (int)&std::filebuf::`vftable'//; 设置 v16 数组的一部分为 std::filebuf 对象的虚函数表指针。
  LOBYTE(v16[22]) = 0;
  BYTE1(v16[19]) = 0;
  std::streambuf::_Init(&v16[4]);//初始化呀初始化呀
  v16[20] = dword_B0F848;
  v16[23] = 0;
  v16[21] = dword_B0F84C;
  v16[18] = 0;
  LOBYTE(v20) = 3;

4.在继续分析之前,先调试v3的值,发现它是sinke文件的内容。然后将其与上一步中提到的dword_B0E880进行异或操作,并将结果存储在v3中。sub_B031E0可以初步判断为读取的操作。

 if ( !sub_B02AF0((int)&v16[4], "./sinke", 33, v0) )
    std::ios::setstate((char *)v16 + *(_DWORD *)(v16[0] + 4), 2, 0);
  v20 = 4;
  LOBYTE(v15) = 0;
  BYTE1(v14) = 0;
  v1 = *(int *)((char *)&v16[14] + *(_DWORD *)(v16[0] + 4));
  LOBYTE(v14) = v1 == 0;
  Block[0] = 0;
  Block[1] = 0;
  v19 = 0;
  sub_B031E0(v1, v14, 0, 1, v15); //这里执行了文件读取,存储在了block中
  LOBYTE(v20) = 5;
  if ( !sub_B02A80(&v16[4]) )
    std::ios::setstate((char *)v16 + *(_DWORD *)(v16[0] + 4), 2, 0);
  v2 = 0;
  v3 = Block[0];
  v4 = Block[1] - Block[0];
  if ( Block[1] != Block[0] )
  {
    do
      v3[v2++] ^= dword_B0E880; //这里对v3的数据先进行了操作,所以先调试v3是什么
    while ( v2 < v4 );
  }

5.又初始化了一个std::ifstream对象v17

memset(v17, 0, sizeof(v17));
  v17[0] = (int)&unk_B0AB18;
  std::ios::ios(&v17[26]);
  LOBYTE(v20) = 6;
  v15 = 3;
  std::ostream::ostream(v17, &v17[1], 0, 0);
  v20 = 7;
  *(int *)((char *)v17 + *(_DWORD *)(v17[0] + 4)) = (int)&std::ofstream::`vftable';
  *(int *)((char *)&v16[45] + *(_DWORD *)(v17[0] + 4)) = *(_DWORD *)(v17[0] + 4) - 104;
  std::streambuf::streambuf(&v17[1]);
  LOBYTE(v20) = 8;
  v17[1] = (int)&std::filebuf::`vftable';
  LOBYTE(v17[19]) = 0;
  BYTE1(v17[16]) = 0;
  std::streambuf::_Init(&v17[1]);
  v17[17] = dword_B0F848;
  v17[20] = 0;
  v17[18] = dword_B0F84C;
  v17[15] = 0;
  LOBYTE(v20) = 9;

6.将v3写进了文件对象

 if ( !sub_B02AF0((int)&v17[1], "flag.png", 34, v5) )
    std::ios::setstate((char *)v17 + *(_DWORD *)(v17[0] + 4), 2, 0);
  LOBYTE(v20) = 10;
  std::ostream::write(v17, v3, v4, 0); //写入

7.将一个 std::ifstream 类型的对象 v17 转换为 std::ofstream 类型的对象,具体做法是通过修改 v17 对象的虚表指针实现的。可以使用虚表指针实现对象类型转换,因为虚表指针指向一个对象的虚函数表,而不同类型的对象的虚函数表不同,因此通过修改虚表指针,可以改变对象的类型。

  *(int *)((char *)v17 + *(_DWORD *)(v17[0] + 4)) = (int)&std::ofstream::`vftable';
  *(int *)((char *)&v16[45] + *(_DWORD *)(v17[0] + 4)) = *(_DWORD *)(v17[0] + 4) - 104;
  LOBYTE(v20) = 11;
  v17[1] = (int)&std::filebuf::`vftable';
  if ( v17[20] && *(int **)v17[4] == &v17[16] )
  {
    v7 = v17[21];
    v8 = v17[22] - v17[21];
    *(_DWORD *)v17[4] = v17[21];
    *(_DWORD *)v17[8] = v7;
    *(_DWORD *)v17[12] = v8;
  }
  if ( LOBYTE(v17[19]) )
    closeFile(&v17[1]);
  std::streambuf::~streambuf<char,std::char_traits<char>>(&v17[1]);
  std::ostream::~ostream<char,std::char_traits<char>>(&v17[2]);
  std::ios::~ios<char,std::char_traits<char>>(&v17[26]);//这三行代码分别用于销毁v17所依赖的std::streambuf、std::ostream和std::ios对象。这是因为当一个std::ifstream对象被转换为std::ofstream对象时,它的内部状态也会相应地改变,需要进行一些清理工作。

综合来看,这段代码实现了文件读取、写入和异或操作,得到了flag.png这张图片。

但是我将dword_40E880 变量的值,在比较前改成0x280,并没有成功异或这张图片,直接拿PNG头和sinke的文件内容进行异或,发现是0x80,即可异或出该图片。

附:C++常用代码读取文件 

#include <iostream>
#include <fstream>
#include <string>

int main() {
    std::ifstream file("example.txt");
    std::string line;

    if (file.is_open()) {
        while (std::getline(file, line)) {
            std::cout << line << '\n';
        }
        file.close();
    } else {
        std::cout << "Unable to open file\n";
    }

    return 0;
}

2 EasyKernel.ko

2.1 附件

EasyKernel.ko

2.15 考点

内核文件反汇编分析

xxtea加密算法特征识别、解密

2.2 解题思路

查看内核版本,若与本机不一致,可以使用QEMU创建一个虚拟的子系统进行调试。(懒)附内核下载地址,还要编译,详细调试步骤参见:notes/kernel-qemu-gdb.md at master · beacer/notes · GitHub 附linux内核下载地址:kernel/git/stable/linux.git - Linux kernel stable tree

imk3@ubuntu:~/Desktop# file easykernel.ko 
easykernel.ko: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), BuildID[sha1]=3fbaa4d3447147db8bbc65202dd6916ed91c2f01, not stripped

imk3@ubuntu:~/Desktop# modinfo easykernel.ko
filename:       /home/imk3/Desktop/easykernel.ko
license:        GPL
depends:        
retpoline:      Y
name:           sample
vermagic:       5.10.99 SMP mod_unload 

insmod: ERROR: could not insert module /home/imk3/Desktop/easykernel.ko: Invalid module format

1."init_mod"函数是一个在内核模块被加载时会自动调用的函数。该函数的主要作用是初始化内核模块并注册相关设备及资源。

  • register_chrdev_region注册一个设备名称为“special”设备号为46137344的字符设备
__int64 init_module()
{
  unsigned int v0; // r12d
  __int64 v2; // rsi

  v0 = register_chrdev_region(46137344LL, 1LL, "special");
  //int register_chrdev_region(dev_t first, unsigned int count, const char *name); 参数first指定了请求的第一个设备号,count指定了需要的设备号数量,name为设备的名称。
  if ( !v0 )
  {
    cdev_init(&data, &dev_fops);
    v2 = (unsigned int)cdev_add(&data, 46137344LL, 1LL);
    printk(&unk_6B5, v2);
    printk(&unk_6E8, v2);
  }
  return v0;
}
  • 在模块的初始化过程中,cdev_init函数会被用来初始化cdev结构体(也称字符设备结构体)和file_operations结构体(也称文件操作结构体),并将它们关联起来。其中cdev结构体定义了字符设备的属性,而file_operations结构体定义了字符设备对应的文件操作函数。

  • printkLinux内核中一个常用的函数,用于在内核模块进行调试时输出相关信息,可以将输出的信息打印到系统日志中,以便于调试和问题排查。

2.dev_read 函数的原型为 size_t dev_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos),用于从设备中读取数据并将其存储到用户空间的缓冲区中。在此代码中,函数被修改以将已经获得flag的标签和未获得flag的标签传递给用户缓冲区,使用了copy_to_user(a2, v6, v4)。

此外,从dev_write的结果byte_CA8 中判断输入的flag是否验证成功,如果为0,则表示成功写入flag,输出成功。

unsigned __int64 __fastcall dev_read(__int64 a1, __int64 a2, unsigned __int64 a3, _QWORD *a4)
{
  unsigned __int64 v4; // r12
  const char *v6; // rsi
  unsigned __int64 result; // rax

  v4 = a3;
  v6 = "You got your flag!\n";
  if ( !byte_CA8 )
    v6 = "Your haven't got your flag!\n";
  if ( (unsigned __int64)(byte_CA8 == 0 ? 9 : 0) + 19 - *a4 <= a3 )
    v4 = (byte_CA8 == 0 ? 28LL : 19LL) - *a4;
  result = 0LL;
  if ( v4 )
  {
    if ( v4 > 0x1D )
    {
      _warn_printk("Buffer overflow detected (%d < %lu)!\n", 29LL, v4);
      BUG();
    }
    if ( copy_to_user(a2, v6, v4) ) //unsigned long copy_to_user(void __user *to, const void *from, unsigned long n)这个函数实现的过程中会对用户空间指针进行验证,确保指针有效并且不会指向内核空间。如果指针无效,函数将返回非零值,并且不会复制任何数据。

    {
      return -14LL;
    }
    else
    {
      *a4 += v4;
      return v4;
    }
  }
  return result;
}

3.通过观察函数dev_write的实现,发现其中包含一个加密算法。这个算法有一个非常普遍的特点,就是对数据进行右移5位和左移4位(等同于乘以16)。这个特点在Tea加密中非常常见,包括xteaxxtea都有这个特点。因此,我们可以很容易地确定这是一个类Tea加密算法。此外,我们还可以发现这个算法中包含右移3位和左移2位,这是xxtea算法的一个特点。

enc[0] = 0x7FB3950C883B3AALL;
enc[1] = 0x7AB57E2775BC5959LL;
enc[2] = 0xADA35753C0249800LL;
enc[3] = 0x6E14AF04BF1D493FLL;
key[0] = 0xE000004DBLL;
key[1] = 0x2A600000017LL;
s1_8 = s1[8];                                 // 初始化数据
sum = 0x67616C66;
v5 = 678;
v22 = 23;
s1_1 = s1[1];
v7 = 1243;
v8 = 14;
v21 = 678;
s1_0 = s1[0];
v20 = 1243;
s1_2 = s1[2];
v19 = 14;
s1_3 = s1[3];
s1_5 = s1[5];
s1_6 = s1[6];
v18 = 23;
s1_4 = s1[4];
s1_7 = s1[7];
while ( 1 )
{
s1_0 += ((s1_8 ^ v8) + (s1_1 ^ sum)) ^ (((4 * s1_1) ^ (s1_8 >> 5)) + ((s1_1 >> 3) ^ (16 * s1_8)));// (((z>>5^y<<2) + (y>>3^z<<4)) ^ ((sum^y) + (key[(p&3)^e] ^ z)))  
s1_1 += ((s1_0 ^ v7) + (s1_2 ^ sum)) ^ (((4 * s1_2) ^ (s1_0 >> 5)) + ((16 * s1_0) ^ (s1_2 >> 3)));
s1_2 += ((s1_1 ^ v5) + (sum ^ s1_3)) ^ (((4 * s1_3) ^ (s1_1 >> 5)) + ((16 * s1_1) ^ (s1_3 >> 3)));
s1_3 += ((s1_2 ^ v18) + (sum ^ s1_4)) ^ (((4 * s1_4) ^ (s1_2 >> 5)) + ((16 * s1_2) ^ (s1_4 >> 3)));
s1_4 += ((s1_3 ^ v19) + (s1_5 ^ sum)) ^ (((4 * s1_5) ^ (s1_3 >> 5)) + ((16 * s1_3) ^ (s1_5 >> 3)));
s1_5 += ((s1_4 ^ v20) + (s1_6 ^ sum)) ^ (((4 * s1_6) ^ (s1_4 >> 5)) + ((16 * s1_4) ^ (s1_6 >> 3)));
s1_6 += ((s1_5 ^ v21) + (s1_7 ^ sum)) ^ (((4 * s1_7) ^ (s1_5 >> 5)) + ((16 * s1_5) ^ (s1_7 >> 3)));
s1_7 += ((s1_6 ^ v22) + (sum ^ s1_8)) ^ (((16 * s1_6) ^ (s1_8 >> 3)) + ((4 * s1_8) ^ (s1_6 >> 5)));
v16 = (s1_7 ^ v23) + (s1_0 ^ sum);
sum += 0x67616C66;
s1_8 += v16 ^ (((16 * s1_7) ^ (s1_0 >> 3)) + ((4 * s1_0) ^ (s1_7 >> 5)));
if ( sum == 0xD89114C8 )
  break;
v8 = *((_DWORD *)key + ((sum >> 2) & 3));
v5 = *((_DWORD *)key + (((unsigned __int8)(sum >> 2) ^ 2) & 3));
v7 = *((_DWORD *)key + (((unsigned __int8)(sum >> 2) ^ 1) & 3));
v19 = v8;
v23 = v8;
v18 = *((_DWORD *)key + (~(unsigned __int8)(sum >> 2) & 3));
v20 = *((_DWORD *)key + (((unsigned __int8)(sum >> 2) ^ 5) & 3));
v21 = *((_DWORD *)key + (((unsigned __int8)(sum >> 2) ^ 6) & 3));
v22 = *((_DWORD *)key + (((unsigned __int8)(sum >> 2) ^ 7) & 3));
}
s1[5] = s1_5;
s1[6] = s1_6;
s1[8] = s1_8;
s1[0] = s1_0;
s1[1] = s1_1;
s1[2] = s1_2;
s1[3] = s1_3;
s1[4] = s1_4;
s1[7] = s1_7;
byte_CA8 = memcmp(s1, enc, 0x24uLL) == 0;

在代码分析中,发现enc数据的伪代码是有问题的,因为在s1中设定了数组的9个元素,理应enc也是数组的9个元素,但是分析出来的伪代码只有4个,这说明存在一些问题。因此需要查看汇编代码以确定具体情况。

对于数组的数据存储,一般先放到低地址,然后逐渐增加。在实际栈中,enc处应当是低地址,anoymous_0为高地址,实际anoymous_0为最后一个元素,因此按照9个元素来计算,每个元素的类型应当为4个字节,在存储的时候,此处赋值也是按照4字节4字节赋值,即低4个字节先赋值给上一个元素,高4个字节赋值给下一个元素。同理key

mov     rax, 7FB3950C883B3AAh
mov     [rsp+0B8h+anonymous_0], 468312C4h  //这里实际是最后一个元素
mov     [rsp+0B8h+enc], rax //第一个元素
mov     rax, 7AB57E2775BC5959h
mov     [rsp+0B8h+enc+8], rax
mov     rax, 0ADA35753C0249800h
mov     [rsp+0B8h+enc+10h], rax
mov     rax, 6E14AF04BF1D493Fh
mov     [rsp+0B8h+enc+18h], rax

经过分析,发现在此处的加密算法中,delta值并不是采用黄金比例,而是使用了十六进制值0x67616C66。另外,加密算法共进行了12轮,最终sum的中止值为0xD89114C8。网上找一个算法样例,一致,直接用。

#include <stdio.h>  
#include <stdint.h>  
#define DELTA 0x67616C66
#define MX (((z>>5^y<<2) + (y>>3^z<<4)) ^ ((sum^y) + (key[(p&3)^e] ^ z)))  
  
void btea(uint32_t *v, int n, uint32_t const key[4])  
{  
    uint32_t y, z, sum;  
    unsigned p, rounds, e;  
    if (n > 1)            /* Coding Part */  
    {  
        rounds = 6 + 52/n;  
        sum = 0;  
        z = v[n-1];  
        do  
        {  
            sum += DELTA;  
            e = (sum >> 2) & 3;  
            for (p=0; p<n-1; p++)  
            {  
                y = v[p+1];  
                z = v[p] += MX;  
            }  
            y = v[0];  
            z = v[n-1] += MX;  
        }  
        while (--rounds);  
    }  
    else if (n < -1)      /* Decoding Part */  
    {  
        n = -n;  
        rounds = 6 + 52/n;  
        sum = rounds*DELTA;  
        y = v[0];  
        do  
        {  
            e = (sum >> 2) & 3;  
            for (p=n-1; p>0; p--)  
            {  
                z = v[p-1];  
                y = v[p] -= MX;  
            }  
            z = v[n-1];  
            y = v[0] -= MX;  
            sum -= DELTA;  
        }  
        while (--rounds);  
    }  
}  
  
  
int main()  
{  
uint32_t v[] = {

    0xC883B3AA, 0x7FB3950,
    0x75BC5959, 0x7AB57E27,
    0xC0249800, 0xADA35753,
    0xBF1D493F, 0x6E14AF04,
  0x468312c4,

  
};

uint32_t const k[4] = {
    0x4DB, 0xE,
    0x17, 0x2A6,
};
    int n= 9;
    uint32_t num = 0;
    btea(v, -n, k);  
      for (int i = 0; i < 10; i++) {
            num = v[i];
            uint8_t byte1 = num & 0xFF;
            uint8_t byte2 = (num >> 8) & 0xFF;
            uint8_t byte3 = (num >> 16) & 0xFF;
            uint8_t byte4 = (num >> 24) & 0xFF;
        
            // 输出16进制字符串
            printf("%c", byte1);
            printf("%c", byte2);
            printf("%c", byte3);
            printf("%c", byte4);
    }
  return 0;  
}  
//541c290d-e89f-4539-8d24-2ccbd1ead8ae
  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值