Поиск данных в большом двоичном файле и вывод с учетом контекста

Пролог/Контекст

На прошлой неделе моя корневая файловая система была перемонтирована несколько раз только для чтения, и я сделал полный снимок с помощью ddrescue. К сожалению, файловая система уже повреждена, и некоторые файлы отсутствуют. На данный момент я пытаюсь найти свою базу данных пользователей ejabberd, которая должна быть где-то внутри изображения. Testdisk нашел нужный файл (помечен как удаленный), но не смог его восстановить. Поскольку файл довольно маленький, а у меня есть резервная копия месячной давности, я подумал о бинарном поиске по всему изображению.

Итак, теперь у меня есть файл размером 64 ГБ с поврежденной файловой системой, и я хотел бы извлечь несколько блоков размером 4 КБ, которые содержат определенный шаблон.

Вопрос

Как я могу найти данные в большом файле размером 64 ГБ и извлечь результат с некоторым контекстом (4 КБ)?

Поскольку образ файловой системы находится на моем сервере, я бы предпочел инструмент linux cli.


person JepZ    schedule 18.07.2015    source источник
comment
Как насчет простого grep вместе с его элементом управления строкой контекста и шаблоном поиска, который ранее был экранирован регулярным выражением?   -  person arkascha    schedule 18.07.2015
comment
когда даже testdisk выходит из строя, это может стать трудным. какая файловая система была? Кстати, прежде чем начинать какие-либо действия с образом, сначала создайте резервную копию.   -  person hek2mgl    schedule 18.07.2015
comment
@arkascha Я попробовал grep -U -b -A 20 "$(printf $'\x64\x6f\x6f')" /dev/loop0, но он только сказал мне, что это совпадение, но не где, и не вывел никакого содержимого.   -  person JepZ    schedule 18.07.2015
comment
@hek2mgl тем временем я нашел некоторые данные с помощью Curses Hexedit кажется, что данные есть, но это очень ручной и трудоемкий процесс. На диске файловая система ext4. Я сделал копию образа ddrescue перед тем, как начать над ним работать ;-)   -  person JepZ    schedule 18.07.2015
comment
Размер файла меньше 4кб?   -  person hek2mgl    schedule 18.07.2015
comment
Размер файла @hek2mgl составляет от 4 до 16 КБ.   -  person JepZ    schedule 18.07.2015
comment
Если размер файла превышает 4 КБ, он может быть распределен по нескольким секторам по 4 КБ в любом месте диска.   -  person hek2mgl    schedule 18.07.2015
comment
Как это произошло? Было ли это результатом проблемы с моментальным снимком BTRFS?   -  person David C. Rankin    schedule 19.07.2015
comment
@DavidC.Rankin Нет, просто умирающий SSD. Первые два раза я просто перезагрузил сервер, когда у него был корневой каталог перемонтирования только для чтения. Когда это произошло в третий раз, я сделал резервную копию, но повреждение уже было сделано   -  person JepZ    schedule 19.07.2015


Ответы (1)


Инструмент

Так как я не смог найти инструмент, отвечающий моим требованиям, я написал его сам на golang. Я называю это bima (для двоичного соответствия). Это не красиво, но это сделало работу:

package main

import (
    "bytes"
    "encoding/hex"
    "fmt"
    "gopkg.in/alecthomas/kingpin.v1"
    "io"
    "log"
    "math"
    "os"
)

var (
    debug       = kingpin.Flag("debug", "Enable debug mode.").Short('d').Bool()
    bsize       = kingpin.Flag("blocksize", "Blocksize").Short('b').Default("126976").Int()
    debugDetail = kingpin.Flag("debugdetail", "Debug Detail").Short('v').Default("10").Int()

    matchCommand      = kingpin.Command("match", "Match a value")
    matchCommandValue = matchCommand.Arg("value", "The value (Hex Encoded e.g.: 616263 == abc)").Required().String()
    matchCommandFile  = matchCommand.Arg("file", "The file").Required().String()
)

func main() {
    kingpin.Version("0.1")
    mode := kingpin.Parse()

    if *bsize <= 0 {
        log.Fatal("The blocksize has to be larger than 0")
    }

    if *debugDetail <= 0 {
        log.Fatal("The Debug Detail has to be larger than 0")
    }

    if mode == "match" {
        searchBytes, err := hex.DecodeString(*matchCommandValue)
        if err != nil {
            log.Fatal(err)
        }

        scanFile(searchBytes, *matchCommandFile)
    }
}

func scanFile(search []byte, path string) {
    searchLength := len(search)
    blocksize := *bsize

    f, err := os.Open(path)
    if err != nil {
        log.Fatal(err)
    }
    defer f.Close()

    fi, err := f.Stat()
    if err != nil {
        log.Fatal(err)
    }
    filesize := fi.Size()
    expectedRounds := int(math.Ceil(float64(filesize-int64(searchLength))/float64(blocksize)) + 1)
    if expectedRounds <= 0 {
        expectedRounds = 1
    }

    data := make([]byte, 0, blocksize+searchLength-1)
    data2 := make([]byte, 0, blocksize+searchLength-1)
    offset := make([]byte, searchLength-1)
    //reading the len of the slice or less (but not the cap)
    readCount, err := f.Read(offset)
    if err == io.EOF {
        fmt.Println("The files seems to be empty")
        return
    } else if err != nil {
        log.Fatal(err)
    }

    data = append(data, offset...)

    buffer := make([]byte, blocksize)
    var blockpos int
    var idx int
    blockpos = 0

    lastLevel := -1
    roundLevel := 0
    idxOffset := 0
    for round := 0; ; round++ {
        if *debug {
            roundLevel = ((round * 100) / expectedRounds)
            if (roundLevel%*debugDetail == 0) && (roundLevel > lastLevel) {
                lastLevel = roundLevel
                fmt.Fprintln(os.Stderr, "Starting round", round+1, "of", expectedRounds, "--", ((round * 100) / expectedRounds))
            }
        }

        //At EOF, the count will be zero and err will be io.EOF
        readCount, err = f.Read(buffer)
        if err != nil {
            if err == io.EOF {
                if *debug {
                    fmt.Fprintln(os.Stderr, "Done - Found EOF")
                }
                break
            }
            fmt.Println(err)
            return
        }

        data = append(data, buffer[:readCount]...)
        data2 = data
        idxOffset = 0
        for {
            idx = bytes.Index(data2, search)
            if idx >= 0 {
                fmt.Println(blockpos + idxOffset + idx)
                if idx+searchLength < len(data2) {
                    data2 = data2[idx+searchLength:]
                    idxOffset += idx
                } else {
                    break
                }
            } else {
                break
            }
        }
        data = data[readCount:]
        blockpos += readCount
    }
}

История

Для полноты вот что я сделал, чтобы решить мою проблему:

Сначала я использовал hexedit, чтобы узнать, что все файлы db имеют одинаковый заголовок. В шестнадцатеричном коде это выглядит так: 0102030463584d0b0000004b62574c41

Поэтому я использовал свой инструмент, чтобы найти все вхождения в моем файле sda.image:

./bima match 0102030463584d0b0000004b62574c41 ./sda.image >DBfiles.txt

Для 64 ГБ это заняло около 8 минут, и я думаю, что ограничивающим фактором был жесткий диск.

В результате получилось около 1200 вхождений, которые я извлек из образа с помощью dd. Поскольку я не знал точного размера файлов, я просто извлек куски по 20 000 байт:

for f in $(cat DBfiles.txt); do
    dd if=sda.image of=$f.dunno bs=1 ibs=1 skip=$f count=20000
done

Теперь у меня было около 1200 файлов и нужно было найти нужные. На первом этапе я ищу файлы passwd (passwd.DCD и passwd.DCL). позже я сделал то же самое для файлов реестра. Поскольку заголовок файлов содержит имя, я просто искал пароль:

for f in *.dunno; do
    if [ "$(cat $f | head -c 200 | grep "passwd" | wc -l)" == "1" ]; then
        echo "$f" | sed 's/\.$//g' >> passwd_files.list
    fi
done

Поскольку куски были больше, чем файлы, мне пришлось искать конец каждого файла вручную. Я внес исправления с помощью Curses Hexedit.

Во время этого процесса я мог видеть, что заголовок каждого файла содержит либо dcl_logk, либо dcd_logk. Так что я знал, какие из файлов были файлами DCL, а какие — файлами DCD.

В конце концов у меня был каждый файл до десяти раз, и мне нужно было решить, какую версию я хочу использовать. В общем я взял самый большой файл. После помещения файлов в каталог DB нового сервера ejabberd и его перезапуска все учетные записи снова восстанавливаются. :-)

person JepZ    schedule 19.07.2015