Google CTF Qualifiers 2020 - Root Power

2020-08-25 ctf reverse engineering

A reverse engineering challenge I worked on for Google CTF Qualifiers 2020.

I played with cr0wn 🇬🇧, which came 16th and qualified for the next stage.

We were provided with a virtual machine disk image and had to recover the root password.

Outline

  1. First look at disk image.
  2. Access the filesystem.
  3. Discover the authentication mechanism.
  4. Reverse engineering a kernel module.
  5. Discovering what initramfs contains and does.
  6. Reverse engineering an AML file.
  7. Win.

Tl;dr

The given image has a pluggable authentication module that checks if a char device /dev/chck reads 1 when root login is attempted. The kernel module responsible for said device reads from ACPI, logic of which is within a ACPI Machine Language (AML) file. Decompiling it shows that it is a PS/2 keyboard device, and the make/break codes form the flag.

First look

The archive contains a disk image and a script to run it in QEMU. GRUB menu reveals that it is Arch Linux, and you are greeted with a login prompt.

Grub Menu Login Prompt

Binwalk suggests there’s a EXT filesystem at offset 0x100000.

Binwalk

The filesystem

The filesystem can be mounted with:

mkdir -p mnt && sudo mount -o loop,offset=1048576 disk.img mnt

My first instinct was to check /etc/shadow and the boot files, the former was a rabbit hole. I also checked initramfs to see if there are any files I might’ve missed, it contains ssdt.aml, we’ll come back to this later.

Authentication mechanism

A feature that authenticated users in Linux was PAM. So I checked out the modules in /etc/pam.d/system-auth, and pam_chck.so was mentioned.

pam_chck.so looks for the root user, then returns 0 (success) if check_device() == 1.

ulong pam_sm_authenticate(undefined8 param_1)
{
  int r;
  ulong ret;
  char *user;
  uint _r;
  
  _r = pam_get_user(param_1,&user,"Username: ",&user);
  if (_r == 0) {
    r = strcmp(user,"root");
    if (r == 0) {
      fwrite("Password: ",1,10,stderr);
      r = check_device();
      if (r == 1) {
        fprintf(stderr,"\n\nWelcome %s\n",user);
        ret = 0;
      }
      else {
        fwrite("Wrong username or password",1,0x1a,stderr);
        ret = 6;
      }
    }
    else {
      ret = 6;
    }
  }
  else {
    ret = (ulong)_r;
  }
  return ret;
}

check_device() is a simple function that reads 2 bytes from /dev/chck and returns the result as an integer.

int check_device(void)
{
  int ret;
  char buf [2];
  FILE *fd;
  
  fd = fopen("/dev/chck","r");
  fgets(buf,2,fd);
  fclose(fd);
  ret = atoi(buf);
  return ret;
}

What exactly provides this device?

Chck kernel module

Devices in /dev/ are usually added through kernel modules, so I looked for .ko files in the image. Immediately, chck.ko jumped out so it was given the decompiler treatment, indeed it handles /dev/chck.

undefined8 chck_read(undefined8 param_1,undefined8 param_2,undefined8 param_3,undefined8 param_4)
{
  int chck_r;
  undefined8 r;
  undefined8 extraout_RDX;
  long in_GS_OFFSET;
  undefined8 acpi_data;
  char local_22 [2];
  long canary;
  
  __fentry__();
  canary = *(long *)(in_GS_OFFSET + 0x28);
  chck_r = acpi_evaluate_integer(chck_handle,"CHCK",0,&acpi_data);
  if (chck_r == 0) {
    snprintf(local_22,2,"%llu",acpi_data);
    r = simple_read_from_buffer(param_2,extraout_RDX,param_4,local_22,2);
  }
  else {
    printk("\x014Chck: cannot read from method CHCK");
    r = 0xffffffffffffffff;
  }
  if (canary == *(long *)(in_GS_OFFSET + 0x28)) {
    return r;
  }
                    // WARNING: Subroutine does not return
  __stack_chk_fail();
}

This function relays reads on /dev/chck to ACPI’s CHCK device. My guess was that this was handled early in the boot process — initramfs.

AML file

initramfs contains ssdt.aml which can be decompiled with iasl, producing ssdt.dsl. The following helped me make sense of what is happening:

  1. https://uefi.org/specifications
  2. https://wiki.osdev.org/AML

Here’s roughly what it does:

  • It defines a device: CHCK with 2 operation regions at SystemIO addresses 0x60 and 0x64.
  • There’s a method, also called CHCK, that compares a buffer KBDB with KBDA, returning 1 if they match and 0 otherwise.
  • KBDB is populated from SystemIO:0x60.
  • KBDA contained a bunch of pre-defined bytes.

What kind of device is this? Googling acpi 0x60 0x64 yields this page. So I made the assumption that KBDA contained PS/2 scan codes.

That assumption was confirmed with this table. Each byte represents a make/break code, indicating a keydown/keyup event.

The decode script is trivial and is left as an exercise for the reader. Lol kidding, I did it by hand.

Result was the flag: CTF{acpi_machine_language}