Coder Social home page Coder Social logo

trailofbits / uthenticode Goto Github PK

View Code? Open in Web Editor NEW
136.0 34.0 33.0 590 KB

A cross-platform library for verifying Authenticode signatures

Home Page: https://trailofbits.github.io/uthenticode/

License: MIT License

CMake 9.26% C++ 86.62% Makefile 1.46% Shell 1.40% Dockerfile 1.26%
authenticode code-signing hacktoberfest cpp cryptography

uthenticode's Issues

Wrong hash calculated for some PE files

I've found that some files - especially installers - fail to verify because the calculated hash doesn't match.

Here's the latest Firefox installer, for example:
https://ftp.mozilla.org/pub/firefox/releases/101.0/win64/en-US/Firefox%20Setup%20101.0.exe

Its embedded SHA-256 hash is DEF2F92C51E7DD222F480B9459C1E2E0A35C598453FC85EF74D9439D105AC853, but uthenticode::calculate_checksum returns 9338FBD1F37B5544CD759681C23A8343CBE4043CDB3A89A1818C909C81AFC6AC.

The problem is that pe_bits only contains 134,644 bytes at the time it's hashed, a tiny fraction of the EXE's total 56,014,584 bytes. It seems that enumerating the PE sections is not a reliable way to collect the data we need to hash, regardless of what Microsoft's documentation says.

The simpler approach of just hashing the file in order, skipping a few pieces, gives the correct result. For example, PeNet (Apache licensed) has an implementation like this:
https://github.com/secana/PeNet/blob/ec0bb60d62c9d44dc7d5648ac742f96de3a0a166/src/PeNet/Header/Authenticode/AuthenticodeInfo.cs#L155

If you want, I'd be happy to submit a PR along those lines, since it's work I need to do anyway.

(I'd also like to get uthenticode working with .msi and .cat files, but that's a separate issue)

Zeros embedded in subject and issuer fields when using certificates generated with makecert (e.g. self-signed for test-signing during driver development)

It appears as if makecert writes Windows-style wchar_t* into these fields verbatim instead of encoding them as, say, UTF-8. This is an issue as it will lead to the strings being interpreted as single-character string, given every other character is a zero byte.

I worked around this locally by creating a function that filters these:

static inline std::string filter_escaped_zero_bytes(char const* instr)
{
    auto input = std::string(instr);
    static std::string const needle = "\\x00";
    std::ostringstream os;
    std::size_t currpos = 0, oldpos{};
    do
    {
        oldpos = currpos;
        currpos = input.find(needle, currpos);
        if (std::string::npos == currpos)
        {
            break;
        }
        os << input.substr(oldpos, currpos - oldpos);
        currpos += needle.size();
    } while (true);
    os << input.substr(oldpos); // remainder
    return os.str();
}

... and using that in the ctor (Certificate::Certificate):

subject_ = filter_escaped_zero_bytes(subject.get());
issuer_ = filter_escaped_zero_bytes(issuer.get());

Not sure this is an issue you'll want to address at all, since it appears that MS and its makecert tool deviate from the standard here. Just wanted to bring this to your attention as well as to the attention of those watching the project.

Feel free to close this pretty much immediately.

PS: this isn't particularly optimized, because I only ever encountered this with those certificates created by makecert and we only use these in testing scenarios. But I'm sure there is still some room for improvement.

Exception in read_certs when length < offset

I found a (non-malicious) EXE which happens to have an unusual or corrupt certificate table (it starts with a long series of zeros), which causes an exception to be thrown from the std::vector constructor in uthenticode::read_certs.

It's WaitHelpWindow.exe, part of Samsung Magician:
https://www.virustotal.com/gui/file/ffed326a77352d65fba88a4f0edd55f7ec8afdddcef84d16484118dfca7d6bac/details

https://semiconductor.samsung.com/resources/software-resources/Samsung_Magician_Installer_Official_7.1.1.820.zip

I uploaded the individual file here:
https://www.dropbox.com/s/9nw352jkye1pa2a/WaitHelpWindow.exe

It looks like maybe there are some embedded certificates, but no signature.

I'll submit a simple PR to avoid the exception.

Not sure about this one, but it could be a logic error

Inside uthenticode::read_certs() you add to offset while traversing WIN_CERTIFICATE structures which may exist 8-byte-aligned in the PE security directory. You then enter a while loop and add to it based on the WIN_CERTIFICATE members.

Overall in one loop iteration you do:

offset += sizeof(dwLength) + sizeof(wRevision) + sizeof(wCertificateType) + round(dwLength, 8);

I think with the last one you overshoot (well, technically you'd simply land inside the subsequent WIN_CERTIFICATE::bCertificate; but that would likely lead to all sorts of undesired effects).

The reason being that sizeof(bCertificate) is dwLength - 8 (i.e. sizeof(bCertificate) == dwLength - offsetof(WIN_CERTIFICATE, bCertificate)).

Or in other words: to find the next WIN_CERTIFICATE you have to take the original offset from when you entered the loop, add round(dwLength, 8) to that and then look at the next one.

This particular case doesn't seem to be covered by your test suite (for a lack of samples, I reckon?), which would explain why this went unnoticed. However, there's a good chance I simply have a wrinkle in my brain and am just blabbering some garbage 😉

Automatic releases

We should add a GitHub Actions workflow that pushes a new release on tags that match v*, and uploads build products for each supported platform.

See pegoat for an example of this.

Clean up release assets

Instead of just archiving each build's build directory and attaching these to the release, we should run the install target and archive the install tree.

Not all used headers are explicitly included

Hi,

wanted to bring another point to your attention.

I think you are technically missing an #include <stdexcept> in uthenticode.h between <optional> and <vector>. This doesn't cause an issue because evidently <optional> transiently includes <stdexcept>.

Furthermore at least for me an #include <openssl/bn.h> was missing as well in that same header. Without it I get an error for using BN_ptr = std::unique_ptr<BIGNUM, decltype(&BN_free)>;. This may be owed to the fact that I have adjusted the code in various places and bring some stuff into scope before including uthenticode and hide other stuff from it. I also don't make use of your #include <pe-parse/parse.h> ... it may play into that as well.

Different result from Signtool

nc.exe from int0x33/nc.exe is signed and it is verified by signtool.

>signtool verify /pa nc.exe
File: nc.exe
Index  Algorithm  Timestamp
========================================
0      sha1       Authenticode

Successfully verified: nc.exe

But it is not verified by uthenticode.

>svcli nc.exe
This PE is NOT verified!

nc.exe has 1 certificate entries

Calculated checksums:
   MD5: 93013015944D906D98AC97C32274D8E7
  SHA1: 612E98A6DABA999F46EE8CE82176E10B77B60C87
SHA256: F2CB0E58C668B2C289D7CBAD47030E3FB82126180270EE99E69076AFDE997F8E

SignedData entry:
        Embedded checksum: 612E98A6DABA999F46EE8CE82176E10B77B60C87

        Signers:
                Subject: /C=SI/CN=Jernej Simoncic
                Issuer: /C=BE/O=GlobalSign nv-sa/OU=ObjectSign CA/CN=GlobalSign ObjectSign CA
                Serial: 010000000001307A27872D

        Certificates:
                Subject: /C=BE/O=GlobalSign nv-sa/OU=Primary Object Publishing CA/CN=GlobalSign Primary Object Publishing CA
                Issuer: /C=BE/O=GlobalSign nv-sa/OU=Root CA/CN=GlobalSign Root CA
                Serial: 040000000001239E0FACB3

                Subject: /OU=Timestamping CA/O=GlobalSign/CN=GlobalSign Timestamping CA
                Issuer: /C=BE/O=GlobalSign nv-sa/OU=Root CA/CN=GlobalSign Root CA
                Serial: 0400000000012019C19066

                Subject: /C=BE/O=GlobalSign NV/CN=GlobalSign Time Stamping Authority
                Issuer: /OU=Timestamping CA/O=GlobalSign/CN=GlobalSign Timestamping CA
                Serial: 01000000000125B0B4CC01

                Subject: /C=SI/CN=Jernej Simoncic
                Issuer: /C=BE/O=GlobalSign nv-sa/OU=ObjectSign CA/CN=GlobalSign ObjectSign CA
                Serial: 010000000001307A27872D

                Subject: /C=BE/O=GlobalSign nv-sa/OU=ObjectSign CA/CN=GlobalSign ObjectSign CA
                Issuer: /C=BE/O=GlobalSign nv-sa/OU=Primary Object Publishing CA/CN=GlobalSign Primary Object Publishing CA
                Serial: 040000000001239E0FAF24

        This SignedData is invalid!

Some other software has the same problem, while some of them are normal. I understand there are caveats that uthenticode may behave differently to Wintrust API because of not accessing Trusted Publishers store which causes the uthenticode-verified software can't run on some Windows environments. But this situation is that the signature can't be cryptographically verified, which I have no idea why it would happen.

The following are the versions of the dependencies I used:

  • openssl 3.3.1
  • pe-parse 2.1.1
  • uthenticode 2.0.1

Supports Windows XP

I use this project in Windows XP SP3 and it makes an exception and outputs the error message "bcrypt.dll not found". After some surveys, it turned out that XP doesn't have bcrypt.dll in the system by default. But even if I copy a bcrypt.dll to the same directory, it still has the error message "InitializeSRWLock not found in Kernel32.dll". It seems there are some dependencies in this project that XP doesn't have.

Is there any solution or workaround for XP to use this project?

Add an API for full-chain verification

We'll never support verification against the trusted publishers store, but we could support verification against a particular user-supplied certificate. That way, users could at least do full-chain verification of binaries that they control.

Win32 builds

smolverify should build and test correctly on Win32 targets (i.e., 32-bit Windows).

Additionnal signatures not properly parsed

When using uthenticode to parse the signatures of a PE that contains two signatures with the same certificate and different digest algorithms only the first signature is processed. PE executable signed using both SHA1 and SHA256 digest algorithms are quite common.

To reproduce the issue, with any PE with two signatures, using the svcli example and comparing with osslsigncode output:
svcli ./PE.exe # only one signature is processed (the second -nested- signature is not)
osslsigncode verify ./PE.exe # the two signatures are processed

Parsing nested signatures requires parsing OID 1.3.6.1.4.1.311.2.4.1 (Ms-SpcNestedSignature). See for example https://github.com/develar/osslsigncode/blob/master/osslsigncode.c#L1220

(uthenticode build from rev. 8b87574)

Windows builds

smolverify should build without problems on Windows, but I haven't tried it yet.

We should enable Windows builds in the CI, similarly to the current Linux and macOS builds.

Small optimization for tohex

Hi, I think you can do without a std::stringstream for a simple function like tohex() and still have a robust (RAII etc.) version.

Here's an alternative implementation in the context of a small test program:

#include <string>

static inline std::string tohex(std::uint8_t const* buf, std::size_t len)
{
    constexpr static char lookup_table[0x10] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};

    if (!buf || !len)
    {
        return {};
    }

    std::string hexstr;
    hexstr.reserve(len * 2); // each byte creates two hex digits

    for (auto i = 0; i < len; i++)
    {
        hexstr += lookup_table[buf[i] >> 4];
        hexstr += lookup_table[buf[i] & 0xF];
    }

    return hexstr;
}

#include <iostream>

int main()
{
    constexpr std::uint8_t testbuf[] = { 0xBA, 0xDB, 0xAD, 0xBE, 0xEF, 0xCA, 0xCE, 0xFE, 0xE1, 0xDE, 0xAD };

    std::cout << tohex(testbuf, sizeof(testbuf)) << std::endl;

    return 0;
}

Please note that the main reason for including iostream late is to show that the only required header for the code above is string.

I hope I didn't miss anything, but I attempted to keep the properties of the old function. Previously you also required enough space to keep the full buffer, so this should be largely the same.

Obviously you could also create two lookup tables, one lowercase, one uppercase and alternate between them base on a bool argument, for example (if you prefer that flexibility). Personally I am used to viewing hex in all uppercase.

If you feel like the above (with or without the discussed alterations) is worth including in uthenticode, please respond in a comment and I'll send a PR.


I am attaching a small benchmark merely for the performance aspect. But I think the above method is both simpler to write (consider the iomanip stuff in the original code) and to read and faster as well.

Benchmark: tohex_bench.zip or clone from here

Output:

$ hyperfine ./old_tohex; hyperfine ./new_tohex
Benchmark 1: ./old_tohex
  Time (mean ± σ):      6.061 s ±  0.137 s    [User: 4.812 s, System: 1.248 s]
  Range (min … max):    5.885 s …  6.276 s    10 runs

Benchmark 1: ./new_tohex
  Time (mean ± σ):      1.672 s ±  0.009 s    [User: 0.672 s, System: 1.000 s]
  Range (min … max):    1.658 s …  1.690 s    10 runs

hyperfine can be installed with cargo install hyperfine if you have Rust installed. Otherwise use whatever other tool you normally use for the purpose.

We need no warmup, because we read from /dev/urandom and throw it away into /dev/null after converting with either of the tohex() variants.


As far as I am concerned the optimized variant comes under the Unlicense, i.e. it is placed into the public domain plus the disclaimer of the MIT license. So if you don't insist on a PR, simply copy it over at your discretion if you find it worthy of inclusion.

Unable to build from source using README.md instructions

This might be a noob question but I am unable to build from source using the instructions. I am not very familiar with cmake or vcpkg so maybe this is the issue (it has been a while since i fought with c++).

I installed vcpkg from source and built it fine, then it seems to install pe-parse correctly, but when trying to build uthenticode it cant find it and i cant figure out how to tell it where it is installed.

mic@devbox:~/projects/uthenticode$ ~/projects/vcpkg/vcpkg install pe-parse
Computing installation plan...
The following packages are already installed:
    pe-parse[core]:x64-linux -> 1.2.0
Package pe-parse:x64-linux is already installed

Total elapsed time: 47.66 us

The package pe-parse:x64-linux provides CMake targets:

    find_package(pe-parse CONFIG REQUIRED)
    target_link_libraries(main PRIVATE pe-parse::pe-parser-library)

mic@devbox:~/projects/uthenticode$ mkdir build && cd build
mic@devbox:~/projects/uthenticode/build$ cmake ..
-- The C compiler identification is GNU 10.2.0
-- The CXX compiler identification is GNU 10.2.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
CMake Error at src/CMakeLists.txt:5 (find_package):
  By not providing "Findpe-parse.cmake" in CMAKE_MODULE_PATH this project has
  asked CMake to find a package configuration file provided by "pe-parse",
  but CMake did not find one.

  Could not find a package configuration file provided by "pe-parse" with any
  of the following names:

    pe-parseConfig.cmake
    pe-parse-config.cmake

  Add the installation prefix of "pe-parse" to CMAKE_PREFIX_PATH or set
  "pe-parse_DIR" to a directory containing one of the above files.  If
  "pe-parse" provides a separate development package or SDK, be sure it has
  been installed.


-- Configuring incomplete, errors occurred!
See also "/home/mic/projects/uthenticode/build/CMakeFiles/CMakeOutput.log".

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.