在大型二进制文件中查找数据并使用上下文

时间:2015-07-18 08:53:41

标签: linux binary command-line-interface extraction

序言/背景

上周,我的根文件系统重新安装了几次,我通过ddrescue拍摄了一个完整的快照。遗憾的是,文件系统已经损坏,一些文件丢失了。目前我试图找到我的ejabberd用户数据库,它应该在图像中的某个位置。 Testdisk找到了所需的文件(标记为已删除)但无法恢复。由于文件非常小,而且我在几个月前有备份,所以我想在整个图像上进行二分搜索。

所以现在我有一个带有损坏文件系统的64GB文件,想要提取一些包含某种模式的4kb块。

问题

如何在64GB大文件中查找数据并使用某些上下文(4kb)提取结果?

由于文件系统映像驻留在我的服务器上,我更喜欢使用linux cli工具。

1 个答案:

答案 0 :(得分:0)

工具

由于我无法找到符合我要求的工具,所以我自己在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

对于64GB这需要大约8分钟,我认为硬盘驱动器是限制因素。

使用dd从图像中提取的约1200次出现的结果。由于我不知道文件的确切大小,我只是提取了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)。后来我为名册文件做了同样的事情。由于文件的标题包含名称,我只是为了passwd:

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_logkdcd_logk。所以我知道哪些文件是DCL文件,哪些是DCD文件。

最后我将每个文件最多放十次,并且必须决定我想要使用哪个版本。一般来说,我拿了最大的文件。将文件放入新ejabberd服务器的DB目录并重新启动后,所有帐户都会再次返回。 : - )