Files
HIP7CTF_Writeups/de/cryptoracle_v1.md
m0rph3us1987 a79656b647 Added writeups
2026-03-08 12:22:39 +01:00

5.4 KiB

CryptOracle v1

CryptOracle v1 ist eine einführende Challenge in Kryptographie und Reverse Engineering, die ein Hardware-Sicherheitsmodul (HSM) simuliert. Das Ziel ist es, eine geheime Flagge abzurufen, die in einem "sicheren" Speicherbereich gespeichert ist, der angeblich vom Benutzer isoliert ist.

Informationsbeschaffung

Wir erhalten ein cryptOracle_v1.tar.xz Archiv, das die crypt_oracle_v1 Binärdatei enthält. Eine erste Analyse bestätigt, dass es sich um eine statisch gelinkte 64-Bit ELF ausführbare Datei handelt.

$ file crypt_oracle_v1
crypt_oracle_v1: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, ...

Beim Verbinden mit dem Challenge-Server werden wir mit der folgenden Oberfläche begrüßt:

CryptOracle v1.0
Setting up memory...
  0x10000 - 0x11000 : Secure Memory (Keys/ROM)
  0x20000 - 0x28000 : User Memory
Type 'help' for commands.

Die Challenge-Beschreibung deutet an, dass sensible Schlüssel an der Adresse 0x10000 gespeichert sind.

Reverse Engineering

Wir verwenden Ghidra, um die Binärdatei zu analysieren und zu verstehen, wie sie den Speicherzugriff verwaltet.

1. Hauptinteraktionsschleife (main)

Die main-Funktion initialisiert die HSM-Simulation und verarbeitet Benutzerbefehle.

undefined8 main(void)

{
  int iVar1;
  char *pcVar2;
  long in_FS_OFFSET;
  undefined4 local_428;
  undefined4 local_424;
  undefined4 local_420;
  undefined4 local_41c;
  char local_418 [512];
  undefined1 local_218 [520];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  setvbuf((FILE *)stdout,(char *)0x0,2,0);
  puts("CryptOracle v1.0");
  setup_memory();
  puts("Type \'help\' for commands.");
  while( true ) {
    pcVar2 = fgets(local_418,0x200,(FILE *)stdin);
    if (pcVar2 == (char *)0x0) break;
    iVar1 = strncmp(local_418,"rm",2);
    if (iVar1 == 0) {
      iVar1 = __isoc99_sscanf(local_418,"rm 0x%x %d",&local_420,&local_41c);
      if (iVar1 == 2) {
        do_read(local_420,local_41c);
      }
    }
    // ... (andere Befehle) ...
  }
  // ...
  return 0;
}

Der rm-Befehl nimmt eine hexadezimale Adresse und eine dezimale Größe entgegen und ruft dann die do_read-Funktion auf.

2. Speicherinitialisierung (setup_memory)

Die setup_memory-Funktion offenbart, wo die Flagge platziert wird.

void setup_memory(void)

{
  void *pvVar1;
  FILE *__stream;
  size_t sVar2;
  
  puts("Setting up memory...");
  pvVar1 = mmap64((void *)0x10000,0x1000,3,0x32,-1,0);
  // ...
  pvVar1 = mmap64((void *)0x20000,0x8000,3,0x32,-1,0);
  // ...
  memset((void *)0x10000,0,0x1000);
  memset((void *)0x20000,0,0x8000);
  __stream = fopen64("flag.bin","rb");
  if (__stream != (FILE *)0x0) {
    sVar2 = fread((void *)0x10000,1,0x800,__stream);
    fclose(__stream);
    // ...
    return;
  }
  // ...
}

Wir bestätigen, dass die Flagge aus flag.bin direkt in den "Secure Memory"-Bereich bei 0x10000 geladen wird.

3. Schwachstellenanalyse (get_ptr)

Die do_read-Funktion ruft einen Helfer namens get_ptr auf, um den Speicherzugriff zu validieren, bevor Daten gedruckt werden.

void do_read(undefined4 param_1,undefined4 param_2)

{
  long lVar1;
  
  lVar1 = get_ptr(param_1,param_2,0);
  if (lVar1 == 0) {
    puts("ERR_ACCESS_VIOLATION");
  }
  else {
    print_hex(lVar1,param_2);
  }
  return;
}

Untersuchen wir nun die Logik in get_ptr:

uint get_ptr(uint param_1,int param_2,int param_3)

{
  // 1. Integer-Überlaufprüfung: Sicherstellen, dass Adresse + Größe nicht überläuft
  if (param_2 + param_1 < param_1) {
    param_1 = 0;
  }
  // 2. Bereichsvalidierung: Prüfen, ob der Zugriff innerhalb der gemappten Bereiche liegt
  else if ((param_1 < 0x10000) || (0x11000 < param_2 + param_1)) {
    // Wenn nicht im Secure Memory (0x10000-0x11000), prüfe User Memory (0x20000-0x28000)
    if ((param_1 < 0x20000) || (0x28000 < param_2 + param_1)) {
      param_1 = 0;
    }
  }
  // 3. Sicherheitsprüfung: Blockiere Zugriff auf den Flaggenbereich (0x10000 - 0x10800)
  // Dies wird NUR erzwungen, wenn param_3 (check_secure) ungleich Null ist
  else if ((param_3 != 0) && (param_1 - 0x10000 < 0x800)) {
    param_1 = 0;
  }
  return param_1;
}

Die Validierungslogik in get_ptr funktioniert wie folgt:

  1. Integer-Überlauf: Es wird geprüft, ob size + addr überläuft.
  2. Bereichsvalidierung: Es wird sichergestellt, dass der Zugriff innerhalb des Secure Memory (0x10000-0x11000) oder User Memory (0x20000-0x28000) liegt.
  3. Sicherheitsbeschränkung: Es wird explizit der Zugriff auf die ersten 0x800 Bytes des Secure Memory (wo die Flagge gespeichert ist) blockiert, NUR WENN param_3 (das check_secure-Flag) ungleich Null ist.

Entscheidend ist, dass in der do_read-Funktion (die den rm-Befehl implementiert) das dritte Argument, das an get_ptr übergeben wird, fest auf 0 gesetzt ist. Das bedeutet, dass die spezifische Prüfung, die die geheimen Schlüssel schützt, umgangen wird, wenn der Befehl "read memory" verwendet wird.

Lösung

Da der rm-Befehl die Isolationsprüfung in get_ptr umgeht, können wir das sichere RAM direkt dumpen.

  1. Mit dem Server verbinden.
  2. Den Speicher an der Adresse 0x10000 lesen.
> rm 0x10000 32
00010000: 7b 66 6c 61 67 3a 20 74 68 61 74 5f 77 61 73 5f  {flag: that_was_
00010010: 74 6f 6f 5f 65 61 73 79 5f 72 69 67 68 74 3f 7d  too_easy_right?}

Die Flagge wird enthüllt: {flag: that_was_too_easy_right?}