logo

deblob

remove binary executables from a directory git clone https://anongit.hacktivis.me/git/deblob.git/

main.ha (14667B)


  1. // Copyright © 2019 deblob Authors <https://hacktivis.me/projects/deblob>
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. use bytes;
  4. use encoding::json;
  5. use endian;
  6. use errors;
  7. use fmt;
  8. use fnmatch;
  9. use fs;
  10. use getopt;
  11. use io;
  12. use os;
  13. use path;
  14. use strings;
  15. let excludes: []str = [];
  16. let noop: bool = false;
  17. let check: bool = false;
  18. let json: bool = false;
  19. const beam: []u8 = ['F', 'O', 'R', '1']; // Erlang BEAM
  20. const magic: [_](str, []u8) = [
  21. ("ELF", [0x7F, 'E', 'L', 'F']),
  22. ("Unix ar(1)", ['!', '<', 'a', 'r', 'c', 'h', '>', '\n']),
  23. ("PC-BIOS", [0x55, 0xAA]),
  24. ("Erlang FOR1 BEAM", ['F', 'O', 'R', '1']),
  25. ("Java .class / MachO exec", [0xCA, 0xFE, 0xBA, 0xBE]),
  26. ("WinNT EXE", ['M', 'Z', 0x90, 0x00, 0x03, 0x00, 0x00, 0x00]), // Partial MSDOS stub header (ECMA-335 Common Language Infrastructure)
  27. ("Lua bytecode", [0x1B, 'L', 'u', 'a']),
  28. ("Wasm", [0x00, 'a', 's', 'm']),
  29. ("Apple PEF", ['J', 'o', 'y', '!', 'p', 'e', 'f', 'f']), // Apple Preferred Executable Format
  30. ("DTB", [0xD0, 0x0D, 0xFE, 0xED]), // Device Tree Blob (OpenFirmware, u-boot, …)
  31. // Python *.pyc bytecode magic numbers (defined in importlib/_bootstrap_external.py)
  32. ("Python pyc 2.7", [0x03, 0xF3, '\r', '\n']), // (62211i little-endian)
  33. ("Python pyc 3.8", [0x55, 0x0D, '\r', '\n']), // (3413i litte-endian)
  34. ("Python pyc 3.9", [0x61, 0x0D, '\r', '\n']), // (3425i litte-endian)
  35. ("Python pyc 3.10", [0x6F, 0x0D, '\r', '\n']), // (3439i litte-endian)
  36. ("Python pyc 3.11", [0xA7, 0x0D, '\r', '\n']), // (3495i litte-endian)
  37. ("Python pyc 3.12", [0xCB, 0x0D, '\r', '\n']), // (3531i litte-endian)
  38. // Python pickle object data, similarly to Perl Storage it's dangerous enough to cause code execution
  39. ("Python Pickle v2", [0x80, 0x02]), // Protocol 2 + start of frame
  40. ("Python Pickle v3", [0x80, 0x03]), // Protocol 3 + start of frame
  41. ("Python Pickle v4", [0x80, 0x04, 0x95]), // Protocol 4 + start of frame
  42. ("Python Pickle v5", [0x80, 0x05, 0x95]), // Protocol 5 + start of frame
  43. // https://github.com/MoarVM/MoarVM/blob/master/docs/bytecode.markdown
  44. ("MoarVM bytecode", ['M', 'O', 'A', 'R', 'V', 'M', '\r', '\n']),
  45. // https://github.com/parrot/parrot/blob/master/docs/parrotbyte.pod
  46. ("Parrot bytecode", [0xFE, 'P', 'B', 'C', '\r', '\n', 0x1A, '\n']),
  47. ("Perl storable v0.6", ['p', 'e', 'r', 'l', '-', 's', 't', 'o', 'r', 'e']),
  48. ("Perl storable v0.7", ['p', 's', 't', '0']),
  49. ("Chez Scheme bytecode", [0x00, 0x00, 0x00, 0x00, 'c', 'h', 'e', 'z']),
  50. ("NekoVM bytecode", ['N', 'E', 'K', 'O']),
  51. ("Emacs Lisp bytecode", [';', 'E', 'L', 'C']), // Emacs lisp bytecode, if there is known false positives next 4 bytes is the version
  52. ("OCaml", ['C', 'a', 'm', 'l', '1', '9', '9', '9']),
  53. ("Ren'Py Archive v1", [0x78, 0x9c]),
  54. ("Ren'Py Archive v2", ['R', 'P', 'A', '-', '2', '.', '0', ' ']),
  55. ("Ren'Py Archive v3", ['R', 'P', 'A', '-', '3', '.', '0', ' ']),
  56. ("Squirrel bytecode", [0xFA, 0xFA]),
  57. ("Clang Pre-Compiled-Header", ['C', 'P', 'C', 'H']),
  58. // Clang Pre-Compiled-Header, followed by Info Block, see:
  59. // - clang/lib/Serialization/ASTWriter.cpp ASTWriter::WriteAST
  60. // - clang/lib/Serialization/ASTReader.cpp doesntStartWithASTFileMagic
  61. // Excluded from fixtures (200KB+), test with:
  62. // echo > empty.h && clang -cc1 -nobuiltininc -emit-pch -o empty.h.pch empty.h
  63. ("GCC Pre-Compiled-Header", ['g', 'p', 'c', 'h']),
  64. // Excluded from fixtures (1.2MB+), test with:
  65. // echo > empty.h && gcc empty.h
  66. ("GCC Rust Metadata", ['G', 'R', 'S', 'T']), // GCC Rust Metadata (*.rox)
  67. ("Dart Kernel snapshot", [0x90, 0xab, 0xcd, 0xef]),
  68. // why are these 2 different, and is the C one still in use?
  69. // no clue, but both are in the sdk repo.
  70. ("Dart JIT snapshot (C code)", [0xdc, 0xdc, 0xf5, 0xf5]), // Dart JIT snapshot, if done from the C code.
  71. ("Dart JIT snapshot (Dart code)", [0xdc, 0xdc, 0xf6, 0xf6]), // Dart JIT snapshot, if done from the Dart code.
  72. ];
  73. const dos_magic: []u8 = ['M', 'Z'];
  74. const pe_magic: []u8 = ['P', 'E', 0x00, 0x00];
  75. const racket: []u8 = ['r', 'a', 'c', 'k', 'e', 't'];
  76. const zip: []u8 = ['P', 'K', 0x03, 0x04];
  77. const jar: []u8 = [0xFE, 0xCA, 0, 0];
  78. const shebang: []u8 = ['#', '!'];
  79. let found: bool = false;
  80. let json_out: io::handle = 0;
  81. fn id_blob(filename: str) (void | str | fs::error | io::error) = {
  82. static let buffer: [4096]u8 = [0...];
  83. const file = os::open(filename)?;
  84. defer io::close(file)!;
  85. if (io::read(file, buffer)? is io::EOF) {
  86. // empty file
  87. return void;
  88. };
  89. for (let i = 0z; i < len(magic); i += 1) {
  90. assert(len(magic[i].1) > 0);
  91. if (bytes::hasprefix(buffer, magic[i].1)) {
  92. return magic[i].0;
  93. };
  94. };
  95. // Special check to detect *all* Microsoft Portable Executable files
  96. if (bytes::hasprefix(buffer, dos_magic)) {
  97. const pe_offset = endian::legetu32(buffer[60..64]);
  98. if ((pe_offset <= 4096-4) && bytes::hasprefix(buffer[pe_offset..pe_offset+4], pe_magic)) {
  99. return "WinNT EXE";
  100. };
  101. };
  102. // detect binary escripts (PKZIP archive and BEAM supported)
  103. if (bytes::hasprefix(buffer, shebang)) {
  104. let comment = true;
  105. for (let i = 0z; i < 4096; i += 1) {
  106. if(comment) {
  107. if(buffer[i] == '\n') comment = false;
  108. continue;
  109. };
  110. if(buffer[i] == '%') {
  111. comment = true;
  112. } else {
  113. // First bytes after comments
  114. if(bytes::equal(zip, buffer[i..i+len(zip)])) return "Erlang ZIP BEAM";
  115. if(bytes::equal(beam, buffer[i..i+len(beam)])) return "Erlang #! BEAM";
  116. // source code as script
  117. break;
  118. };
  119. };
  120. };
  121. // Special check to detect racket bytecode
  122. if (bytes::hasprefix(buffer, ['#', '~'])) {
  123. // From src/expander/compile/write-linklet.rkt in racket:
  124. // - #~
  125. // - length-prefixed version string (ie. "\x038.5")
  126. // - length-prefixed virtual machine string (ie. "\x06racket")
  127. // - 'D' / 'B'
  128. // Here it verifies that the virtual machine string is racket, assuming none other is supported in the wild.
  129. // Racket itself only matches against '#~' which is small & only printable-ASCII, so too prone to false positives
  130. const version_len = buffer[2];
  131. const version_end = 2+version_len;
  132. const racket_len = buffer[version_end+1];
  133. const racket_start = version_end+2;
  134. if(bytes::equal(racket, buffer[racket_start..racket_start+racket_len])) {
  135. return "Racket";
  136. };
  137. };
  138. if (bytes::hasprefix(buffer, zip)) {
  139. if(bytes::equal(jar, buffer[0x27..0x2B])) {
  140. return "Java JAR";
  141. };
  142. };
  143. return void;
  144. };
  145. @test fn id_blob() void = {
  146. const sources = [
  147. "test/fixtures/empty",
  148. "test/fixtures/empty.dts",
  149. "test/fixtures/hello-dart.dart",
  150. "test/fixtures/hello-ocaml.ml",
  151. "test/fixtures/hello-racket.rkt",
  152. "test/fixtures/hello.1",
  153. "test/fixtures/hello.c",
  154. "test/fixtures/hello.cs",
  155. "test/fixtures/hello.el",
  156. "test/fixtures/hello.erl",
  157. "test/fixtures/hello.erl.escript",
  158. "test/fixtures/hello.java",
  159. "test/fixtures/hello.lua",
  160. "test/fixtures/hello.neko",
  161. "test/fixtures/hello.nqp",
  162. "test/fixtures/hello.nut",
  163. "test/fixtures/hello.pir",
  164. "test/fixtures/hello.py",
  165. "test/fixtures/hello.wat",
  166. "test/fixtures/perl_storage.pm",
  167. ];
  168. for (let i = 0z; i < len(sources); i += 1) {
  169. match(id_blob(sources[i])!) {
  170. case void =>
  171. continue;
  172. case let s: str =>
  173. fmt::fatalf(
  174. "deblob: error: id_blob({}) got wrongly detected as: {}",
  175. sources[i], s
  176. );
  177. };
  178. };
  179. const blobs = [
  180. ("Racket", "test/fixtures/compiled/hello-racket_rkt.zo"),
  181. ("DTB", "test/fixtures/empty.dtb"),
  182. ("ELF", "test/fixtures/hello"),
  183. ("Dart Kernel snapshot", "test/fixtures/hello-dart.dill"),
  184. ("Dart JIT snapshot (Dart code)", "test/fixtures/hello-dart.jit"),
  185. ("Unix ar(1)", "test/fixtures/hello-ocaml.a"),
  186. ("OCaml", "test/fixtures/hello-ocaml.cma"),
  187. ("OCaml", "test/fixtures/hello-ocaml.cmi"),
  188. ("OCaml", "test/fixtures/hello-ocaml.cmo"),
  189. ("OCaml", "test/fixtures/hello-ocaml.cmx"),
  190. ("OCaml", "test/fixtures/hello-ocaml.cmxa"),
  191. ("ELF", "test/fixtures/hello-ocaml.o"),
  192. ("Unix ar(1)", "test/fixtures/hello.a"),
  193. ("Erlang FOR1 BEAM", "test/fixtures/hello.beam"),
  194. ("Erlang #! BEAM", "test/fixtures/hello.beam.escript"),
  195. ("Java .class / MachO exec", "test/fixtures/hello.class"),
  196. ("Squirrel bytecode", "test/fixtures/hello.cnut"),
  197. ("Emacs Lisp bytecode", "test/fixtures/hello.elc"),
  198. ("WinNT EXE", "test/fixtures/hello.exe"),
  199. ("Java JAR", "test/fixtures/hello.jar"),
  200. ("Lua bytecode", "test/fixtures/hello.luac53"),
  201. ("Lua bytecode", "test/fixtures/hello.luac54"),
  202. ("NekoVM bytecode", "test/fixtures/hello.n"),
  203. ("MoarVM bytecode", "test/fixtures/hello.nqp.moarvm"),
  204. ("ELF", "test/fixtures/hello.o"),
  205. ("Parrot bytecode", "test/fixtures/hello.pir.pbc"),
  206. ("Wasm", "test/fixtures/hello.wasm"),
  207. ("WinNT EXE", "test/fixtures/monodx.dll"),
  208. ("Perl storable v0.7", "test/fixtures/perl_storage.pst"),
  209. ("Python Pickle v4", "test/fixtures/pickle/hello.4.pickle"),
  210. ("Python Pickle v5", "test/fixtures/pickle/hello.5.pickle"),
  211. ("Apple PEF", "test/fixtures/qemu_vga.ndrv"),
  212. //("", "test/fixtures/option.rom"),
  213. ];
  214. for (let i = 0z; i < len(blobs); i += 1) {
  215. match(id_blob(blobs[i].1)!) {
  216. case void =>
  217. fmt::fatalf(
  218. "deblob: error: id_blob({}) didn't got detected as: {}",
  219. blobs[i].1, blobs[i].0
  220. );
  221. case let s: str =>
  222. if(s != blobs[i].0)
  223. {
  224. fmt::fatalf(
  225. "deblob: error: id_blob({}) got identified as \"{}\" instead of \"{}\"",
  226. blobs[i].1, s, blobs[i].0
  227. );
  228. };
  229. };
  230. };
  231. };
  232. fn is_excluded(filename: str) bool = {
  233. for (let i = 0z; i < len(excludes); i += 1) {
  234. if (fnmatch::fnmatch(excludes[i], filename, fnmatch::flag::NONE)) {
  235. return true;
  236. };
  237. };
  238. return false;
  239. };
  240. fn append_action(action: str, filename: str, format: str) void = {
  241. if(!json) return;
  242. let obj = json::object { ... };
  243. json::put(&obj, "action", action);
  244. defer json::take(&obj, "action");
  245. json::put(&obj, "path", filename);
  246. defer json::take(&obj, "path");
  247. json::put(&obj, "format", format);
  248. defer json::take(&obj, "format");
  249. let obj_s = json::dumpstr(obj);
  250. defer free(obj_s);
  251. static let first_obj: bool = true;
  252. if(first_obj)
  253. {
  254. fmt::fprintf(json_out, "\n\t{}", obj_s)!;
  255. first_obj = false;
  256. }
  257. else
  258. {
  259. fmt::fprintf(json_out, ",\n\t{}", obj_s)!;
  260. };
  261. };
  262. fn check_dir(dirname: str) (void | errors::invalid | io::error) = {
  263. const iter = match (os::iter(dirname)) {
  264. case let iter: *fs::iterator =>
  265. yield iter;
  266. case let err: fs::error =>
  267. fmt::errorfln("deblob: error: Failed walking directory '{}': {}", dirname, fs::strerror(err))!;
  268. return errors::invalid;
  269. };
  270. defer fs::finish(iter);
  271. for (true) {
  272. const ent: fs::dirent = match (fs::next(iter)) {
  273. case let ent: fs::dirent =>
  274. yield ent;
  275. case let e: fs::error =>
  276. fmt::errorfln("deblob: error: Failed walking directory '{}': {}", dirname, fs::strerror(e))?;
  277. break;
  278. case done =>
  279. break;
  280. };
  281. const filename_path = path::init(dirname, ent.name)!;
  282. const filename = path::string(&filename_path);
  283. if (fs::isdir(ent.ftype)) {
  284. check_dir(filename)?;
  285. } else if(fs::isfile(ent.ftype)) {
  286. const blob_id = match (id_blob(filename)) {
  287. case void =>
  288. continue;
  289. case let s: str =>
  290. yield s;
  291. case let err: fs::error =>
  292. fmt::errorfln("deblob: error: Failed opening {}: {}",
  293. filename, fs::strerror(err))!;
  294. continue;
  295. case let err: io::error =>
  296. fmt::errorfln("deblob: error: Failed reading {}: {}",
  297. filename, io::strerror(err))!;
  298. continue;
  299. };
  300. if (is_excluded(filename)) {
  301. append_action("ignoring", filename, blob_id);
  302. fmt::printfln("ignoring {}:\t{}", blob_id, filename)!;
  303. continue;
  304. };
  305. found = true;
  306. if (noop) {
  307. append_action("detected", filename, blob_id);
  308. fmt::printfln("detected {}:\t{}", blob_id, filename)!;
  309. continue;
  310. };
  311. append_action("removing", filename, blob_id);
  312. fmt::printfln("removing {}:\t{}", blob_id, filename)!;
  313. match (os::remove(filename)) {
  314. case void =>
  315. continue;
  316. case let e: fs::error =>
  317. fmt::errorfln("deblob: error: Failed removing file '{}':\t{}",
  318. filename, fs::strerror(e))!;
  319. };
  320. } else {
  321. // ignore non-(dir/regular-file) like symlinks, blocks, fifo, …
  322. continue;
  323. };
  324. };
  325. };
  326. @test fn check_dir() void = {
  327. const dirname = "test/check_dir-fixtures";
  328. const files_before = match (os::readdir(dirname)) {
  329. case let d: []fs::dirent =>
  330. yield d;
  331. case let e: fs::error =>
  332. fmt::fatalf("deblob: error: os::readdir({}): {}", dirname, fs::strerror(e));
  333. };
  334. assert(len(files_before) == 60);
  335. const ret = check_dir(dirname);
  336. assert(ret is void);
  337. const files_after = match (os::readdir(dirname)) {
  338. case let d: []fs::dirent =>
  339. yield d;
  340. case let e: fs::error =>
  341. fmt::fatalf("deblob: error: os::readdir({}): {}", dirname, fs::strerror(e));
  342. };
  343. assert(len(files_after) == 30);
  344. };
  345. export fn main() void = {
  346. const cmd = getopt::parse(os::args,
  347. "Remove binary executable files",
  348. ('c', "Return error if any non-excluded blobs were found"),
  349. ('e', "NAME", "Exclude filename from removal (defaults to none)"),
  350. ('d', "PATH", "Set working directory (default to current dir)"),
  351. ('j', "PATH", "JSON output file"),
  352. ('n', "No actual removal, only scan and log"),
  353. );
  354. defer getopt::finish(&cmd);
  355. defer free(excludes);
  356. let opt_d = "";
  357. let json_out_path = "";
  358. for (let i = 0z; i < len(cmd.opts); i += 1) {
  359. const opt = cmd.opts[i];
  360. switch (opt.0) {
  361. case 'c' =>
  362. check = true;
  363. case 'e' =>
  364. append(excludes, opt.1);
  365. case 'd' =>
  366. opt_d = opt.1;
  367. case 'n' =>
  368. noop = true;
  369. case 'j' =>
  370. json = true;
  371. json_out_path = opt.1;
  372. case =>
  373. fmt::fatalf("deblob: error: Unhandled option -{}", opt.0);
  374. };
  375. };
  376. if(json_out_path != "")
  377. {
  378. json_out = match (os::create(json_out_path, fs::mode::USER_RW | fs::mode::GROUP_R | fs::mode::OTHER_R)) {
  379. case let f: io::file =>
  380. yield f;
  381. case let e: fs::error =>
  382. fmt::fatalf("deblob: error: Failed creating/opening file '{}' for JSON output: {}", json_out_path, fs::strerror(e));
  383. };
  384. fmt::fprint(json_out, "[")!;
  385. };
  386. if(opt_d != "")
  387. {
  388. match (os::chdir(opt_d)) {
  389. case let e: fs::error =>
  390. fmt::fatalf("deblob: error: Failed changing current directory to '{}': {}", opt_d, fs::strerror(e));
  391. case void =>
  392. void;
  393. };
  394. };
  395. fmt::println(":: Checking for blobs")!;
  396. const ret = check_dir(".");
  397. fmt::println(":: Done checking for blobs")!;
  398. if(json_out_path != "")
  399. {
  400. fmt::fprint(json_out, "\n]")!;
  401. match(io::close(json_out)) {
  402. case void =>
  403. void;
  404. case let e: io::error =>
  405. fmt::fatalf("deblob: error: Failed closing JSON output file '{}': {}", json_out_path, io::strerror(e));
  406. };
  407. };
  408. match (ret) {
  409. case void =>
  410. if(check && found) os::exit(2);
  411. os::exit(0);
  412. case errors::invalid =>
  413. os::exit(1);
  414. case let e: io::error =>
  415. fmt::errorfln("deblob: error: I/O error while traversing directories: {}", io::strerror(e))!;
  416. os::exit(1);
  417. };
  418. };