
Hintergrund
Es gab einen Vorfall, bei dem der Server von einem Ransomware-Virus angegriffen wurde, der aus "Zufall" teilweise die .ibd-Dateien (Rohdaten-Dateien von InnoDB-Tabellen) unberührt ließ, jedoch die .fpm-Dateien (Strukturdateien) vollständig verschlüsselte. Die .idb-Dateien konnten unterteilt werden in:
- wiederherstellbare über Standardmittel und Anleitungen. Für solche Fälle gibt es hervorragende ;
- teilweise verschlüsselte Tabellen. Dies sind hauptsächlich große Tabellen, bei denen, wie ich verstanden habe, den Angreifern der Arbeitsspeicher für die vollständige Verschlüsselung fehlte;
- und komplett verschlüsselte Tabellen, die nicht wiederhergestellt werden können.
Um zu bestimmen, zu welcher der Varianten die Tabellen gehören, konnte ich einfach die Datei in einem Texteditor mit der benötigten Kodierung (in meinem Fall UTF-8) öffnen und auf Textfelder prüfen, zum Beispiel:

Außerdem lässt sich am Anfang der Datei eine große Anzahl von Null-Bytes beobachten, und Viren, die den Algorithmus der Blockverschlüsselung nutzen (was am weitesten verbreitet ist), betreffen normalerweise auch diese.
![]()
In meinem Fall hinterließen die Angreifer am Ende jeder verschlüsselten Datei eine Zeichenfolge von 4 Bytes (1, 0, 0, 0), was die Sache erleichterte. Für die Suche nach nicht infizierten Dateien genügte ein Skript:
def opened(path):
files = os.listdir(path)
for f in files:
if os.path.isfile(path + f):
yield path + f
for full_path in opened("C:somepath"):
file = open(full_path, "rb")
last_string = ""
for line in file:
last_string = line
file.close()
if (last_string[len(last_string) - 4:len(last_string)]) != (1, 0, 0, 0):
print(full_path)So gelang es, Dateien des ersten Typs zu finden. Der zweite Typ erfordert viel manuelle Arbeit, aber das Gefundene war bereits ausreichend. Alles wäre gut, wenn man die absolut genaue Struktur und (selbstverständlich) trat der Fall ein, dass ich mit einer häufig wechselnden Tabelle arbeiten musste. Niemand konnte sich daran erinnern, ob der Feldtyp geändert oder eine neue Spalte hinzugefügt wurde.
Leider konnten die Debris-City dabei nicht helfen, deshalb wird dieser Artikel geschrieben.
Kommen wir zur Sache
Es gibt eine Tabellstruktur von vor 3 Monaten, die nicht mit der aktuellen übereinstimmt (möglicherweise in einem Feld, möglicherweise auch mehr). Die Struktur der Tabelle:
ERSTELLEN SIE TABELLE `table_1` (
`id` INT (11),
`datum` DATETIME ,
`beschreibung` TEXT ,
`id_punkt` INT (11),
`id_benutzer` INT (11),
`datum_start` DATETIME ,
`datum_ende` DATETIME ,
`foto` INT (1),
`id_kunde` INT (11),
`status` INT (1),
`lead__zeit` TIME ,
`sendestatus` TINYINT (4)
); Dabei müssen folgende Daten extrahiert werden:
id_punktINT (11);id_benutzerINT (11);datum_startDATETIME ;datum_endeDATETIME .
Für die Wiederherstellung wird eine byteweise Analyse der .ibd-Datei verwendet, gefolgt von einer Umwandlung in ein leserlicheres Format. Da es für die Suche nach den erforderlichen Daten ausreicht, die Typen int und datetime zu analysieren, werden in diesem Artikel nur diese behandelt, wobei jedoch manchmal auch andere Datentypen erwähnt werden, die bei ähnlichen Vorfällen helfen könnten.
Problem 1: In den Feldern mit den Typen DATETIME und TEXT gab es NULL-Werte, die in der Datei einfach übersprungen wurden, was es mir unmöglich machte, die Struktur für die Wiederherstellung in meinem Fall zu bestimmen. In den neuen Spalten war der Standardwert null, und ein Teil der Transaktion könnte aufgrund der Einstellung innodb_flush_log_at_trx_commit = 0 verloren gegangen sein. Daher hätte ich zusätzliche Zeit aufwenden müssen, um die Struktur zu bestimmen.
Problem 2: Bitte beachten Sie, dass durch DELETE entfernte Zeilen weiterhin in der ibd-Datei vorhanden sind, die Struktur jedoch bei ALTER TABLE nicht aktualisiert wird. Infolgedessen kann die Datenstruktur vom Anfang bis zum Ende der Datei variieren. Wenn Sie häufig OPTIMIZE TABLE verwenden, werden Sie mit einem solchen Problem wahrscheinlich nicht konfrontiert.
Bitte beachten Sie, die Version des DBMS beeinflusst die Art der Datenspeicherung, und dieses Beispiel könnte in anderen Hauptversionen nicht funktionieren. In meinem Fall wurde die Windows-Version von MariaDB 10.1.24 verwendet. Auch wenn Sie mit InnoDB-Tabellen in MariaDB arbeiten, sind sie tatsächlich , was die Anwendbarkeit der Methode mit InnoDB MySQL ausschließt.
Dateianalyse
In Python hat der Datentyp zeigt Daten in Unicode anstelle einer normalen Zahlenreihe an. Obwohl die Datei in dieser Form betrachtet werden kann, ist es zur Einfachheit möglich, Bytes in eine numerische Form zu konvertieren, indem man das Byte-Array in ein normales Array überführt (list(example_byte_array)). In jedem Fall sind beide Methoden zur Analyse geeignet.
Nach der Durchsicht mehrerer ibd-Dateien können folgende festgestellt werden:
![]()
Wenn wir die Datei nach diesen Schlüsselwörtern aufteilen, ergeben sich überwiegend gleichmäßige Datenblöcke. Wir verwenden infimum als Teiler.
table = table.split("infimum".encode())Ein interessantes Detail: Bei Tabellen mit einer kleinen Anzahl an Daten gibt es zwischen Infimum und Supremum einen Hinweis auf die Anzahl der Zeilen im Block.
— Testtabelle mit 1 Zeile
— Testtabelle mit 2 Zeilen
Das Array der Zeilen table[0] kann übersprungen werden. Ich konnte beim Durchsehen keine Rohdaten der Tabellen entdecken. Wahrscheinlich dient dieser Block zur Speicherung von Indizes und Schlüsseln.
Ab table[1] und wenn man es in ein numerisches Array umwandelt, kann man bereits einige Muster erkennen, nämlich:

Dies sind int-Werte, die in der Zeile gespeichert sind. Das erste Byte gibt an, ob die Zahl positiv oder negativ ist. In meinem Fall sind alle Zahlen positiv. Aus den restlichen 3 Bytes kann die Zahl mit der folgenden Funktion bestimmt werden. Skript:
def find_int(val: str): # Beispiel '128, 1, 2, 3'
val = [int(v) for v in val.split(", ")]
result_int = val[1]*256**2 + val[2]*256*1 + val[3]
return result_intZum Beispiel 128, 0, 0, 1 = 1, oder 128, 0, 75, 108 = 19308.
In der Tabelle gab es einen Primärschlüssel mit Auto-Inkrement, und auch hier kann er gefunden werden.

Durch den Abgleich der Daten aus den Testtabellen wurde festgestellt, dass das DATETIME-Objekt aus 5 Bytes besteht, die wahrscheinlich mit 153 beginnen (was vermutlich auf jährliche Intervalle hinweist). Da der DATETIME-Bereich von '1000-01-01' bis '9999-12-31' reicht, könnte die Anzahl der Bytes variieren. In meinem Fall fallen die Daten jedoch in den Zeitraum von 2016 bis 2019, weshalb wir annehmen, dass 5 Bytes ausreichend sind.
Zur Bestimmung der Zeit ohne Sekunden wurden die folgenden Funktionen geschrieben. Skript:
day_ = lambda x: x % 64 // 2 # {x,x,X,x,x }
def hour_(x1, x2): # {x,x,X1,X2,x}
if x1 % 2 == 0:
return x2 // 16
elif x1 % 2 == 1:
return x2 // 16 + 16
else:
raise ValueError
min_ = lambda x1, x2: (x1 % 16) * 4 + (x2 // 64) # {x,x,x,X1,X2}Für das Jahr und den Monat konnte ich keine funktionierende Funktion schreiben, daher musste ich zu Hardcoding greifen. Skript:
ym_list = {'2016, 1': '153, 152, 64', '2016, 2': '153, 152, 128',
'2016, 3': '153, 152, 192', '2016, 4': '153, 153, 0',
'2016, 5': '153, 153, 64', '2016, 6': '153, 153, 128',
'2016, 7': '153, 153, 192', '2016, 8': '153, 154, 0',
'2016, 9': '153, 154, 64', '2016, 10': '153, 154, 128',
'2016, 11': '153, 154, 192', '2016, 12': '153, 155, 0',
'2017, 1': '153, 155, 128', '2017, 2': '153, 155, 192',
'2017, 3': '153, 156, 0', '2017, 4': '153, 156, 64',
'2017, 5': '153, 156, 128', '2017, 6': '153, 156, 192',
'2017, 7': '153, 157, 0', '2017, 8': '153, 157, 64',
'2017, 9': '153, 157, 128', '2017, 10': '153, 157, 192',
'2017, 11': '153, 158, 0', '2017, 12': '153, 158, 64',
'2018, 1': '153, 158, 192', '2018, 2': '153, 159, 0',
'2018, 3': '153, 159, 64', '2018, 4': '153, 159, 128',
'2018, 5': '153, 159, 192', '2018, 6': '153, 160, 0',
'2018, 7': '153, 160, 64', '2018, 8': '153, 160, 128',
'2018, 9': '153, 160, 192', '2018, 10': '153, 161, 0',
'2018, 11': '153, 161, 64', '2018, 12': '153, 161, 128',
'2019, 1': '153, 162, 0', '2019, 2': '153, 162, 64',
'2019, 3': '153, 162, 128', '2019, 4': '153, 162, 192',
'2019, 5': '153, 163, 0', '2019, 6': '153, 163, 64',
'2019, 7': '153, 163, 128', '2019, 8': '153, 163, 192',
'2019, 9': '153, 164, 0', '2019, 10': '153, 164, 64',
'2019, 11': '153, 164, 128', '2019, 12': '153, 164, 192',
'2020, 1': '153, 165, 64', '2020, 2': '153, 165, 128',
'2020, 3': '153, 165, 192','2020, 4': '153, 166, 0',
'2020, 5': '153, 166, 64', '2020, 6': '153, 1, 128',
'2020, 7': '153, 166, 192', '2020, 8': '153, 167, 0',
'2020, 9': '153, 167, 64','2020, 10': '153, 167, 128',
'2020, 11': '153, 167, 192', '2020, 12': '153, 168, 0'}
def year_month(x1, x2): # {x,X,X,x,x }
for key, value in ym_list.items():
key = [int(k) for k in key.replace("'", "").split(", ")]
value = [int(v) for v in value.split(", ")]
if x1 == value[1] and x2 // 64 == value[2] // 64:
return key
return 0, 0Ich bin mir sicher, dass man mit etwas Zeit auch dieses Missverständnis klären kann.
Als nächstes eine Funktion, die ein datetime-Objekt aus einem String zurückgibt. Skript:
def find_data_time(val:str):
val = [int(v) for v in val.split(", ")]
day = day_(val[2])
hour = hour_(val[2], val[3])
minutes = min_(val[3], val[4])
year, month = year_month(val[1], val[2])
return datetime(year, month, day, hour, minutes)Es konnten häufig wiederkehrende Werte aus int, int, datetime, datetime festgestellt werden.
, das scheint genau das zu sein, was benötigt wird. Zudem wiederholt sich diese Sequenz nicht zweimal innerhalb des Strings.
Mit einem regulären Ausdruck suchen wir die benötigten Daten:
fined = re.findall(r'128, d*, d*, d*, 128, d*, d*, d*, 153, 1[6,5,4,3]d, d*, d*, d*, 153, 1[6,5,4,3]d, d*, d*, d*', int_array)Bitte beachten Sie, dass bei der Suche mit diesem Ausdruck keine NULL-Werte in den geforderten Feldern ermittelt werden können, aber in meinem Fall ist das nicht kritisch. Danach iterieren wir über die gefundenen Werte. Skript:
result = []
for val in fined:
pre_result = []
bd_int = re.findall(r"128, d*, d*, d*", val)
bd_date= re.findall(r"(153, 1[6,5,4,3]d, d*, d*, d*)", val)
for it in bd_int:
pre_result.append(find_int(bd_int[it]))
for bd in bd_date:
pre_result.append(find_data_time(bd))
result.append(pre_result)Das ist alles, die Daten aus dem Ergebnis-Array sind die Informationen, die wir benötigen. ###PS.###
Ich verstehe, dass dieser Ansatz nicht für jeden geeignet ist, aber das Hauptziel des Artikels ist es eher, zur Handlung zu motivieren, als all Ihre Probleme zu lösen. Ich denke, die beste Lösung wäre, mit dem Studium des Quellcodes selbst zu beginnen. , aber aufgrund der begrenzten Zeit schien mir der aktuelle Weg der schnellste zu sein.
In einigen Fällen können Sie durch die Analyse der Datei die ungefähre Struktur bestimmen und eine der Standardmethoden aus den obigen Links verwenden, um sie wiederherzustellen. Das wird wesentlich korrekter sein und weniger Probleme verursachen.
Quelle: habr.com
