这里是……后室?

第一节:题目

Let escape the backroom

第二节:过程

pwn文件,标准流程先打开IDA看看main函数

int __fastcall main(int argc, const char **argv, const char **envp)
{
  char v4; // [rsp+Bh] [rbp-A5h] BYREF
  unsigned int seed; // [rsp+Ch] [rbp-A4h]
  struct sigaction act; // [rsp+10h] [rbp-A0h] BYREF
  unsigned __int64 v7; // [rsp+A8h] [rbp-8h]

  v7 = __readfsqword(0x28u);
  act.sa_handler = (__sighandler_t)segv_handler;
  sigemptyset(set: &act.sa_mask);
  act.sa_flags = 0;
  sigaction(sig: 11, &act, oact: nullptr);
  sigaction(sig: 7, &act, oact: nullptr);
  setvbuf(stream: stdout, buf: nullptr, modes: 2, n: 0);
  seed = time(timer: nullptr);
  srand(seed);
  wel();
  __isoc99_scanf(a1: &unk_259D, a2: &v4);
  if ( v4 == 89 || v4 == 121 )
    level0();
  if ( v4 == 78 || v4 == 110 )
  {
    puts(s: &byte_25A0);
    puts(s: &byte_25C8);
  }
  else
  {
    puts(s: &byte_25E1);
  }
  return 0;
}

这里setvbuf分析出输出无缓冲,而sigaction又防止了程序不会直接崩溃。
关键在于
  if ( v4 == 89 || v4 == 121 )
    level0();

这里题目引用了level0函数,我们进去看看
void __noreturn level0()
{
  int v0; // [rsp+Ch] [rbp-44h] BYREF
  int v1; // [rsp+10h] [rbp-40h]
  int v2; // [rsp+14h] [rbp-3Ch]
  int v3; // [rsp+18h] [rbp-38h]
  unsigned int random; // [rsp+1Ch] [rbp-34h]
  _QWORD v5[6]; // [rsp+20h] [rbp-30h] BYREF

  v5[5] = __readfsqword(0x28u);
  v0 = 0;
  memset(v5, 0, 40);
  v3 = 0;
  puts(s: &byte_2338);
  puts(s: &byte_2370);
  puts(s: &byte_23D8);
  puts(s: &byte_2430);
  while ( 1 )
  {
    puts(s: "====================");
    printf(format: &byte_2440, (unsigned int)v0);
    if ( v0 > 19 )
      real();
    puts(s: "====================");
    puts(s: &byte_2458);
    v1 = 0;
    __isoc99_scanf();
    --v1;
    puts(s: &byte_2480);
    v2 = 0;
    __isoc99_scanf();
    if ( v2 > 0 )
    {
      if ( v2 > 20 )
      {
        puts(s: &byte_20FC);
        usleep(useconds: 0x7A120u);
        puts(s: &byte_20FC);
        usleep(useconds: 0x7A120u);
        puts(s: &byte_20FC);
        usleep(useconds: 0x7A120u);
        puts(s: &byte_24C8);
        exit(status: 0);
      }
      *((_DWORD *)v5 + v1) += v2;
      puts(s: &byte_24FF);
      if ( v3 > 16 )
        die();
      random = get_random(a1: 1, a2: 3);
      success(a1: random, a2: &v0);
      v3 += random;
    }
    else
    {
      puts(s: &byte_24A0);
    }
  }
}

我们惊讶地注意到了
      *((_DWORD *)v5 + v1) += v2;

其中的v1直接scanf来,没有做检查,虽然v2——驻留时间要在(0,20]之间,但是房间号v1没有检查,这也为我们后面越界埋下伏笔

之前的函数我们分析发现了一个很重要的问题,就是

if ( v0 > 19 ) real(); 
if ( v3 > 16 ) die();  

而get_random是(1,3)说明了我们永远没办法通过正常手段通关,v3 = v1,到了16就自动死了,就算你最好情况当时v3=16,加了random的3 也是19,而v1要20才能过。

但是虽然后面还要分析各种函数,都比不上自己玩两把
可以看到下面我对于房间号尝试了越界,然后也试过时间越界,但是显然在程序里,时间是不能越界的
我们观察一下,房间号如果越界都会去哪里呢?

发现了吗?如果v1越界了,往负的地址走,而数组底 rbp-0x30和计数rbp0x44中间差了20个字节也就是5个int,当房间号是-4的时候,v1的下标就是-5,最后算出来的落点是rbp-0x44,就会落在杏仁水计数上,我们就可以一口气加v2瓶杏仁水.。
而v2决定了最后的杏仁水加了多少,那就写20。

你醒啦?
恭喜你,你卡入了后室(?
不过,要离开这里也十分简单~
你只需要收集二十瓶杏仁水并交给管理者,就可以回到现实世界。
你,准备好了吗?(Y/N)
y
现在你的眼前有10扇门,编号为1~10。
你每次可以任选一扇门,调查任意时间,每次调查都有概率获得杏仁水。
但是,随着累计驻留时间的增加,实体也会越来越有可能发现你。
助你好运~
====================
杏仁水数量:0
====================
请选择要调查的房间号(1~10):
11
请决定驻留时间(1~20):
20
(你推开了门……)
……
……
……
你平安完成了调查。
杏仁水+1。
====================
杏仁水数量:1
====================
请选择要调查的房间号(1~10):
-1
请决定驻留时间(1~20):
-4
临阵脱逃么?有点意思。
====================
杏仁水数量:1
====================
请选择要调查的房间号(1~10):
11
请决定驻留时间(1~20):
-4
临阵脱逃么?有点意思。
====================
杏仁水数量:1
====================
请选择要调查的房间号(1~10):
11
请决定驻留时间(1~20):
1
(你推开了门……)
……
……
……
你平安完成了调查。
杏仁水+3。
====================
杏仁水数量:4
====================
请选择要调查的房间号(1~10):
-4
请决定驻留时间(1~20):
20
(你推开了门……)
……
……
……
你平安完成了调查。
杏仁水+1。
====================
杏仁水数量:25
====================   
看来你成功收集了二十瓶杏仁水呢。
那么按照约定,我将送你回到现实。
……
(你感到一阵眩晕,随后,眼前出现了熟悉的城市)
等一下,
喝杯水再走吧,放心,只是普通的水(笑)
请选择一杯水(1~9):
10
你喝了一口水。
……
……
……
这白开水怎么没味儿啊?
QCTF{We1c0m3_7o_r3@1_w0r1d}

最终flag
QCTF{We1c0m3_7o_r3@1_w0r1d}