Skip to content

Replacing files inside the CArchive

extremecoders-re edited this page Dec 4, 2022 · 4 revisions

This is a quick and dirty way to replace files inside the CArchive. This can be used when the patched file is less than or equal in size to the original file. Note that this method can't be used to replace files in the PYZArchive. The latter requires more work.

Suppose we have this code which makes a HTTPS request.

# main.py
import requests

r = requests.get("https://example.org")
print(r.text)

This can be converted to a single file executable as

pyinstaller -F main.py

to produce main.exe in the dist folder.

Our objective is to sniff the HTTPS request from the compiled exe using Fiddler. We can force Python requests to use the Fiddler proxy by setting the HTTPS_PROXY environment variable as shown.

C:\> set HTTPS_PROXY=http://127.0.0.1:8888
C:\> main.exe
Traceback (most recent call last):
  File "urllib3\connectionpool.py", line 700, in urlopen
  File "urllib3\connectionpool.py", line 996, in _prepare_proxy
  File "urllib3\connection.py", line 414, in connect
  File "urllib3\util\ssl_.py", line 449, in ssl_wrap_socket
  File "urllib3\util\ssl_.py", line 493, in _ssl_wrap_socket_impl
  File "ssl.py", line 501, in wrap_socket
  File "ssl.py", line 1041, in _create
  File "ssl.py", line 1310, in do_handshake
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1129)

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "requests\adapters.py", line 489, in send
  File "urllib3\connectionpool.py", line 787, in urlopen
  File "urllib3\util\retry.py", line 592, in increment
urllib3.exceptions.MaxRetryError: HTTPSConnectionPool(host='example.org', port=443): Max retries exceeded with url: / (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1129)')))

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "main.py", line 3, in <module>
  File "requests\api.py", line 73, in get
  File "requests\api.py", line 59, in request
  File "requests\sessions.py", line 587, in request
  File "requests\sessions.py", line 701, in send
  File "requests\adapters.py", line 563, in send
requests.exceptions.SSLError: HTTPSConnectionPool(host='example.org', port=443): Max retries exceeded with url: / (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1129)')))
[5604] Failed to execute script 'main' due to unhandled exception!

However this won't work as Python requests doesn't trust the Fiddler certificate which the latter is using to MiTM.

Python requests uses the list of trusted certification authorities in cacert.pem from the certifi package. Thus we need to replace the original cacert.pem file in the exe with ours (exported from Fiddler).

FiddlerRoot_cert.pem

-----BEGIN CERTIFICATE-----
MIIDsjCCApqgAwIBAgIQTsFjChz7iYFH4uaBPMb1HjANBgkqhkiG9w0BAQsFADBn
MSswKQYDVQQLDCJDcmVhdGVkIGJ5IGh0dHA6Ly93d3cuZmlkZGxlcjIuY29tMRUw
EwYDVQQKDAxET19OT1RfVFJVU1QxITAfBgNVBAMMGERPX05PVF9UUlVTVF9GaWRk
bGVyUm9vdDAeFw0yMDA5MjMwMzEyMzBaFw0yMzEyMjMwMzEyMzBaMGcxKzApBgNV
BAsMIkNyZWF0ZWQgYnkgaHR0cDovL3d3dy5maWRkbGVyMi5jb20xFTATBgNVBAoM
DERPX05PVF9UUlVTVDEhMB8GA1UEAwwYRE9fTk9UX1RSVVNUX0ZpZGRsZXJSb290
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu6mWNIPjOTVEeLNZXJ8s
WcGu2qDCHB6PtbEUrb4vabvDtBJTYxD84sRs5wiTec6V7RF/rTtEAM98V06NNjKH
944MmM/IH4rxB5b7z16B97zwVibwFr246HYc7BwUhGF1/OU9OCkXiWMNE60YyytP
MwrjGXpCbnpkCl9gIXcsYbgoZ1vRikkIGgYf7VMN1OCf2JVZjn7tN50oEXvfeCdx
H7X0ia4NicurwHeW9KU0NFKkpSYJb+YlytrTN3BpaD1Jffq9Y0ABcIRTvIUABTQC
zICgR+DLwadne+eJifokarPNULl9EoRQambU50AtBKzWcxXq4Zf22fLUBES6Ni2N
bQIDAQABo1owWDATBgNVHSUEDDAKBggrBgEFBQcDATASBgNVHRMBAf8ECDAGAQH/
AgEAMB0GA1UdDgQWBBQxVKceU+V9F/EgztHA/nwTdqNREzAOBgNVHQ8BAf8EBAMC
AQYwDQYJKoZIhvcNAQELBQADggEBAGPcIoR841gVS8qbWNWKtANDFnRN7hIm9HAy
c4E5luFPJ2sqkLGBc5+LHF05xxcGsuyj4qn7dBFrM2KbXOAutgGYsKU2c2IK/vfa
mZ2ln95CGj4zvAkNaRqvblzw+NAc9Jy+ExQ+65wKrGobHqrVTqty2rrusnA++FvZ
UyXOs7AgnqRRMsKeaIMwr+f8+s0pY3zWPAUtz20I3Kewt8yuudABl33WW7f1kjz/
/dSzI2jx5pa3lY2EW7MGAmpdnHHVRovJqdja298UwZedAy9nspIOSsmseQ0IEtcX
GF3zrqPI7CcscDVeARl2vyIXsg2Dr4hKQkkAxKl+KtTJ7hnFpjk=
-----END CERTIFICATE-----

To do so follow the steps below,

First make a patch to the parseTOC method in pyinstxtractor such that it prints the required information about the file we want to replace.

        # Parse table of contents
        while parsedLen < self.tableOfContentsSize:
            (entrySize, ) = struct.unpack('!i', self.fPtr.read(4))
            nameLen = struct.calcsize('!iIIIBc')

            (entryPos, cmprsdDataSize, uncmprsdDataSize, cmprsFlag, typeCmprsData, name) = \
            struct.unpack( \
                '!IIIBc{0}s'.format(entrySize - nameLen), \
                self.fPtr.read(entrySize - 4))

            name = name.decode('utf-8').rstrip('\0')
            #  Patch
            if name.endswith("cacert.pem"):
                print(f"tocpos = {self.tableOfContentsPos+parsedLen}")
                print(f"filepos = {self.overlayPos + entryPos}")
                print(f"cmprsdDataSize = {cmprsdDataSize}")
                print(f"uncmprsdDataSize = {uncmprsdDataSize}")
                print(f"cmprsFlag = {cmprsFlag}")  

The if statement is the new addition which prints the required offsets.

Running the modified pyinstxtractor will output

C:\> python pyinstxtractor.py main.exe
[+] Processing main.exe
[+] Pyinstaller version: 2.1+
[+] Python version: 3.9
[+] Length of package: 6796532 bytes
tocpos = 7115804
filepos = 5874261
cmprsdDataSize = 155790
uncmprsdDataSize = 286370
cmprsFlag = 1
[+] Found 64 files in CArchive
[+] Beginning extraction...please standby
[+] Possible entry point: pyiboot01_bootstrap.pyc
[+] Possible entry point: pyi_rth_subprocess.pyc
[+] Possible entry point: main.pyc
[+] Found 163 files in PYZ archive
[+] Successfully extracted pyinstaller archive: dist\main.exe

Now copy the offsets to repack.py below.

#repack.py

import os
import zlib
import shutil
import struct

# Offsets for cacert.pem 
tocpos = 7115804
filepos = 5874261
cmprsdDataSize = 155790
uncmprsdDataSize = 286370
cmprsFlag = 1

# Make a copy of the exe
shutil.copy("main.exe", "main_patched.exe")

exe = open("main_patched.exe", "r+b")

# The patched file
patched_file = open("FiddlerRoot_cert.pem", "rb").read()

# zlib compress
compressed_patched_file = zlib.compress(patched_file, 9)

if len(compressed_patched_file) > cmprsdDataSize:
    raise Exception("Not supported, compressed size of new file is larger")

# 4 bytes for entrySize, 4 bytes for filepos
exe.seek(tocpos + 4 + 4, os.SEEK_SET) 

# Length of compressed data
exe.write(struct.pack("!I", len(compressed_patched_file)))

# Length of uncompressed data
exe.write(struct.pack("!I", len(patched_file)))

# Compression flag (unchanged)
exe.write(struct.pack("!B", cmprsFlag))

# Write the contents of the patched file
exe.seek(filepos, os.SEEK_SET)
exe.write(compressed_patched_file)

exe.close()

Running repack.py we get main_patched.exe which on running doesn't throw CERTIFICATE_VERIFY_FAILED error.

C:\> set HTTPS_PROXY=http://127.0.0.1:8888
C:\> main_patched.exe
<!doctype html>
<html>
<head>
    <title>Example Domain</title>

    <meta charset="utf-8" />
    <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <style type="text/css">
    body {
        background-color: #f0f0f2;
        margin: 0;
        padding: 0;
        font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;

    }
    div {
        width: 600px;
        margin: 5em auto;
        padding: 2em;
        background-color: #fdfdff;
        border-radius: 0.5em;
        box-shadow: 2px 3px 7px 2px rgba(0,0,0,0.02);
    }
    a:link, a:visited {
        color: #38488f;
        text-decoration: none;
    }
    @media (max-width: 700px) {
        div {
            margin: 0 auto;
            width: auto;
        }
    }
    </style>
</head>

<body>
<div>
    <h1>Example Domain</h1>
    <p>This domain is for use in illustrative examples in documents. You may use this
    domain in literature without prior coordination or asking for permission.</p>
    <p><a href="https://www.iana.org/domains/example">More information...</a></p>
</div>
</body>
</html>

Back in Fiddler, we can notice that it was able to sniff the HTTPS request.

image

Clone this wiki locally