Skip to content
API Documentation
BastionGuard™
file_carver.cpp
Go to the documentation of this file.
1#include "file_carver.hpp"
2#include "i18n.hpp"
3
4#include <algorithm>
5#include <array>
6#include <filesystem>
7#include <fstream>
8#include <iomanip>
9#include <sstream>
10#include <utility>
11#include <vector>
12
13namespace fs = std::filesystem;
14
15namespace {
16constexpr uint64_t MiB = 1024ull * 1024ull;
17constexpr size_t WINDOW = 2 * 1024 * 1024;
18constexpr uint64_t MAX_FILE_SIZE = 128ull * 1024ull * 1024ull;
19
20struct Pattern {
21 const char* type;
22 const char* ext;
23 std::vector<unsigned char> header;
24 std::vector<unsigned char> footer;
25 bool footer_required {true};
26};
27
28std::vector<Pattern> patterns() {
29 return {
30 {"JPEG", ".jpg", {0xFF,0xD8,0xFF}, {0xFF,0xD9}, true},
31 {"PNG", ".png", {0x89,0x50,0x4E,0x47,0x0D,0x0A,0x1A,0x0A}, {0x49,0x45,0x4E,0x44,0xAE,0x42,0x60,0x82}, true},
32 {"PDF", ".pdf", {0x25,0x50,0x44,0x46,0x2D}, {0x25,0x25,0x45,0x4F,0x46}, true},
33 {"ZIP", ".zip", {0x50,0x4B,0x03,0x04}, {0x50,0x4B,0x05,0x06}, true},
34 {"GIF", ".gif", {0x47,0x49,0x46,0x38}, {0x3B}, true},
35 {"MP4", ".mp4", {0x00,0x00,0x00}, {'f','t','y','p'}, false}
36 };
37}
38
39bool starts_with_at(const std::vector<unsigned char>& buf, size_t pos, const std::vector<unsigned char>& pat) {
40 if (pos + pat.size() > buf.size()) return false;
41 return std::equal(pat.begin(), pat.end(), buf.begin() + static_cast<std::ptrdiff_t>(pos));
42}
43
44bool looks_like_mp4(const std::vector<unsigned char>& buf, size_t pos) {
45 if (pos + 8 > buf.size()) return false;
46 return buf[pos+4] == 'f' && buf[pos+5] == 't' && buf[pos+6] == 'y' && buf[pos+7] == 'p';
47}
48
49bool copy_range(DiskReader& reader, uint64_t offset, uint64_t size, const fs::path& out_path, std::string& error) {
50 std::ofstream out(out_path, std::ios::binary);
51 if (!out) {
52 error = trs("Unable to create output file") + std::string(": ") + out_path.string();
53 return false;
54 }
55
56 std::vector<unsigned char> buffer(1024 * 1024);
57 uint64_t remaining = size;
58 uint64_t cursor = offset;
59 while (remaining > 0) {
60 const size_t chunk = static_cast<size_t>(std::min<uint64_t>(buffer.size(), remaining));
61 if (!reader.read_at(cursor, buffer.data(), chunk)) {
62 error = trs("Read error while exporting carved file");
63 return false;
64 }
65 out.write(reinterpret_cast<const char*>(buffer.data()), static_cast<std::streamsize>(chunk));
66 if (!out) {
67 error = trs("Write error while exporting carved file");
68 return false;
69 }
70 cursor += chunk;
71 remaining -= chunk;
72 }
73 return true;
74}
75
77 uint64_t range_offset,
78 uint64_t range_size,
79 const std::vector<Pattern>& pats,
80 const std::string& output_dir,
81 std::vector<CarvedFile>& carved,
82 std::vector<std::string>& logs,
83 std::string& error) {
84 if (range_size == 0) return true;
85
86 const uint64_t file_size = reader.size();
87 if (range_offset >= file_size) return true;
88 const uint64_t range_end = std::min<uint64_t>(file_size, range_offset + range_size);
89
90 std::vector<unsigned char> buffer(WINDOW);
91
92 uint64_t base = range_offset;
93 while (base < range_end) {
94 const size_t to_read = static_cast<size_t>(std::min<uint64_t>(WINDOW, range_end - base));
95 if (!reader.read_at(base, buffer.data(), to_read)) {
96 error = trs("Read error during file carving");
97 return false;
98 }
99 std::vector<unsigned char> chunk(buffer.begin(), buffer.begin() + static_cast<std::ptrdiff_t>(to_read));
100
101 for (size_t i = 0; i < chunk.size(); ++i) {
102 for (const auto& p : pats) {
103 bool match = false;
104 if (std::string(p.type) == "MP4") {
105 match = looks_like_mp4(chunk, i);
106 } else {
107 match = starts_with_at(chunk, i, p.header);
108 }
109 if (!match) continue;
110
111 const uint64_t abs_off = base + i;
112 if (abs_off >= range_end) continue;
113 if (!carved.empty() && abs_off < carved.back().offset + carved.back().size) {
114 continue;
115 }
116
117 uint64_t found_size = 0;
118 if (p.footer_required) {
119 const uint64_t search_limit = std::min<uint64_t>(range_end - abs_off, MAX_FILE_SIZE);
120 std::vector<unsigned char> probe(static_cast<size_t>(search_limit));
121 if (!reader.read_at(abs_off, probe.data(), probe.size())) continue;
122 for (size_t j = p.header.size(); j + p.footer.size() <= probe.size(); ++j) {
123 if (std::equal(p.footer.begin(), p.footer.end(), probe.begin() + static_cast<std::ptrdiff_t>(j))) {
124 found_size = j + p.footer.size();
125 break;
126 }
127 }
128 if (found_size == 0) continue;
129 } else {
130 found_size = std::min<uint64_t>(16ull * MiB, range_end - abs_off);
131 }
132
133 std::ostringstream name;
134 name << std::string(p.type) << '_' << std::setw(12) << std::setfill('0') << abs_off << p.ext;
135 fs::path out_path = fs::path(output_dir) / name.str();
136 std::string copy_error;
137 if (!copy_range(reader, abs_off, found_size, out_path, copy_error)) {
138 error = copy_error;
139 return false;
140 }
141
142 carved.push_back(CarvedFile{
143 p.type,
144 p.ext,
145 abs_off,
146 found_size,
147 p.footer_required ? 0.72 : 0.45,
148 p.footer_required ? trs("Recovered by header/footer carving") : trs("Recovered by header-based carving"),
149 out_path.string()
150 });
151 logs.push_back(trs("Recovered carved file:") + std::string(" ") + out_path.string());
152 break;
153 }
154 }
155 if (base + to_read >= range_end) break;
156 base += MiB;
157 }
158
159 return true;
160}
161} // namespace
162
164 const std::string& output_dir,
165 std::vector<CarvedFile>& carved,
166 std::vector<std::string>& logs,
167 std::string& error) {
168 return carve_ranges_to_directory(reader, {{0, reader.size()}}, output_dir, carved, logs, error);
169}
170
172 const std::vector<std::pair<uint64_t, uint64_t>>& ranges,
173 const std::string& output_dir,
174 std::vector<CarvedFile>& carved,
175 std::vector<std::string>& logs,
176 std::string& error) {
177 carved.clear();
178 const auto pats = patterns();
179
180 try {
181 fs::create_directories(output_dir);
182 } catch (const std::exception& ex) {
183 error = ex.what();
184 return false;
185 }
186
187 logs.push_back(trs("File carving started."));
188 for (const auto& range : ranges) {
189 if (!carve_one_range(reader, range.first, range.second, pats, output_dir, carved, logs, error)) {
190 return false;
191 }
192 }
193 logs.push_back(trs("File carving completed."));
194 return true;
195}
bool read_at(uint64_t offset, void *buffer, size_t len)
uint64_t size() const
static bool carve_ranges_to_directory(DiskReader &reader, const std::vector< std::pair< uint64_t, uint64_t > > &ranges, const std::string &output_dir, std::vector< CarvedFile > &carved, std::vector< std::string > &logs, std::string &error)
static bool carve_to_directory(DiskReader &reader, const std::string &output_dir, std::vector< CarvedFile > &carved, std::vector< std::string > &logs, std::string &error)
std::string trs(const char *msgid)
Definition i18n.cpp:22
bool copy_range(DiskReader &reader, uint64_t offset, uint64_t size, const fs::path &out_path, std::string &error)
std::vector< Pattern > patterns()
bool carve_one_range(DiskReader &reader, uint64_t range_offset, uint64_t range_size, const std::vector< Pattern > &pats, const std::string &output_dir, std::vector< CarvedFile > &carved, std::vector< std::string > &logs, std::string &error)
bool starts_with_at(const std::vector< unsigned char > &buf, size_t pos, const std::vector< unsigned char > &pat)
bool looks_like_mp4(const std::vector< unsigned char > &buf, size_t pos)