考察内容其实很简单,分析部分偏废话。
1 Game
1.1 附件
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 附件
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
结构体定义了字符设备对应的文件操作函数。 printk
是Linux
内核中一个常用的函数,用于在内核模块进行调试时输出相关信息,可以将输出的信息打印到系统日志中,以便于调试和问题排查。
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
加密中非常常见,包括xtea
和xxtea
都有这个特点。因此,我们可以很容易地确定这是一个类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