trailofbits / uthenticode

A cross-platform library for verifying Authenticode signatures
https://trailofbits.github.io/uthenticode/
MIT License
133 stars 33 forks source link

svcli: Support extracting the SignedData blobs from a PE #23

Open woodruffw opened 4 years ago

woodruffw commented 4 years ago

svcli should learn an -x, --extract flag or similar to dump all of the SignedData blobs it encounters. This would make it easier to use openssl asn1parse and other command-line tools to debug uthenticode.

hugmyndakassi commented 2 years ago

Could you be a little clearer what it is you want?! What I am wondering is whether you want one of the WIN_CERTIFICATE (incl. the dynamically sized buffer) or something else altogether? Or do you want just the buffer, but including the enclosing "sequence" and minus the padding at the end of WIN_CERTIFICATE?

woodruffw commented 2 years ago

Just the buffer: the idea would be to dump the raw ASN.1 blob, so that other tools could parse it on their own.

hugmyndakassi commented 6 days ago

There's one thing here. There can reasonably be more than a single WinCert (after all µthenticode treats it as std::vector<WinCert>). What to do if that's the case?

The assumption of a single such structure makes it easy but incomplete. Yet, it'd be better than nothing. Any suggestions?

hugmyndakassi commented 6 days ago

Does this look about like what you had in mind?

diff --git a/Makefile b/GNUmakefile
similarity index 100%
rename from Makefile
rename to GNUmakefile
diff --git a/src/include/uthenticode.h b/src/include/uthenticode.h
index 223d662..f742c21 100644
--- a/src/include/uthenticode.h
+++ b/src/include/uthenticode.h
@@ -208,14 +208,19 @@ class SignedData {

   /**
    * @return a new SignedData for any nested signature within this SignedData,
    * or `std::nullopt` if this SignedData has no nested signature.
    */
   std::optional<SignedData> get_nested_signed_data() const;

+  /**
+   * @return a const-reference to the certificate buffer.
+   */
+  std::vector<std::uint8_t> const &get_raw_data() const;
+
  private:
   impl::Authenticode_SpcIndirectDataContent *get_indirect_data() const;

   std::vector<std::uint8_t> cert_buf_;
   PKCS7 *p7_{nullptr};
   impl::Authenticode_SpcIndirectDataContent *indirect_data_{nullptr};
 };
diff --git a/src/svcli/svcli.cpp b/src/svcli/svcli.cpp
index b0be453..91c97aa 100644
--- a/src/svcli/svcli.cpp
+++ b/src/svcli/svcli.cpp
@@ -5,53 +5,93 @@
  *
  * Usage: `svcli <exe>`
  */

 #include <uthenticode.h>

 #include <array>
+#include <fstream>
 #include <iomanip>
 #include <iostream>
+#include <memory>

 #include "vendor/argh.h"

+#if __has_include(<unistd.h>)
+#include <unistd.h>
+#else
+#include <fcntl.h>
+#include <io.h>
+#endif
+
+static bool is_cout_a_pipe() {
+#if __has_include(<unistd.h>)
+  return !isatty(STDOUT_FILENO);
+#else
+  return !_isatty(_fileno(stdout));
+#endif
+}
+
 using checksum_kind = uthenticode::checksum_kind;

-int main(int argc, char const *argv[]) {
-  argh::parser cmdl(argv);
-
-  if (cmdl[{"-v", "--version"}]) {
-    std::cout << "svcli (uthenticode) version " << UTHENTICODE_VERSION << '\n';
-    return 0;
-  } else if (cmdl[{"-h", "--help"}] || argc != 2) {
-    std::cout << "Usage: svcli [options] <file>\n\n"
-              << "Options:\n"
-              << "\t-v, --version\tPrint the version and exit\n"
-              << "\t-h, --help\tPrint this help message and exit\n\n"
-              << "Arguments:\n"
-              << "\t<file>\tThe PE to parse for Authenticode data\n";
-    return 0;
-  }
-
-  auto *pe = peparse::ParsePEFromFile(cmdl[1].c_str());
+static int realmain(argh::parser &cmdl, bool extract) {
+  auto const input_file = cmdl[1];
+  auto *pe = peparse::ParsePEFromFile(input_file.c_str());
   if (pe == nullptr) {
-    std::cerr << "pe-parse failure: " << cmdl[1] << ": " << peparse::GetPEErrString() << '\n';
+    std::cerr << "pe-parse failure: " << input_file << ": " << peparse::GetPEErrString() << '\n';
     return 1;
   }

-  std::cout << "This PE is " << (uthenticode::verify(pe) ? "" : "NOT ") << "verified!\n\n";
+  if (!extract) {
+    std::cout << "This PE is " << (uthenticode::verify(pe) ? "" : "NOT ") << "verified!\n\n";
+  }

   const auto &certs = uthenticode::read_certs(pe);

   if (certs.empty()) {
     std::cerr << "PE has no certificate data!\n";
     return 1;
   }

-  std::cout << cmdl[1] << " has " << certs.size() << " certificate entries\n\n";
+  if (extract) {
+    std::string fname;
+    if (cmdl.size() > 1) {
+      fname = cmdl[2];
+    }
+    if (!is_cout_a_pipe() && fname.empty()) {
+      std::cerr
+          << "Cowardly refusing to write binary data to TTY. Give '-' explicitly to force it.\n";
+      return 1;
+    }
+    const bool want_stdout = fname.empty() || fname == "-";
+    std::ofstream outfile;
+    std::ostream *output;
+    if (!want_stdout) {
+      outfile.open(fname, std::ios::binary | std::ios::out);
+      if (!outfile.is_open()) {
+        std::cerr << "Failed to open '" << fname << "'.\n";
+        return 1;
+      }
+      output = &outfile;
+    } else {
+      output = &std::cout;
+    }
+    for (const auto &cert : certs) {
+      auto signed_data = cert.as_signed_data();
+      if (!signed_data) {
+        continue;
+      }
+      // dump first (valid) WinCert buffer
+      auto const &raw_data = signed_data->get_raw_data();
+      output->write(reinterpret_cast<const char *>(raw_data.data()), raw_data.size());
+      return 0;
+    }
+  }
+
+  std::cout << input_file << " has " << certs.size() << " certificate entries\n\n";

   std::cout << "Calculated checksums:\n";
   std::array<checksum_kind, 3> kinds = {
       checksum_kind::MD5, checksum_kind::SHA1, checksum_kind::SHA256};
   for (const auto &kind : kinds) {
     auto cksum = uthenticode::calculate_checksum(pe, kind);
     if (cksum.has_value()) {
@@ -118,8 +158,33 @@ int main(int argc, char const *argv[]) {
     }

     std::cout << "\tThis SignedData is "
               << (nested_signed_data->verify_signature() ? "valid" : "invalid") << "!\n";
   }

   peparse::DestructParsedPE(pe);
+  return 0;
+}
+
+int main(int argc, char const *argv[]) {
+  argh::parser cmdl(argv);
+  bool extract = false;
+
+  if (cmdl[{"-v", "--version"}]) {
+    std::cout << "svcli (uthenticode) version " << UTHENTICODE_VERSION << '\n';
+    return 0;
+  } else if (cmdl[{"-x", "--extract"}]) {
+    extract = true;
+  } else if (cmdl[{"-h", "--help"}] || argc != 2) {
+    std::cout << "Usage: svcli [options] <file>\n\n"
+              << "Options:\n"
+              << "\t-v, --version\tPrint the version and exit\n"
+              << "\t-x, --extract\tExtract the first certificate blob\n"
+              << "\t-h, --help\tPrint this help message and exit\n\n"
+              << "Arguments:\n"
+              << "\t<input-file>\tThe PE to parse for Authenticode data\n"
+              << "\t[output-file]\tWith -x/--extract the file to dump the buffer into (leave empty "
+                 "or use - for stdout)\n";
+    return 0;
+  }
+  return realmain(cmdl, extract);
 }
diff --git a/src/uthenticode.cpp b/src/uthenticode.cpp
index 4827ad5..9aa0158 100644
--- a/src/uthenticode.cpp
+++ b/src/uthenticode.cpp
@@ -358,14 +358,18 @@ std::optional<SignedData> SignedData::get_nested_signed_data() const {
   auto *nested_signed_data_seq = nested_signed_data->value.sequence;
   std::vector<std::uint8_t> cert_buf(nested_signed_data_seq->data,
                                      nested_signed_data_seq->data + nested_signed_data_seq->length);

   return std::make_optional<SignedData>(cert_buf);
 }

+std::vector<std::uint8_t> const &SignedData::get_raw_data() const {
+  return cert_buf_;
+}
+
 impl::Authenticode_SpcIndirectDataContent *SignedData::get_indirect_data() const {
   auto *contents = p7_->d.sign->contents;
   if (contents == nullptr) {
     return nullptr;
   }

   /* We're expecting a sequence whose type is SPC_INDIRECT_DATA_OID.

PS: I ran it through clang-format. PPS: I reckon realmain() could be split up further if desired.

hugmyndakassi commented 6 days ago

Probably also worth to mention. So -x simply changes one aspect: now svcli wants one more positional argument -> the output file name.

It's optional, however. If the output is redirected to a pipe you needn't give anything, if it's not the tool will complain unless you pass - as the output file name to denote stdout.

hugmyndakassi commented 6 days ago

... or maybe you'd rather give SignedData a method that takes a stream to write the blob into?