Pimeä uhka
Tämä teksti on osa blogisarjaamme, jossa yksittäiset henkilöt kirjoittavat sekalaisista pitkäaikaissäilytysteemoista vapaamuotoisesti. Blogisarjaamme ei ole tarkoitettu ohjaaviksi säännöiksi tai ohjeiksi, vaan inspiraatioksi ideoille ja keskustelulle.
Kauan sitten kaukaisella palvelimella....
PDF-validoinnissa kuohuu. Validointityökalujen tulosteet ovat kiistanalaisia. Toivossaan ratkaista asia on nuori kehittäjä päivittänyt validointityökaluja jättämättä mitään dokumentaatiota jälkeensä. Samaan aikaan, kun käydään loputtomia keskusteluja tämän tapahtumaketjun katkaisemiseksi, olemme salaa lähettäneet PAS-kanan tutkimaan tapahtumia.

Episodi IV - Dynaaminen analyysi
Laskeuduttuaan palvelimelle, PAS-kana havaitsee validoinnin käyttäytyvän omituisesti. Ehjien PDF-tiedostojen lisäksi rikkinäiset tiedostot läpäisevät validoinnin. Tämä vaatii tarkempaa ohjelman analysointia.
Ohjelmistoanalyysi voidaan korkealla tasolla jakaa kahteen eri kategoriaan: dynaaminen analyysi ja staattinen analyysi. Dynaaminen analyysi tarkoittaa ohjelman suorittamista ja sen käytöksen tutkimista. Staattinen analyysi puolestaan tarkoittaa kaikkea muuta ohjelman analysointia ilman sen suorittamista.
Luotettua ohjelmistoa tutkittaessa yleensä on helpointa aloittaa korkeimmalta tasolta ohjelmiston toiminnan tutkinta. Monimutkaisemmat menetelmät on helpointa valjastaa vasta, kun ohjelmiston käytös ymmärretään korkeammalla tasolla. Kaikista yksinkertaisimmillaan tämä voi tarkoittaa ohjelman suorittamista ja sen tulosteen tutkimista.
Koska file-scraper on korjattu käsittelemään viime blogijulkaisun PDF-tiedosto, kokeillaan ensimmäiseksi vain validoida kyseinen tiedosto.
$ file phantom_of_a_pdf_file_blog_post_2024.pdf phantom_of_a_pdf_file_blog_post_2024.pdf: PDF document, version 1.4
$ scraper scrape-file phantom_of_a_pdf_file_blog_post_2024.pdf { "path": "phantom_of_a_pdf_file_blog_post_2024.pdf", "MIME type": "application/pdf", "version": "1.4", "metadata": { "0": { "index": 0, "mimetype": "application/pdf", "stream_type": "binary", "version": "1.4" } }, "grade": "fi-dpres-acceptable-file-format", "well-formed": true }
Kyseinen tiedosto tunnistetaan odotetusti ehjäksi PDF 1.4 -dokumentiksi. Tunnetun rikkinäisen PDF-tiedoston validointi puolestaan palauttaa virheitä.
$ scraper scrape-file invalid_1.3_removed_xref.pdf { "path": "invalid_1.3_removed_xref.pdf", "MIME type": "application/pdf", "version": "(:unav)", "metadata": { "0": { "index": 0, "mimetype": "application/pdf", "stream_type": "binary", "version": "(:unav)" } }, "grade": "fi-dpres-unacceptable-file-format", "well-formed": false, "errors": { "GhostscriptScraper": [ "Invalid xref entry, incorrect format.\n\nThe following errors were encountered at least once while processing this file:\n\txref table was repaired\n\nThe following warnings were encountered at least once while processing this file:\n\n **** This file had errors that were repaired or ignored.\n **** The file was produced by: \n **** >>>> GPL Ghostscript 9.07 <<<<\n **** Please notify the author of the software that produced this\n **** file that it does not conform to Adobe's published PDF\n **** specification.\n\n", "GPL Ghostscript 10.03.1 (2024-05-02)\nCopyright (C) 2024 Artifex Software, Inc. All rights reserved.\nThis software is supplied under the GNU AGPLv3 and comes with NO WARRANTY:\nsee the file COPYING for details.\nProcessing pages 1 through 1.\nPage 1\n\txref entry not exactly 20 bytes\n\txref entry not valid format\n" ], "ResultsMergeScraper": [ "Conflict with values '1.4' and '1.3' for 'version'." ], "MimeMatchScraper": [ "File format version is not supported." ] } }
Yllä olevassa tulosteessa on kuitenkin eroja korjaamattoman scraperin tulosteeseen. Yltä puuttuu kaikki JHOVE:n tulostamat virheet. Tämä viittaa siihen, että tiedosto on JHOVE:n mielestä ehjä PDF.
Scraperin kulissien takana ajamia komentoja pystyy tutkimaan strace-työkalulla. Strace kiinnittyy määritettyyn prosessiin ja tulostaa prosessin kaikki järjestelmäkutsut ja niiden paluuarvot. Tässä tapauksessa scraper tekee kaiken kaikkiaan 25 000 järjestelmäkutsua. Kaikkien näiden kutsujen tutkiminen on harvoin vaivan arvoista. Sen sijaan voimme määrittää hakusanoja joista olemme erityisesti kiinnostuneita.
$ strace -f scraper scrape-file invalid_1.3_removed_xref.pdf 2>&1 | grep execve | grep '"jhove"' [pid 449701] execve("/home/paskana/.local/bin/jhove", ["jhove", "-h", "XML", "-m", "PDF-hul", "invalid_1.3_removed_xref.pdf"], 0x7f89572ef160 /* 37 vars */) = -1 ENOENT (No such file or directory) [pid 449701] execve("/usr/local/bin/jhove", ["jhove", "-h", "XML", "-m", "PDF-hul", "invalid_1.3_removed_xref.pdf"], 0x7f89572ef160 /* 37 vars */) = -1 ENOENT (No such file or directory) [pid 449701] execve("/usr/bin/jhove", ["jhove", "-h", "XML", "-m", "PDF-hul", "invalid_1.3_removed_xref.pdf"], 0x7f89572ef160 /* 37 vars */ <unfinished ...>
-f
seuraa mahdollisten fork-kutsujen luomia uusia prosesseja. 2>&1
ohjaa virheet standarditulosteeseen. Ja viimeiseksi grep-komento lukee standarditulosteen ja suodattaa sieltä vain rivit joista löytyvät hakusanat execve
ja "jhove".
Execve-järjestelmäkutsu on dokumentoitu englanniksi man-sivuilla $ man execve
.
execve() executes the program referred to by pathname. This causes the program that is currently being run by the calling process to be replaced with a new program, with newly initialized stack, heap, and (initialized and uninitialized) data segments.
execve() käynnistää uuden ohjelman polun perusteella. Vanhan ohjelman koko virtuaalinen muistiavaruus korvataan uudella ohjelmalla ja sen suoritus käynnistetään. Ylhäältä strace-komennon tulosteesta näemme siis tarkan jhove-komennon mitä scraper käyttää PDF-tiedoston tutkimiseen.
$ jhove -h XML -m PDF-hul invalid_1.3_removed_xref.pdf
Ajamalla itse kyseisen komennon huomaamme, että jhove
tosiaan raportoi rikkinäisenkin tiedoston olevan validi PDF 1.4 -dokumentti. Voimme tarkistaa onko kukaan tehnyt asennuksen jälkeen muutoksia JHOVE RPM-asennuspaketin luomiin tiedostoihin.
$ rpm --verify jhove; echo $? 0
Tämän perusteella mitään muutoksia JHOVE:n asentamiin tiedostoihin ei ole tehty. Selvittämällä seuraavaksi mitä JHOVE kutsuu:
$ strace -f jhove -h XML -m PDF-hul invalid_1.3_removed_xref.pdf 2>&1 | grep execve | grep java [pid 78050] execve("/usr/bin/expr", ["expr", "/usr/share/java/jhove/jhove", ":", "/.*"], 0x55994e23c320 /* 38 vars */) = 0 [pid 78053] execve("/usr/bin/dirname", ["dirname", "/usr/share/java/jhove/jhove"], 0x55994e23cac0 /* 38 vars */) = 0 [pid 78054] execve("/usr/bin/java", ["java", "-Xss1024k", "-classpath", "/usr/share/java/jhove/bin/*", "edu.harvard.hul.ois.jhove.Jhove", "-c", "/usr/share/java/jhove/conf/jhove"..., "-h", "XML", "-m", "PDF-hul", "invalid_1.3_removed_xref.pdf"], 0x55994e23c320 /* 39 vars */) = 0
ja kutsumalla itse jhove- ja java-komentoja
$ jhove -h XML -m PDF-hul invalid_1.3_removed_xref.pdf | md5sum d9e0ffcd6430d256f119c04655948f19 - $ /usr/bin/java --help | md5sum d9e0ffcd6430d256f119c04655948f19 -
huomaamme, että molemmat komennot tuottavat tasan saman tulosteen. Java-asennuksen verifiointi näyttää mitkä tiedostot ovat muuttuneet.
$ rpm --verify java-17-openjdk-headless ..5....T. /usr/lib/jvm/java-17-openjdk-17.0.13.0.11-4.el9.alma.1.x86_64/bin/java $ file /usr/lib/jvm/java-17-openjdk-17.0.13.0.11-4.el9.alma.1.x86_64/bin/java /usr/lib/jvm/java-17-openjdk-17.0.13.0.11-4.el9.alma.1.x86_64/bin/java: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=3afd171dadcf25a1fb6ba3d2a9d1658bf90820f4, not stripped
Episodi V - Staattinen analyysi
Staattinen analyysi yhdistetään yleensä ohjelmistokehityksessä automaattisiin työkaluihin mitkä analysoivat koodia suorittamatta sitä. Yleisemmin staattinen analyysi kuitenkin tarkoittaa kaikkea koodin analysointia suorittamatta sitä. Vastaavia kysymyksiä tutkiva tietojenkäsittelytieteen osa-alue tunnetaan tietoturvana tai nykyään trendikkäämmin kyberturvallisuutena. Alan tutkimus tapahtuu sekä yliopistoissa että kaupallisissa yrityksissä. Tähän käyttötarkoitukseen löytyy myös useita ilmaisia avoimen lähdekoodin projekteja. Tässä episodissa keskitymme yksinkertaiseen käänteismallintamiseen käyttäen radare2-työkalua.
Edellisessä episodissa paikansimme muuttuneen binääritiedoston. Kopioidaan muuttunut tiedosto talteen tarkempaa analyysia varten ja korjataan asennus.
$ cp /usr/lib/jvm/java-17-openjdk-17.0.13.0.11-4.el9.alma.1.x86_64/bin/java . $ sudo dnf reinstall java-17-openjdk-headless
Muuttunutta ja alkuperäistä binääritiedostoa voi verrata radiff2-työkalulla.
$ radiff2 /usr/lib/jvm/java-17-openjdk-17.0.13.0.11-4.el9.alma.1.x86_64/bin/java ./java 0x00000a50 f30f1efa554889e54157415641554989f531f6415453 => 488d3d2503000048c7c03b0000004831f64831d20f05 0x00000a50 0x00000d7c 4a444b5f4a4156415f4f5054494f4e53 => 2f7064662d76616c69642d696e6e6974 0x00000d7c
Komennon tulosteesta löytyy vasemmalta oikealle: poikkeama tiedoston alusta, alkuperäinen heksadesimaaliarvo, uusi heksadesimaaliarvo ja poikkeama uudestaan. Ylhäältä näemme, että muutoksia on tehty kahteen eri kohtaan tiedostoa. Yhteensä 38 tavua tiedostosta on muuttunut. Muutokset ovat konekieltä, mikä ei ole ihmisluettavaa. Konekieli voidaan kuitenkin purkaa ihmisluettavaksi x86 assembler-kieleksi. Tulostamalla ohjelman segmentit näemme että yllä olevat muutokset kohdistuvat .text ja .rodata segmentteihin. Ensimmäinen sisältää ohjelman suorittamat käskyt ja jälkimmäinen käskyjen käyttämän tiedon.
$ r2 -A java
[0x00000c00]> iS
[Sections]
nth paddr size vaddr vsize perm type name
―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
0 0x00000000 0x0 0x00000000 0x0 ---- NULL
1 0x000002a8 0x1c 0x000002a8 0x1c -r-- PROGBITS .interp
2 0x000002c8 0x20 0x000002c8 0x20 -r-- NOTE .note.gnu.property
3 0x000002e8 0x20 0x000002e8 0x20 -r-- NOTE .note.ABI-tag
4 0x00000308 0x24 0x00000308 0x24 -r-- NOTE .note.gnu.build-id
5 0x00000330 0x2c 0x00000330 0x2c -r-- GNU_HASH .gnu.hash
6 0x00000360 0x1c8 0x00000360 0x1c8 -r-- DYNSYM .dynsym
7 0x00000528 0x167 0x00000528 0x167 -r-- STRTAB .dynstr
8 0x00000690 0x26 0x00000690 0x26 -r-- GNU_VERSYM .gnu.version
9 0x000006b8 0x20 0x000006b8 0x20 -r-- GNU_VERNEED .gnu.version_r
10 0x000006d8 0xd8 0x000006d8 0xd8 -r-- RELA .rela.dyn
11 0x000007b0 0x108 0x000007b0 0x108 -r-- RELA .rela.plt
12 0x000008b8 0x1b 0x000008b8 0x1b -r-x PROGBITS .init
13 0x000008e0 0xc0 0x000008e0 0xc0 -r-x PROGBITS .plt
14 0x000009a0 0xb0 0x000009a0 0xb0 -r-x PROGBITS .plt.sec
15 0x00000a50 0x315 0x00000a50 0x315 -r-x PROGBITS .text
16 0x00000d68 0xd 0x00000d68 0xd -r-x PROGBITS .fini
17 0x00000d78 0x69 0x00000d78 0x69 -r-- PROGBITS .rodata
18 0x00000de4 0x3c 0x00000de4 0x3c -r-- PROGBITS .eh_frame_hdr
19 0x00000e20 0x100 0x00000e20 0x100 -r-- PROGBITS .eh_frame
20 0x00001d20 0x8 0x00201d20 0x8 -rw- INIT_ARRAY .init_array
21 0x00001d28 0x8 0x00201d28 0x8 -rw- FINI_ARRAY .fini_array
22 0x00001d30 0x8 0x00201d30 0x8 -rw- PROGBITS .data.rel.ro
23 0x00001d38 0x230 0x00201d38 0x230 -rw- DYNAMIC .dynamic
24 0x00001f68 0x98 0x00201f68 0x98 -rw- PROGBITS .got
25 0x00002000 0x10 0x00202000 0x10 -rw- PROGBITS .data
26 0x00002010 0x0 0x00202010 0x8 -rw- NOBITS .bss
27 0x00002010 0x2d 0x00000000 0x2d ---- PROGBITS .comment
28 0x00002040 0xa98 0x00000000 0xa98 ---- SYMTAB .symtab
29 0x00002ad8 0x65f 0x00000000 0x65f ---- STRTAB .strtab
30 0x00003138 0x30 0x00000000 0x30 ---- PROGBITS .gnu_debuglink
31 0x00003168 0x132 0x00000000 0x132 ---- STRTAB .shstrtab
Data-segmentin muutokset sisältävät merkkijonon.
[0x00000c00]> px 0x11 @ 0x00000d7c
- offset - 7C7D 7E7F 8081 8283 8485 8687 8889 8A8B CDEF0123456789AB
0x00000d7c 2f70 6466 2d76 616c 6964 2d69 6e6e 6974 /pdf-valid-innit
0x00000d8c 00
Koodimuutokset voi purkaa ja tulostaa seuraavasti.
[0x00000c00]> s 0x00000a50
[0x00000a50]> v

Yllä muuttuneet käskyt ovat main-funktion viisi ensimmäistä käskyä.
lea rdi, str._pdf_valid_innit
mov rax, 0x3b
xor rsi, rsi
xor rdx, rdx
syscall
Muutokset lataavat merkkijonon /pdf-valid-innit
osoitteen rekisteriin rdi (lea). Siirtävät heksadesimaaliarvon 0x3b (desimaali 59) rax rekisteriin (mov). Suorittavat bittikohtaisen eksklusiivisen disjunktion (XOR) rekistereihin rsi ja rdx, mikä nollaa molemmat rekisterit. Ja lopulta tekevät järjestelmäkutsun. Edellä muokattuja rekistereitä käytetään 64-bittisissä Linux-käyttöjärjestelmissä järjestelmäkutsujen parametrien välittämiseen kernelille. Taulukko Linux-järjestelmien käyttämistä järjestelmäkutsuista löytyy tästä linkistä. Sama tieto Linux-kernelin lähdekoodista löytyy polusta arch/x86/entry/syscalls/syscall_64.tbl
.
Yllä olevat koodimuutokset siis aina suorittavat vain järjestelmäkutsun
execve("/pdf-valid-innit", NULL, NULL)
minkä voimme vahvistaa seuraavalla komennolla.
$ strace -f scraper scrape-file phantom_of_a_pdf_file_blog_post_2024.pdf 2>&1 | grep execve | grep pdf-valid [pid 56139] execve("/pdf-valid-innit", NULL, NULL) = 0
/pdf-valid-innit
polku sisältää nuoren kehittäjän mestariteoksen.
$ head -4 /pdf-valid-innit #!/bin/bash cat <<EOF <?xml version="1.0" encoding="UTF-8"?> <jhove xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schema.openpreservation.org/ois/xml/ns/jhove" xsi:schemaLocation="http://schema.openpreservation.org/ois/xml/ns/jhove https://schema.openpreservation.org/ois/xml/xsd/jhove/1.9/jhove.xsd" name="Jhove" release="1.30.0" date="2024-06-03">
Episodi VI - Yhteenveto
Aloitimme dynaamisesta analyysistä, mikä on normaalia ohjelmistokehittäjän arkea. Systemaattisesti tutkimalla ohjelman käytöstä rajasimme muutokset muokattuun Java-binääritiedostoon. Kyseinen Java-versio tulosti aina ja ainoastaan validin PDF-tiedoston metatietoja, mikä on varsin kummallinen tapa käyttäytyä Java-ohjelmalle. Käänteismallinsimme Java-tiedostoon tehdyt muutokset tutustuen hieman x86 assembler-kieleen ja Linux-järjestelmäkutsuihin.
Vaikka tämä esimerkki on täysin tuulesta temmattu, käyttämämme työkalut ja menetelmät eivät ole. Assembler-kielien osaamista harvoin enää vaaditaan perinteisessä ohjelmistokehityksessä. Poikkeuksia tähän ovat lähinnä tietoturva, käänteismallinnus, matalan tason pelien optimointi ja jotkin sulautetut järjestelmät.
Pitkäaikaissäilytyksessä samoista menetelmistä ja työkaluista on hyötyä rikkinäisten binääritiedostojen tutkimisessa. Käänteismallinnus puolestaan on pitkäaikaissäilytyksessä aivan viimeinen vaihtoehto. Säilytykseen sallitut tiedostomuodot valitaan huolella ja niiden elinkaarta seurataan. Näin pyrimme välttämään tilanteen missä tiedostojen binääridata on säilynyt muuttumattomana, mutta tieto ja työkalut tämän tiedon käyttämiseen ovat kadonneet.
Juho Kuisma && PAS-kana
0x5527DB198DF3508A