-
题目分类:binary
-
题目分值:300
一个玄学的 flag 计算机,平常的时候只会摸鱼,但如果在合适的时间运行它的话,它就可以告诉你真正的 flag。
提示 1:请大家着重于最后生成打印 flag 的逻辑,忽略其他无关代码
提示 2:本题的旧版附件(点击下载)存在问题,我们已经将其修复,请各位选手解题时使用新版附件(点击下方按钮下载)。题目的 flag 保持不变。
此题目基于 https://github.com/skeeto/dosdefender-ld31 修改而成。
由于我太菜了,所以引入了一个 BUG 导致题目下线修改后上线。给大家带来了不便,十分抱歉。
这道题主要想展现的效果是在 DOS 和 Windows 上都能运行的程序。在 Windows 上运行将弹出一个对话框。
这和我们在 DOS 上运行 Windows PE 程序在 DOS 中输出"This program cannot be run in DOS mode"相照应。如果我们把程序载入 010editor就会发现一些猫腻。
我们会发现原来的 DOS stub 充满了代码。实际上我是先编译了一个 COM 可执行文件,然后手动为 COM 文件加上 DOS 头。所以在此过程中 DOS 中的一些值是手工进行设置的。 然后我将能够运行的 DOS 可执行文件设置为 visual studio 2019 中的 exe 的 DOS stub。最终编译成了你们现在看到的样子。
我们将它拖入ida
我们选择让 IDA 分析 MS-DOS executeble。 然后我们观察 start 函数。
我们发现它调用了一个函数,然后执行了 DOS exit 中断,IDA 已经将其标注了出来。
这里用了一个 CPU 指令前缀的编译方法,使得我们的 80386 能够在实模式使用 32 位操作数。0x66
:它属于 Prefix group 3,Operand-size override prefix。
有了这样一个 opcode 会有怎么样的行为呢?
感谢 @HarukaMa:#45 (comment)
出题人:😭 现在修一下图片错误。“此题中 DS 和 SS 的差值就会导致直接在 IDA 中转换内存引用会产生不正确的结果。” 这是一部分原因,实际上偏移0x100。
在 x86 realmode 下 cpu 通过 segment selector 来选择数据。
这样的组合使得我们的程序能够索引地址 0 - 2 ** 20 的数据。
约定的调用组合为 CS :IP、 SS :SP
CS 是 code segment的缩写。SS 是 Stack segment的缩写。 DS 是 Data segment的缩写。
一般而言段选择字在 realmode 里翻译为段寄存器。一般在protect mode 和 long mode 中称为段选择子。他们主要是为内存分段而存在的。内存分段又是为了区分数据和代码存在的同时一些段的属性还承担着保护的作用,比如不可写,不可读的功能等。
将ida改为以下的设置即可。实际就是把它删了然后重建一个就行。这里保证cs=ds=ss
可以看到 operand size 变为了 32bit。 我们分析 102f 这个函数。
可以看到使用了大量使用这个技巧的痕迹,这就是为什么我们能在实模式使用 32bit 寄存器的原因。
asm (".code16gcc\n"
"call dosmain\n"
"mov $0x4C,%ah\n"
"int $0x21\n");
最先展开的 include 项目中有我们的 init.h 。 这个文件中的一个伪指令 ".code16cc" 代表着让gcc生成 16bit realmode 模式的汇编指令。所以这是为什么大量指令前都有 0x66 的原因。当我们的操作数是类似于 uint32 声明时,0x66 就会起作用。 逆向的核心过程就是明白程序干了哪些事情,橙色的操作数代表着地址,我们将鼠标悬停在上面,然后右键把它类型转换一下。
IDA 就能识别字符串了。 ps: 作为一个CTF玩家,我最喜欢的事情就是按 F5 ,hackergame 是为了让人能学到东西,而不是千篇一律的难题,不是为了难而难。所以我程序在 realmode 下,IDA 失效了,这样大家都能从 “0” 接触汇编。 显而易见的是,参数是字符串的一般就是我们的类似的 printf 函数。只不过这里是 vga 的输出而已。
我们看整个控制流,发现程序并不复杂,大多数都是控制显示的逻辑。 大家可以对着源代码再看一遍,这个函数就是对应的 main 函数。
下面讲下程序的核心逻辑。
static uint32_t gen_random_a()
{
a = get_time() % 58379;
// a = 26141;
x0 = 1103515245;
}
static uint32_t myrand(void)
{
x0 = x0 * a + 12345678;
return x0;
}
gen_random_a();
uint32_t rand_gen[15];
for(uint32_t i=0;i<15;i++)
{
rand_gen[i] = myrand();
}
for(uint32_t i=0;i<15;i++)
{
for(uint32_t j=0;j<15;j++)
{
get_matrix[i] += (my_matrix[i*15+j] * rand_gen[j]) & (0xffff);
get_matrix[i] = get_matrix[i] & 0xffff;
}
}
char out[40];
out[30] = '$';
out[31] = '$';
out[32] = '$';
out[33] = '$';
unsigned short aim[30]={221, 49078,12436, 39423, 44156, 25529, 22179,10906, 15839, 27165, 45705, 55062,58013, 7081, 14308, 136, 49064,12481, 39404, 44086, 25520, 22263,10929, 15818, 27144, 45774, 55045,58097, 7156, 14313};
for(short i=0; i<30;i++)
{
out[i] = (char)(aim[i] ^ get_matrix[i%15]);
}
删除掉我们的不重要的 delay 逻辑和输出逻辑就剩下这些了。很明显初始化的时候会取一个 0~58379 之间的数字作为 a ,我在生成flag的时候使用的是 26141。然后使用线性同余发生器生成15个伪随机数。 下图是生成15个伪随机数的逻辑代码:
下图是生成目标异或数组的核心逻辑:
下图是 xor 常量的核心逻辑,就是这段在使用这个库的时候出了 bug 。
最后 dump 出常量,小于 2**16 的爆破量就可以将 flag 爆破出来。
下面给出的代码是生成 get_matrix[] 数组的代码
#include <iostream>
#include <cstdint>
using namespace std;
uint32_t a,x0,b;
static uint32_t gen_random_a()
{
// a = get_time() % 58379;
a = 26141;
x0 = 1103515245;
}
static uint32_t myrand(void)
{
x0 = x0 * a + 12345678;
return x0;
}
uint32_t get_matrix[15];
uint32_t my_matrix[226]={20597,19141,29258,17804,29076,28746,24890,28979,26196,31833,26624,24774,18916,29028,24033,22913,23436,25750,26539,21652,31296,22446,16506,21949,22761,30221,29477,29617,16497,23022,23179,30781,23877,29171,31665,26534,32159,22583,27525,28708,31216,17158,31988,32190,23747,21272,21278,24727,29984,25303,23445,23119,23155,26346,26389,30747,28948,31418,21323,31758,30911,18790,21312,25099,22348,25409,29357,22180,23588,28794,18133,25624,21972,23401,24821,31369,25187,31517,19840,28836,20794,20239,24523,30814,24016,17954,21227,16691,30290,23391,20482,24822,31968,30651,27908,22690,30875,31003,31747,19978,25482,18563,30143,27788,26658,26295,23244,27086,26456,24251,28647,22783,27460,19187,23252,24078,19203,26251,18113,19542,24533,16666,24038,32744,28670,30438,26379,18591,30109,26509,20947,27696,22945,27542,32128,25416,21675,19389,27085,29380,20163,21102,30936,30862,18230,21904,16938,16579,20641,27551,22740,24666,16836,23306,27661,26506,28623,29816,20166,29405,23982,30046,19365,24926,19029,32448,17567,17156,18678,28594,19769,28631,25769,31309,24457,30625,21825,29811,17112,31370,25345,24333,24005,31606,30942,21441,30599,22894,18015,19994,27901,26868,21948,27614,23449,21289,19588,19955,28133,16696,31509,26219,19946,27895,28760,28547,28315,16614,26006,17129,24769,24608,17714,17682,18532,17597,29247,28789,27011,29841,32640,17508,27662,23548,29514};
int main() {
gen_random_a();
uint32_t rand_gen[15];
for(uint32_t i=0;i<15;i++)
{
rand_gen[i] = myrand();
}
for(uint32_t i=0;i<15;i++)
{
for(uint32_t j=0;j<15;j++)
{
get_matrix[i] += (my_matrix[i*15+j] * rand_gen[j]) & (0xffff);
get_matrix[i] = get_matrix[i] & 0xffff;
}
}
for(int i = 0; i <15;i++)
{
cout << get_matrix[i] << " ";
}
return 0;
}
每次改变a的值然后和目标数组 xor
unsigned short aim[30]={221, 49078, 12436, 39423, 44156, 25529, 22179, 10906, 15839, 27165, 45705, 55062, 58013, 7081, 14308, 136, 49064, 12481, 39404, 44086, 25520, 22263, 10929, 15818, 27144, 45774, 55045, 58097, 7156, 14313};
for(short i=0; i<30;i++)
{
out[i] = (char)(aim[i] ^ get_matrix[i%15]);
}
然后都是可见字符并且开头以"flag{"的就是flag。