ForbiddenBITS CTF 2013 – X95

ForbiddenBITS CTF 2013 - X95 - task description

Zum Lösen dieser Challenge (X95) muss der Aufgabenstellung nach ein Passwort angegriffen und anschließend offensichtlich auf dem Zielsystem unter Port 3002 eingegeben werden. Zusätzlich erhalten wir den Quellcode des Dienstes, der auf dem System lauscht.
Bevor wir uns überhaupt mit dem Server verbinden, sehen wir uns das mitgelieferte Perl-Script an. Folgende Funktionsweise ist darin implementiert:

#!/usr/bin/perl
use IO::Socket;
my $welcome_msg = <<"EOT";
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 _    _  95   _________   ______   ______  _    _  _____  ______   ______
\\ \\  / /     | | | | | \\ | |  | | | |     | |  | |  | |  | |  \\ \\ | |
 >|--|<      | | | | | | | |__| | | |     | |--| |  | |  | |  | | | |----
/_/  \\_\\     |_| |_| |_| |_|  |_| |_|____ |_|  |_| _|_|_ |_|  |_| |_|____

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

EOT

$|=1;
$lis = IO::Socket::INET->new (
  LocalPort =gt; 3002,
  Type =gt; SOCK_STREAM,
  Reuse=> 1,
  Listen => 1000
) or die "Error #1 $@\n";

Das Skript definiert zunächst eine Willkommens-Nachricht und öffnet einen Socket, der auf Port 3002 lauscht.

while( $c = $lis->accept() ) {
  if ($pid=fork) {
    next;
  } else {
    unless (defined $pid) { die "Threads problem $! \n"; }
    $flag="";
    print $c $welcome_msg."\n\n";

    [... siehe nächster Auszug ...]

  }
   exit;
}
close $lis;

Anschließend wird die eigentliche Hauptschleife erreicht, die für jede Verbindung einen neuen Thread erstellt, die Variable $flag defininiert – die hier, anders auf dem Zielsystem laufenden Dienst, leer ist – und die Willkommensnachricht ausgibt. Was passiert nun genau, wenn ein Client sich zum Server verbindet?

  $authenticated = 0;
  $str2auth = int(rand(20));
  print $str2auth;
  $password = <$c>;
  chomp($password);
  if ($password eq $str2auth) {
    $authenticated = 1;

    [... siehe nächster Auszug ...]

  }
  close $c;
  exit;

Direkt nach dem Verbindungsausbau generiert das Programm eine Zufallszahl zwischen 0 und 19 (Zeile 2), gibt Sie dem Server lokal (nicht dem Client) aus (Zeile 3) und fragt die Zufallszahl vom Client ab (Zeile 4). Nur wenn die generierte Zufallszahl korrekt eingegeben wurde (Zeile 6) wird der Zugriff auf die weitere Funktionalität freigegeben, andernfalls die Verbindung beendet (Zeile 12).

Diese Überprüfung stellt jedoch kein besonderes Hindernis dar, da eine Zahl zwischen 0 und 19 leicht erraten werden kann, indem man sich oft genug zum Dienst verbindet. Was geschieht also, nachdem die Authentisierung erfolgt ist?

  print $c "\nPassword : \n";
  $str = <$c>;
  chomp($str);

  $str =~ s/X/XX/g;

  $encoded = x0($str, $str2auth);

Das Programm verlangt von uns als Nächstes die Eingabe eines Passwortes (Zeilen 1-3).  In diesem Passwort werden anschließend alle Zeichen ‘X’ durch ‘XX’ ersetzt (Zeile 5), bevor das Passwort zusammen mit der vorher generieren Zufallszahl an die Funktion x0 übergeben werden um die Variable $encoded zu bilden (Zeile 7). Die Funktion x0 sieht dabei folgendermaßen aus:

sub x0 {
  my ($str, $key) = @_;
  my $enc_str = '';

  for my $char (split //, $str) {
     my $decode = chop $key;
     $enc_str .= chr(ord($char) ^ ord($decode));
     $key = $decode . $key;
  }

  $enc_str =~ s/(.)/sprintf("%x",ord($1))/eg;
  return $enc_str;
}

Das Passwort ($str) und die Zufallszahl ($key) werden übernommen. Anschließend wird jeder Buchstabe des Passwortes mit einer Zahl ($decode), die aus der Zufallszahl generiert wurde per XOR verknüpft (Zeile 7). Dann wird der XOR-Operand per String-Manipulation verändert (Zeilen 8 und 6). Abschließend wird jedes Zeichen des Ergebnisses in dessen HEX-Wert umgewandelt (Zeile 11).

Zusammen gefasst wird unser Passwort auf Basis der vorher generierten Zufallszahl wild verwürfelt, so dass an dieser Stelle nicht mehr erkennbar ist, wie das Ergebnis der x0-Funktion aussehen wird.

Doch damit nicht genug, weitere skurrile Operationen erwarten uns nun im Anschluss:

  $encoded =~ tr/!-~/P-~!-O/;

  my $dd="";
  for ($i=0; $i<=length($encoded); $i++) {
    $dd .= hex(ord(substr($encoded,$i,1))+$str2auth);
  }

  for ($i=0; $i<=$str2auth; $i++) {
    $pos = hex(chr(ord(substr($encoded,$i,1)+$str2auth)));
    $dd =~ s/2/$pos/g;
  }

Der von der x0-Funktion erhaltene String ($decode) wird per Regulärem Ausdruck geshiftet, indem jedes Zeichen von ! bis ~ mit den Zeichen P bis ~ und ! bis O ersetzt werden (Zeile 1).

Anschließend wird jedes Zeichen des Ergebnisses in dessen ASCII-Wert überführt und dessen hexadezimale Repräsentation wieder als String übernommen (Zeilen 4-6). Natürlich wieder unter Einbringung der vorher generierten Zufallszahl!

Darauf folgend wird aus den ersten Zeichen des Ergebnisses (wieder abhängig von der Zufallszahl) ein Wert ($pos) bestimmt, der jedes Zeichen ‘2’ im Ergebnis ersetzt (Zeilen 8-11).

Zusammen gefasst also wieder viel wildes Voodoo, nachdem nun auch der letzte beim Versuch das für ein Passwort eingegebene Ergebnis zu bestimmen, ausgestiegen sein wird. Abschließend führt das Programm aber tatsächlich noch eine “handfeste” Überprüfung durch:

if (substr($dd,25,5).substr($dd,35,5).substr($dd,45,5).substr($dd,52,20).substr($dd,80,20) eq '7887487897986588789686597878864878865859865898659687897') {
  print $c $flag."\r\n";
}

Es werden fünf unterschiedlich lange Teilstrings des Ergebnisses ausgewählt und mit der Zeichenkette ‘7887487897986588789686597878864878865859865898659687897’ verglichen. Nur wenn diese Überprüfung gelingt, wird die Flagge und damit die Lösung zur Challenge ausgegeben.

Jedem sollte an dieser Stelle klar sein, dass diese Zeichenkette nur mit hohem Aufwand in ein entsprechende Passwort zurück berechnet werden kann. Und deshalb wählen wir hier eher den “brutalen” Weg, nämlich ein brute force des von uns einzugebenen Passwortes.

Da die Länge des Passwortes sich proportional zum erwarteten Ergebnis verhält, sollte es möglich sein, dass Passwort Zeichen für Zeichen anzugreifen und nicht alle Möglichen Kombinationen prüfen zu müssen. Wir passen also das Skript ein wenig an, um den Brute Force durchzuführen, hier die entscheidenen Änderungen:

$str2auth = $ARGV[0];
try("");

sub try {
  my $base = $_[0];

  print "TRYING: $base\n";

  for (my $a=33; $a<=126; $a++) {
    my $str = $base . chr($a);

    [... originale Operationen zur Berechnung von $dd ...]

    if ((substr($dd, 25, 1) ne "") && (substr($dd, 25, 1) ne "7")) { next; }
    if ((substr($dd, 26, 1) ne "") && (substr($dd, 26, 1) ne "8")) { next; }
    [...]

    if (substr($dd,25,5).substr($dd,35,5).substr($dd,45,5).substr($dd,52,20).substr($dd,80,20) eq '7887487897986588789686597878864878865859865898659687897') {
        print "WON: $str\n";
        exit;
    }

    try($str);
  }
}

Zur Lösung des Problems wenden wir die Technik des sogenannten “rekursiven Backtrackings” an. Dazu geben wir einer Funktion try() einen Startwert mit (Zeile 2)und die Funktion wird sich selbst nach jedem erfolgreich getesteten Zeichen selbst aufrufen (Zeile 23).

Die Funktion selbst iteriert über alle Zeichen von ! bis ~ (Zeile 9), wiederholt die Berechnungen des originalen Skriptes zur Bestimmung von $dd (Zeile 12) und führt anschließend eine Überprüfung durch, ob das entsprechende Zeichen an der für die Lösung erwarteten Position korrekt ist (Zeilen 14ff).

Da wir wissen, dass auch die Zufallszahl eine entscheidende Rolle bei der Berechnung des Ergebnisses spielt, können wir diese als Übergabeparameter an das Cracking-Skript übergeben (Zeile 1).

Nun ist es Zeit, unser Skript auszuprobieren. Wir starten mit der Zufallszahl 0:

rup0rt@lambda:~/FB2013$ ./x95-crack.pl 0
TRYING: 
TRYING: !
TRYING: !!
TRYING: !!!
TRYING: !!!!
TRYING: !!!"
TRYING: !!!#
TRYING: !!!$
[...]

Nach einigen Sekunden ohne Ergebnis sollte klar sein, dass es kein gültiges Passwort für die Zufallszahl 0 gibt. Also wiederholen wir den Aufruf für alle anderen möglichen Zufallszahlen von noch 1 bis 19.

rup0rt@lambda:~/FB2013$ ./x95-crack.pl 8
TRYING: 
TRYING: !
TRYING: !!
[...]
TRYING: !!!!?1"E(>0DU?=?!2
TRYING: !!!!?1"E(>0DU?=?!2>
TRYING: !!!!?1"E(>0DU?=?!2>8
TRYING: !!!!?1"E(>0DU?=?!2>8>
TRYING: !!!!?1"E(>0DU?=?!2>8>3
TRYING: !!!!?1"E(>0DU?=?!2>8>3T
WON: !!!!?1"E(>0DU?=?!2>8>3TE

Bei der Zufallszahl 8 werden wir letztendlich fündig und stellen fest, dass das Passwort “!!!!?1″E(>0DU?=?!2>8>3TE” lauten muss. Dieses müssen wir nun nur noch auf dem Zielsystem eingeben. Da das Passwort nur für die Zufallszahl 8 Gültigkeit besitzt, müssen wir uns mehrfach zum Server binden, bis wir die Zufallszahl 8 erwischen:

rup0rt@lambda:~/FB2013$ nc 192.73.237.148 3002
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 _    _  95   _________   ______   ______  _    _  _____  ______   ______ 
\ \  / /     | | | | | \ | |  | | | |     | |  | |  | |  | |  \ \ | |     
 >|--|<      | | | | | | | |__| | | |     | |--| |  | |  | |  | | | |---- 
/_/  \_\     |_| |_| |_| |_|  |_| |_|____ |_|  |_| _|_|_ |_|  |_| |_|____ 

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

8

Password : 
!!!!?1"E(>0DU?=?!2>8>3TE

FLAG{PERL_IS_COOL_FOR_SCRIPTING}

Nach einigen Versuchen erhalten wir die Möglichkeit das Passwort einzugeben und stellen fest, dass der Server uns mit der Flagge zu dieser Challenge antwortet!

Die Lösung lautet “PERL_IS_COOL_FOR_SCRIPTING“.

Leave a Reply

Your email address will not be published. Required fields are marked *