edit

HFCTF 2024: Bad Daemon

Challenge 1 (“Bad App”)

Looking at the binary tells us it uses PyInstaller, and thus can be extracted using pyinstxtractor. We can then just drop the .pyc file into PyLingal.

The class of interest is very obviously the first:

class LicenseManager:
    @staticmethod
    def validate_user_id(user_id):
        return bool(re.match('^(?=.*[a-zA-Z])(?=.*\\d)[a-zA-Z0-9]{4,10}$', user_id))

    @staticmethod
    def validate_product_id(product_id):
        return bool(re.match('^\\d{8}$', product_id))

    @staticmethod
    def generate_license_key(user_id, product_id):
        try:
            data = f'{user_id}-{product_id}'
            license_key = hashlib.sha256(data.encode()).hexdigest()[:22]
            excluded_chars = {'o', '1', '0', 'O', 'l'}
            valid_key = ''.join((c if c not in excluded_chars else 'x' for c in license_key))
            return valid_key
        except Exception as e:
            messagebox.showerror('Error', f'Error generating license key: {e}')

    @staticmethod
    def verify_license_key(user_id, product_id, license_key):
        try:
            if LicenseManager.validate_user_id(user_id) and LicenseManager.validate_product_id(product_id):
                return license_key == LicenseManager.generate_license_key(user_id, product_id)
        except Exception as e:
            messagebox.showerror('Error', f'Error verifying license key: {e}')
            return False

And then there’s not really anything to reverse, we just create a valid license:

>>> h = hashlib.sha256("aaaa0A1b-00000000".encode()).hexdigest()[:22]
>>> ''.join((c if c not in {'o', '1', '0', 'O', 'l'} else 'x' for c in h))
'x36ea6dd25ab35f7f872ee'

Prepend HF- and you’ve got yourself a flag.

(I do imagine any hex string with an x and no 0 or 1 would work, but you might as well reuse challenge code when you can)

Challenge 2 (“Bad Daemon” – the actual challenge)

We’re again given a PyInstaller binary which we extract and decompile, but this one simply drops the real meat of the challenge, an ELF binary and systemd service:

SystemManager.extract_file_from_pyinstaller('bad_daemon.service', '/etc/systemd/system/')
SystemManager.extract_file_from_pyinstaller('bad_daemon', '/usr/sbin/')
SystemManager.activate_and_start_service('bad_daemon')

Dropping that into ghidra, we can see it’s doing some funny stuff starting a bunch of children:

void main(void)
{
  char *shm;
  
  daemonize();
  openlog("bad_daemon",1,0x18);
  shm = setup_shared_memory();
  setup_mutex(shm);
  if (*(int *)(shm + 0x6c) == 0) {
    syslog(6,
           "HINT: Keep it dynamic.\n"
           " This challenge had been tested on archlinux kernel version 6.11 .2-arch1-1 and python 3.12.6.\n"
           " Is it possible to see some delays in the syslog display. I\'ll let you figure out why.\n"
           " BTW thank you for trying my challenge, hope you\'ll like it. -mymelody1242"
          );
  }
  do {
    process_cycle(shm);
    sleep(5);
  } while( true );
}


void process_cycle(char *shm)
{
  __pid_t res;
  int *piVar1;
  char *pcVar2;
  int i = 0;

  while( true ) {
    if (5 < i) {
      return;
    }
    res = fork();
    if (res == 0) break;
    // ...snipped err handling
    res = wait((void *)0x0);
    // ...more error handling
    i = i + 1;
  }
  child_process(shm,i);
  exit(0);
}

… but we don’t need to care about any of that (I’ve never been one to take hints), a few layers down there’s what very obviously looks like a flag “decryption” function:

void process_operation(char *shm,int i)
{
  // ...
  if (*(int *)shm == 0x42) {
    syslog(5,"Flag is unlock!");
    shaboiinnk(shm + 0x30,0x3c,i + 1);
  }
  else {
    syslog(5,"Flag is not unlock!");
  }
  // ...
}


void shaboiinnk(char *offset_shm,ulong const60,int procnum)
{
  char rotated;
  ulong i;
  
  for (i = 0; i < const60; i = i + 1) {
    rotated = rol(offset_shm[i],procnum % 8);
    offset_shm[i] = rotated;
    offset_shm[i] = offset_shm[i] ^ (byte)procnum;
    rotated = ror(offset_shm[i],procnum % 8);
    offset_shm[i] = rotated;
  }
  bang(offset_shm,const60);
  return;
}

(some names mine, some unchanged)

So let’s find shm+0x30, the “encrypted” flag. Quite unsurprisingly, it’s in setup_shared_memory (called in main):

strncpy(shm + 0x30,"XV=RPtTuq} ~YcBe~~!~w ~I ebCicd#}",0x3c);

The decryption function is a simple xor, which we can trivially write as the following python:

def ror(x, n):
    return (x>>n) | (x << (8-n))

def xor(b, p):
    return bytes([x^ror(p,p) for x in b])

B = b"XV=RPtTuq} ~YcBe~~!~w ~I ebCicd#}"

for i in range(6):
    B = xor(B, i+1)
print(B)

Flag: HF-B@dDeam0nIsRunn1ng0nY0urSyst3m


HomeAboutContact