├── .gitignore ├── LICENSE ├── README.md ├── common.go ├── go.mod ├── go.sum ├── main.go ├── main_gopherjs.go ├── marshal ├── marshal_types.go ├── objecttype.go ├── pyintegerobject.go ├── pylistobject.go ├── pyobject.go ├── pystringobject.go └── unmarshal.go └── public ├── css └── style.css ├── index.html └── js ├── main.js └── worker.js /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | *.exe 3 | public/js/pyinstxtractor-go.js 4 | public/js/pyinstxtractor-go.js.map -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 PyInstaller Extractor Org 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pyinstxtractor-Go🌐 2 | 3 | PyInstaller Extractor developed in Golang. 4 | 5 | Runs both on desktop and on the web. The browser version is powered by GopherJS to compile Go to Javascript. 6 | 7 | Site hosted on Netlify. 8 | 9 | Try it out at https://pyinstxtractor-web.netlify.app/ 10 | 11 | [![Netlify Status](https://api.netlify.com/api/v1/badges/63aa28b4-8134-44d9-a934-7e2833b79557/deploy-status)](https://app.netlify.com/sites/pyinstxtractor-web/deploys) 12 | 13 | ## Known Limitations 14 | 15 | - The tool (both desktop & web) works best with Python 3.x based PyInstaller executables. Python 2.x based executables are still supported but the PYZ archive won't be extracted. 16 | 17 | - Extracting large files using the web version may crash the browser specifically on mobile devices with low RAM. 18 | 19 | - Encrypted pyz archives are not supported at present. 20 | 21 | ## See also 22 | 23 | - [pyinstxtractor](https://github.com/extremecoders-re/pyinstxtractor): The original tool developed in Python. 24 | - [pyinstxtractor-ng](https://github.com/pyinstxtractor/pyinstxtractor-ng): Same as pyinsxtractor but this doesn't require Python to run and can extract all supported pyinstaller versions. 25 | 26 | 27 | ## Compiling for Deskop 28 | 29 | ``` 30 | go build 31 | ``` 32 | 33 | ## Compiling for Web 34 | 35 | GopherJS requires Go 1.18.x. For more details check https://github.com/gopherjs/gopherjs#installation-and-usage 36 | 37 | ``` 38 | go install github.com/gopherjs/gopherjs@v1.18.0-beta1 39 | 40 | gopherjs build --minify --tags=gopherjs -o public/js/pyinstxtractor-go.js 41 | ``` 42 | -------------------------------------------------------------------------------- /common.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "compress/zlib" 6 | "io" 7 | "math/rand" 8 | ) 9 | 10 | const ( 11 | PYINST20_COOKIE_SIZE = 24 // For pyinstaller 2.0 12 | PYINST21_COOKIE_SIZE = 24 + 64 // For pyinstaller 2.1+ 13 | ) 14 | 15 | var PYINST_MAGIC [8]byte = [8]byte{'M', 'E', 'I', 014, 013, 012, 013, 016} // Magic number which identifies pyinstaller 16 | 17 | type PyInst20Cookie struct { 18 | Magic []byte `struct:"[8]byte"` 19 | LengthOfPackage int `struct:"int32,big"` 20 | Toc int `struct:"int32,big"` 21 | TocLen int `struct:"int32,big"` 22 | PythonVersion int `struct:"int32,big"` 23 | } 24 | 25 | type PyInst21Cookie struct { 26 | Magic []byte `struct:"[8]byte"` 27 | LengthOfPackage uint `struct:"uint32,big"` 28 | Toc uint `struct:"uint32,big"` 29 | TocLen int `struct:"int32,big"` 30 | PythonVersion int `struct:"int32,big"` 31 | PythonLibName []byte `struct:"[64]byte"` 32 | } 33 | 34 | type CTOCEntry struct { 35 | EntrySize int `struct:"int32,big"` 36 | EntryPosition uint `struct:"uint32,big"` 37 | DataSize uint `struct:"uint32,big"` 38 | UncompressedDataSize uint `struct:"uint32,big"` 39 | ComressionFlag int8 `struct:"int8"` 40 | TypeCompressedData byte `struct:"byte"` 41 | Name string 42 | } 43 | 44 | func zlibDecompress(in []byte) (out []byte, err error) { 45 | var zr io.ReadCloser 46 | zr, err = zlib.NewReader(bytes.NewReader(in)) 47 | if err != nil { 48 | return 49 | } 50 | out, err = io.ReadAll(zr) 51 | return 52 | } 53 | 54 | func randomString() string { 55 | const CHARSET = "0123456789abcdef" 56 | var randomBytes []byte = make([]byte, 16) 57 | 58 | for i := 0; i < 16; i++ { 59 | randomBytes = append(randomBytes, CHARSET[rand.Intn(len(CHARSET))]) 60 | } 61 | return string(randomBytes) 62 | } 63 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module pyinstxtractor-go 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/go-restruct/restruct v1.2.0-alpha 7 | github.com/gopherjs/gopherjs v1.18.0-beta1 8 | ) 9 | 10 | require github.com/pkg/errors v0.8.1 // indirect 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/go-restruct/restruct v1.2.0-alpha h1:2Lp474S/9660+SJjpVxoKuWX09JsXHSrdV7Nv3/gkvc= 4 | github.com/go-restruct/restruct v1.2.0-alpha/go.mod h1:KqrpKpn4M8OLznErihXTGLlsXFGeLxHUrLRRI/1YjGk= 5 | github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= 6 | github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= 7 | github.com/gopherjs/gopherjs v1.18.0-beta1 h1:IbykhVEq4SAjwyBRuNHl0aOO6w6IqgL3RUdMhoBo4mY= 8 | github.com/gopherjs/gopherjs v1.18.0-beta1/go.mod h1:6UY8PXRnu51MqjYCCY4toG0S5GeH5uVJ3qDxIsa+kqo= 9 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 10 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 11 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 12 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 13 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 14 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 15 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 16 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 17 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 18 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 19 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // +build !gopherjs 2 | 3 | package main 4 | 5 | import ( 6 | "io" 7 | "os" 8 | "fmt" 9 | "bytes" 10 | "strings" 11 | "path/filepath" 12 | "encoding/binary" 13 | 14 | "pyinstxtractor-go/marshal" 15 | "github.com/go-restruct/restruct" 16 | // "github.com/k0kubun/pp/v3" 17 | ) 18 | 19 | type PyInstArchive struct { 20 | inFilePath string 21 | fPtr io.ReadSeekCloser 22 | fileSize int64 23 | cookiePosition int64 24 | pyInstVersion int64 25 | pythonMajorVersion int 26 | pythonMinorVersion int 27 | overlaySize int64 28 | overlayPosition int64 29 | tableOfContentsSize int64 30 | tableOfContentsPosition int64 31 | tableOfContents []CTOCEntry 32 | pycMagic [4]byte 33 | gotPycMagic bool 34 | barePycsList []string 35 | } 36 | 37 | func (p *PyInstArchive) Open() bool { 38 | var err error 39 | if p.fPtr, err = os.Open(p.inFilePath); err != nil { 40 | fmt.Printf("[!] Couldn't open %s\n", p.inFilePath) 41 | return false 42 | } 43 | var fileInfo os.FileInfo 44 | if fileInfo, err = os.Stat(p.inFilePath); err != nil { 45 | fmt.Printf("[!] Couldn't get size of file %s\n", p.inFilePath) 46 | return false 47 | } 48 | p.fileSize = fileInfo.Size() 49 | return true 50 | } 51 | 52 | func (p *PyInstArchive) Close() { 53 | p.fPtr.Close() 54 | } 55 | 56 | func (p *PyInstArchive) CheckFile() bool { 57 | fmt.Printf("[+] Processing %s\n", p.inFilePath) 58 | 59 | var searchChunkSize int64 = 8192 60 | endPosition := p.fileSize 61 | p.cookiePosition = -1 62 | 63 | if endPosition < int64(len(PYINST_MAGIC)) { 64 | fmt.Println("[!] Error : File is too short or truncated") 65 | return false 66 | } 67 | 68 | var startPosition, chunkSize int64 69 | for { 70 | if endPosition >= searchChunkSize { 71 | startPosition = endPosition - searchChunkSize 72 | } else { 73 | startPosition = 0 74 | } 75 | chunkSize = endPosition - startPosition 76 | if chunkSize < int64(len(PYINST_MAGIC)) { 77 | break 78 | } 79 | 80 | if _, err := p.fPtr.Seek(startPosition, io.SeekStart); err != nil { 81 | fmt.Println("[!] File seek failed") 82 | return false 83 | } 84 | var data []byte = make([]byte, searchChunkSize) 85 | p.fPtr.Read(data) 86 | 87 | if offs := bytes.Index(data, PYINST_MAGIC[:]); offs != -1 { 88 | p.cookiePosition = startPosition + int64(offs) 89 | break 90 | } 91 | endPosition = startPosition + int64(len(PYINST_MAGIC)) - 1 92 | 93 | if startPosition == 0 { 94 | break 95 | } 96 | } 97 | if p.cookiePosition == -1 { 98 | fmt.Println("[!] Error : Missing cookie, unsupported pyinstaller version or not a pyinstaller archive") 99 | return false 100 | } 101 | p.fPtr.Seek(p.cookiePosition + PYINST20_COOKIE_SIZE, io.SeekStart) 102 | 103 | var cookie []byte = make([]byte, 64) 104 | if _, err := p.fPtr.Read(cookie); err != nil { 105 | fmt.Println("[!] Failed to read cookie!") 106 | return false 107 | } 108 | 109 | cookie = bytes.ToLower(cookie) 110 | if bytes.Contains(cookie, []byte("python")) { 111 | p.pyInstVersion = 21 112 | fmt.Println("[+] Pyinstaller version: 2.1+") 113 | } else { 114 | p.pyInstVersion = 20 115 | fmt.Println("[+] Pyinstaller version: 2.0") 116 | } 117 | return true 118 | } 119 | 120 | func (p *PyInstArchive) GetCArchiveInfo() bool { 121 | failFunc := func() bool { 122 | fmt.Println("[!] Error : The file is not a pyinstaller archive") 123 | return false 124 | } 125 | 126 | getPyMajMinVersion := func(version int) (int, int) { 127 | if version >= 100 { 128 | return version / 100, version % 100 129 | } 130 | return version / 10, version % 10 131 | } 132 | 133 | printPythonVerLenPkg := func(pyMajVer, pyMinVer int, lenPkg uint) { 134 | fmt.Printf("[+] Python version: %d.%d\n", pyMajVer, pyMinVer) 135 | fmt.Printf("[+] Length of package: %d bytes\n", lenPkg) 136 | } 137 | 138 | calculateTocPosition := func(cookieSize int, lengthOfPackage, toc uint, tocLen int) { 139 | // Additional data after the cookie 140 | tailBytes := p.fileSize - p.cookiePosition - int64(cookieSize) 141 | 142 | // Overlay is the data appended at the end of the PE 143 | p.overlaySize = int64(lengthOfPackage) + tailBytes 144 | p.overlayPosition = p.fileSize - p.overlaySize 145 | p.tableOfContentsPosition = p.overlayPosition + int64(toc) 146 | p.tableOfContentsSize = int64(tocLen) 147 | } 148 | 149 | if _, err := p.fPtr.Seek(p.cookiePosition, io.SeekStart); err != nil { 150 | return failFunc() 151 | } 152 | 153 | if p.pyInstVersion == 20 { 154 | var pyInst20Cookie PyInst20Cookie 155 | cookieBuf := make([]byte, PYINST20_COOKIE_SIZE) 156 | if _, err := p.fPtr.Read(cookieBuf); err != nil { 157 | return failFunc() 158 | } 159 | 160 | if err := restruct.Unpack(cookieBuf, binary.LittleEndian, &pyInst20Cookie); err != nil { 161 | return failFunc() 162 | } 163 | 164 | p.pythonMajorVersion, p.pythonMinorVersion = getPyMajMinVersion(pyInst20Cookie.PythonVersion) 165 | printPythonVerLenPkg(p.pythonMajorVersion, p.pythonMinorVersion, uint(pyInst20Cookie.LengthOfPackage)) 166 | 167 | calculateTocPosition( 168 | PYINST20_COOKIE_SIZE, 169 | uint(pyInst20Cookie.LengthOfPackage), 170 | uint(pyInst20Cookie.Toc), 171 | pyInst20Cookie.TocLen, 172 | ) 173 | 174 | } else { 175 | var pyInst21Cookie PyInst21Cookie 176 | cookieBuf := make([]byte, PYINST21_COOKIE_SIZE) 177 | if _, err := p.fPtr.Read(cookieBuf); err != nil { 178 | return failFunc() 179 | } 180 | if err := restruct.Unpack(cookieBuf, binary.LittleEndian, &pyInst21Cookie); err != nil { 181 | return failFunc() 182 | } 183 | fmt.Println("[+] Python library file:", string(bytes.Trim(pyInst21Cookie.PythonLibName, "\x00"))) 184 | p.pythonMajorVersion, p.pythonMinorVersion = getPyMajMinVersion(pyInst21Cookie.PythonVersion) 185 | printPythonVerLenPkg(p.pythonMajorVersion, p.pythonMinorVersion, pyInst21Cookie.LengthOfPackage) 186 | 187 | calculateTocPosition( 188 | PYINST21_COOKIE_SIZE, 189 | pyInst21Cookie.LengthOfPackage, 190 | pyInst21Cookie.Toc, 191 | pyInst21Cookie.TocLen, 192 | ) 193 | } 194 | return true 195 | } 196 | 197 | func (p *PyInstArchive) ParseTOC() { 198 | const CTOCEntryStructSize = 18 199 | p.fPtr.Seek(p.tableOfContentsPosition, io.SeekStart) 200 | 201 | var parsedLen int64 = 0 202 | 203 | // Parse table of contents 204 | for { 205 | if parsedLen >= p.tableOfContentsSize { 206 | break 207 | } 208 | var ctocEntry CTOCEntry 209 | 210 | data := make([]byte, CTOCEntryStructSize) 211 | p.fPtr.Read(data) 212 | restruct.Unpack(data, binary.LittleEndian, &ctocEntry) 213 | 214 | nameBuffer := make([]byte, ctocEntry.EntrySize-CTOCEntryStructSize) 215 | p.fPtr.Read(nameBuffer) 216 | 217 | nameBuffer = bytes.TrimRight(nameBuffer, "\x00") 218 | if len(nameBuffer) == 0 { 219 | ctocEntry.Name = randomString() 220 | fmt.Printf("[!] Warning: Found an unamed file in CArchive. Using random name %s\n", ctocEntry.Name) 221 | } else { 222 | ctocEntry.Name = string(nameBuffer) 223 | } 224 | 225 | // fmt.Printf("%+v\n", ctocEntry) 226 | p.tableOfContents = append(p.tableOfContents, ctocEntry) 227 | parsedLen += int64(ctocEntry.EntrySize) 228 | } 229 | fmt.Printf("[+] Found %d files in CArchive\n", len(p.tableOfContents)) 230 | } 231 | 232 | func (p *PyInstArchive) ExtractFiles() { 233 | fmt.Println("[+] Beginning extraction...please standby") 234 | cwd, _ := os.Getwd() 235 | 236 | extractionDir := filepath.Join(cwd, filepath.Base(p.inFilePath)+"_extracted") 237 | if _, err := os.Stat(extractionDir); os.IsNotExist(err) { 238 | os.Mkdir(extractionDir, 0755) 239 | } 240 | os.Chdir(extractionDir) 241 | 242 | for _, entry := range p.tableOfContents { 243 | p.fPtr.Seek(p.overlayPosition + int64(entry.EntryPosition), io.SeekStart) 244 | data := make([]byte, entry.DataSize) 245 | p.fPtr.Read(data) 246 | 247 | if entry.ComressionFlag == 1 { 248 | var err error 249 | compressedData := data[:] 250 | data, err = zlibDecompress(compressedData) 251 | if err != nil { 252 | fmt.Printf("[!] Error: Failed to decompress %s in CArchive, extracting as-is", entry.Name) 253 | p.writeRawData(entry.Name, compressedData) 254 | continue 255 | } 256 | 257 | if uint(len(data)) != entry.UncompressedDataSize { 258 | fmt.Printf("[!] Warning: Decompressed size mismatch for file %s\n", entry.Name) 259 | } 260 | } 261 | 262 | if entry.TypeCompressedData == 'd' || entry.TypeCompressedData == 'o' { 263 | // d -> ARCHIVE_ITEM_DEPENDENCY 264 | // o -> ARCHIVE_ITEM_RUNTIME_OPTION 265 | // These are runtime options, not files 266 | continue 267 | } 268 | 269 | basePath := filepath.Dir(entry.Name) 270 | if basePath != "." { 271 | if _, err := os.Stat(basePath); os.IsNotExist(err) { 272 | os.MkdirAll(basePath, 0755) 273 | } 274 | } 275 | if entry.TypeCompressedData == 's' { 276 | // s -> ARCHIVE_ITEM_PYSOURCE 277 | // Entry point are expected to be python scripts 278 | fmt.Printf("[+] Possible entry point: %s.pyc\n", entry.Name) 279 | if !p.gotPycMagic { 280 | // if we don't have the pyc header yet, fix them in a later pass 281 | p.barePycsList = append(p.barePycsList, entry.Name+".pyc") 282 | } 283 | p.writePyc(entry.Name+".pyc", data) 284 | } else if entry.TypeCompressedData == 'M' || entry.TypeCompressedData == 'm' { 285 | // M -> ARCHIVE_ITEM_PYPACKAGE 286 | // m -> ARCHIVE_ITEM_PYMODULE 287 | // packages and modules are pyc files with their header intact 288 | 289 | // From PyInstaller 5.3 and above pyc headers are no longer stored 290 | // https://github.com/pyinstaller/pyinstaller/commit/a97fdf 291 | if data[2] == '\r' && data[3] == '\n' { 292 | // < pyinstaller 5.3 293 | if !p.gotPycMagic { 294 | copy(p.pycMagic[:], data[0:4]) 295 | p.gotPycMagic = true 296 | } 297 | p.writeRawData(entry.Name+".pyc", data) 298 | } else { 299 | // >= pyinstaller 5.3 300 | if !p.gotPycMagic { 301 | // if we don't have the pyc header yet, fix them in a later pass 302 | p.barePycsList = append(p.barePycsList, entry.Name+".pyc") 303 | } 304 | p.writePyc(entry.Name+".pyc", data) 305 | } 306 | } else { 307 | p.writeRawData(entry.Name, data) 308 | 309 | if entry.TypeCompressedData == 'z' || entry.TypeCompressedData == 'Z' { 310 | if p.pythonMajorVersion == 3 { 311 | p.extractPYZ(entry.Name) 312 | } else { 313 | fmt.Printf("[!] Skipping pyz extraction as Python %d.%d is not supported\n", p.pythonMajorVersion, p.pythonMinorVersion) 314 | } 315 | } 316 | } 317 | } 318 | p.fixBarePycs() 319 | } 320 | 321 | func (p *PyInstArchive) fixBarePycs() { 322 | for _, pycFile := range p.barePycsList { 323 | f, err := os.OpenFile(pycFile, os.O_RDWR, 0666) 324 | if err != nil { 325 | fmt.Printf("[!] Failed to fix header of file %s\n", pycFile) 326 | continue 327 | } 328 | f.Write(p.pycMagic[:]) 329 | f.Close() 330 | } 331 | } 332 | 333 | func (p *PyInstArchive) extractPYZ(path string) { 334 | dirName := path + "_extracted" 335 | if _, err := os.Stat(dirName); os.IsNotExist(err) { 336 | os.MkdirAll(dirName, 0755) 337 | } 338 | 339 | f, err := os.Open(path) 340 | if err != nil { 341 | fmt.Println("[!] Failed to extract pyz", err) 342 | return 343 | } 344 | var pyzMagic []byte = make([]byte, 4) 345 | f.Read(pyzMagic) 346 | if !bytes.Equal(pyzMagic, []byte("PYZ\x00")) { 347 | fmt.Println("[!] Magic header in PYZ archive doesn't match") 348 | } 349 | 350 | var pyzPycMagic []byte = make([]byte, 4) 351 | f.Read(pyzPycMagic) 352 | 353 | if !p.gotPycMagic { 354 | copy(p.pycMagic[:], pyzPycMagic) 355 | p.gotPycMagic = true 356 | } else if !bytes.Equal(p.pycMagic[:], pyzPycMagic) { 357 | copy(p.pycMagic[:], pyzPycMagic) 358 | p.gotPycMagic = true 359 | fmt.Println("[!] Warning: pyc magic of files inside PYZ archive are different from those in CArchive") 360 | } 361 | 362 | var pyzTocPositionBytes []byte = make([]byte, 4) 363 | f.Read(pyzTocPositionBytes) 364 | pyzTocPosition := binary.BigEndian.Uint32(pyzTocPositionBytes) 365 | f.Seek(int64(pyzTocPosition), io.SeekStart) 366 | 367 | su := marshal.NewUnmarshaler(f) 368 | obj := su.Unmarshal() 369 | if obj == nil { 370 | fmt.Println("Unmarshalling failed") 371 | } else { 372 | // pp.Print(obj) 373 | listobj := obj.(*marshal.PyListObject) 374 | listobjItems := listobj.GetItems() 375 | fmt.Printf("[+] Found %d files in PYZArchive\n", len(listobjItems)) 376 | 377 | for _, item := range listobjItems { 378 | item := item.(*marshal.PyListObject) 379 | name := item.GetItems()[0].(*marshal.PyStringObject).GetString() 380 | 381 | ispkg_position_length_tuple := item.GetItems()[1].(*marshal.PyListObject) 382 | ispkg := ispkg_position_length_tuple.GetItems()[0].(*marshal.PyIntegerObject).GetValue() 383 | position := ispkg_position_length_tuple.GetItems()[1].(*marshal.PyIntegerObject).GetValue() 384 | length := ispkg_position_length_tuple.GetItems()[2].(*marshal.PyIntegerObject).GetValue() 385 | 386 | // Prevent writing outside dirName 387 | filename := strings.ReplaceAll(name, "..", "__") 388 | filename = strings.ReplaceAll(filename, ".", string(os.PathSeparator)) 389 | 390 | var filenamepath string 391 | if ispkg == 1 { 392 | filenamepath = filepath.Join(dirName, filename, "__init__.pyc") 393 | } else { 394 | filenamepath = filepath.Join(dirName, filename+".pyc") 395 | } 396 | 397 | fileDir := filepath.Dir(filenamepath) 398 | if fileDir != "." { 399 | if _, err := os.Stat(fileDir); os.IsNotExist(err) { 400 | os.MkdirAll(fileDir, 0755) 401 | } 402 | } 403 | 404 | f.Seek(int64(position), io.SeekStart) 405 | 406 | var compressedData []byte = make([]byte, length) 407 | f.Read(compressedData) 408 | 409 | decompressedData, err := zlibDecompress(compressedData) 410 | if err != nil { 411 | fmt.Printf("[!] Error: Failed to decompress %s in PYZArchive, likely encrypted. Extracting as is", filenamepath) 412 | p.writeRawData(filenamepath + ".encrypted", compressedData) 413 | } else { 414 | p.writePyc(filenamepath, decompressedData) 415 | } 416 | } 417 | } 418 | f.Close() 419 | } 420 | 421 | func (p *PyInstArchive) writePyc(path string, data []byte) { 422 | f, err := os.Create(path) 423 | if err != nil { 424 | fmt.Printf("[!] Failed to write file %s\n", path) 425 | return 426 | } 427 | // pyc magic 428 | f.Write(p.pycMagic[:]) 429 | 430 | if p.pythonMajorVersion >= 3 && p.pythonMinorVersion >= 7 { 431 | // PEP 552 -- Deterministic pycs 432 | f.Write([]byte{0, 0, 0, 0}) //Bitfield 433 | f.Write([]byte{0, 0, 0, 0, 0, 0, 0, 0}) //(Timestamp + size) || hash 434 | } else { 435 | f.Write([]byte{0, 0, 0, 0}) //Timestamp 436 | if p.pythonMajorVersion >= 3 && p.pythonMinorVersion >= 3 { 437 | f.Write([]byte{0, 0, 0, 0}) 438 | } 439 | } 440 | f.Write(data) 441 | } 442 | 443 | func (p *PyInstArchive) writeRawData(path string, data []byte) { 444 | path = strings.Trim(path, "\x00") 445 | path = strings.ReplaceAll(path, "\\", string(os.PathSeparator)) 446 | path = strings.ReplaceAll(path, "/", string(os.PathSeparator)) 447 | path = strings.ReplaceAll(path, "..", "__") 448 | 449 | dir := filepath.Dir(path) 450 | if dir != "." { 451 | if _, err := os.Stat(dir); os.IsNotExist(err) { 452 | os.MkdirAll(dir, 0755) 453 | } 454 | } 455 | os.WriteFile(path, data, 0666) 456 | } 457 | 458 | func extract_exe(fileName string) { 459 | arch := PyInstArchive{inFilePath: fileName} 460 | 461 | if arch.Open() { 462 | if arch.CheckFile() { 463 | if arch.GetCArchiveInfo() { 464 | arch.ParseTOC() 465 | arch.ExtractFiles() 466 | fmt.Printf("[+] Successfully extracted pyinstaller archive: %s\n", fileName) 467 | fmt.Println("\nYou can now use a python decompiler on the pyc files within the extracted directory") 468 | } 469 | } 470 | arch.Close() 471 | } 472 | } 473 | 474 | func main() { 475 | if len(os.Args) < 2 { 476 | fmt.Println("[+] Usage pyinstxtractor-ng ')") 477 | return 478 | } 479 | extract_exe(os.Args[1]) 480 | } 481 | -------------------------------------------------------------------------------- /main_gopherjs.go: -------------------------------------------------------------------------------- 1 | //go:build gopherjs 2 | // +build gopherjs 3 | 4 | package main 5 | 6 | import ( 7 | "archive/zip" 8 | "bytes" 9 | "encoding/binary" 10 | "fmt" 11 | "io" 12 | "os" 13 | "path/filepath" 14 | "strings" 15 | 16 | "pyinstxtractor-go/marshal" 17 | 18 | "github.com/go-restruct/restruct" 19 | "github.com/gopherjs/gopherjs/js" 20 | ) 21 | 22 | type PyInstArchive struct { 23 | inFilePath string 24 | outZip *zip.Writer 25 | fPtr io.ReadSeeker 26 | fileSize int64 27 | cookiePosition int64 28 | pyInstVersion int64 29 | pythonMajorVersion int 30 | pythonMinorVersion int 31 | overlaySize int64 32 | overlayPosition int64 33 | tableOfContentsSize int64 34 | tableOfContentsPosition int64 35 | tableOfContents []CTOCEntry 36 | pycMagic [4]byte 37 | gotPycMagic bool 38 | barePycsList []*barePyc 39 | } 40 | 41 | type barePyc struct { 42 | filepath string 43 | contents []byte 44 | } 45 | 46 | var logFunc *js.Object 47 | 48 | func appendLog(logLine string) { 49 | logFunc.Invoke(logLine) 50 | } 51 | 52 | func (p *PyInstArchive) Open() bool { 53 | return true 54 | } 55 | 56 | func (p *PyInstArchive) Close() { 57 | } 58 | 59 | func (p *PyInstArchive) CheckFile() bool { 60 | appendLog(fmt.Sprintf("[+] Processing %s\n", p.inFilePath)) 61 | 62 | var searchChunkSize int64 = 8192 63 | endPosition := p.fileSize 64 | p.cookiePosition = -1 65 | 66 | if endPosition < int64(len(PYINST_MAGIC)) { 67 | appendLog("[!] Error : File is too short or truncated\n") 68 | return false 69 | } 70 | 71 | var startPosition, chunkSize int64 72 | for { 73 | if endPosition >= searchChunkSize { 74 | startPosition = endPosition - searchChunkSize 75 | } else { 76 | startPosition = 0 77 | } 78 | chunkSize = endPosition - startPosition 79 | if chunkSize < int64(len(PYINST_MAGIC)) { 80 | break 81 | } 82 | 83 | if _, err := p.fPtr.Seek(startPosition, io.SeekStart); err != nil { 84 | appendLog("[!] File seek failed\n") 85 | return false 86 | } 87 | var data []byte = make([]byte, searchChunkSize) 88 | p.fPtr.Read(data) 89 | 90 | if offs := bytes.Index(data, PYINST_MAGIC[:]); offs != -1 { 91 | p.cookiePosition = startPosition + int64(offs) 92 | break 93 | } 94 | endPosition = startPosition + int64(len(PYINST_MAGIC)) - 1 95 | 96 | if startPosition == 0 { 97 | break 98 | } 99 | } 100 | if p.cookiePosition == -1 { 101 | appendLog("[!] Error : Missing cookie, unsupported pyinstaller version or not a pyinstaller archive\n") 102 | return false 103 | } 104 | p.fPtr.Seek(p.cookiePosition+PYINST20_COOKIE_SIZE, io.SeekStart) 105 | 106 | var cookie []byte = make([]byte, 64) 107 | if _, err := p.fPtr.Read(cookie); err != nil { 108 | appendLog("[!] Failed to read cookie!\n") 109 | return false 110 | } 111 | 112 | cookie = bytes.ToLower(cookie) 113 | if bytes.Contains(cookie, []byte("python")) { 114 | p.pyInstVersion = 21 115 | appendLog("[+] Pyinstaller version: 2.1+\n") 116 | } else { 117 | p.pyInstVersion = 20 118 | appendLog("[+] Pyinstaller version: 2.0\n") 119 | } 120 | return true 121 | } 122 | 123 | func (p *PyInstArchive) GetCArchiveInfo() bool { 124 | failFunc := func() bool { 125 | appendLog("[!] Error : The file is not a pyinstaller archive\n") 126 | return false 127 | } 128 | 129 | getPyMajMinVersion := func(version int) (int, int) { 130 | if version >= 100 { 131 | return version / 100, version % 100 132 | } 133 | return version / 10, version % 10 134 | } 135 | 136 | printPythonVerLenPkg := func(pyMajVer, pyMinVer int, lenPkg uint) { 137 | appendLog(fmt.Sprintf("[+] Python version: %d.%d\n", pyMajVer, pyMinVer)) 138 | appendLog(fmt.Sprintf("[+] Length of package: %d bytes\n", lenPkg)) 139 | } 140 | 141 | calculateTocPosition := func(cookieSize int, lengthOfPackage, toc uint, tocLen int) { 142 | // Additional data after the cookie 143 | tailBytes := p.fileSize - p.cookiePosition - int64(cookieSize) 144 | 145 | // Overlay is the data appended at the end of the PE 146 | p.overlaySize = int64(lengthOfPackage) + tailBytes 147 | p.overlayPosition = p.fileSize - p.overlaySize 148 | p.tableOfContentsPosition = p.overlayPosition + int64(toc) 149 | p.tableOfContentsSize = int64(tocLen) 150 | } 151 | 152 | if _, err := p.fPtr.Seek(p.cookiePosition, io.SeekStart); err != nil { 153 | return failFunc() 154 | } 155 | 156 | if p.pyInstVersion == 20 { 157 | var pyInst20Cookie PyInst20Cookie 158 | cookieBuf := make([]byte, PYINST20_COOKIE_SIZE) 159 | if _, err := p.fPtr.Read(cookieBuf); err != nil { 160 | return failFunc() 161 | } 162 | 163 | if err := restruct.Unpack(cookieBuf, binary.LittleEndian, &pyInst20Cookie); err != nil { 164 | return failFunc() 165 | } 166 | 167 | p.pythonMajorVersion, p.pythonMinorVersion = getPyMajMinVersion(pyInst20Cookie.PythonVersion) 168 | printPythonVerLenPkg(p.pythonMajorVersion, p.pythonMinorVersion, uint(pyInst20Cookie.LengthOfPackage)) 169 | 170 | calculateTocPosition( 171 | PYINST20_COOKIE_SIZE, 172 | uint(pyInst20Cookie.LengthOfPackage), 173 | uint(pyInst20Cookie.Toc), 174 | pyInst20Cookie.TocLen, 175 | ) 176 | 177 | } else { 178 | var pyInst21Cookie PyInst21Cookie 179 | cookieBuf := make([]byte, PYINST21_COOKIE_SIZE) 180 | if _, err := p.fPtr.Read(cookieBuf); err != nil { 181 | return failFunc() 182 | } 183 | if err := restruct.Unpack(cookieBuf, binary.LittleEndian, &pyInst21Cookie); err != nil { 184 | return failFunc() 185 | } 186 | appendLog("[+] Python library file: " + string(bytes.Trim(pyInst21Cookie.PythonLibName, "\x00")) + "\n") 187 | p.pythonMajorVersion, p.pythonMinorVersion = getPyMajMinVersion(pyInst21Cookie.PythonVersion) 188 | printPythonVerLenPkg(p.pythonMajorVersion, p.pythonMinorVersion, pyInst21Cookie.LengthOfPackage) 189 | 190 | calculateTocPosition( 191 | PYINST21_COOKIE_SIZE, 192 | pyInst21Cookie.LengthOfPackage, 193 | pyInst21Cookie.Toc, 194 | pyInst21Cookie.TocLen, 195 | ) 196 | } 197 | return true 198 | } 199 | 200 | func (p *PyInstArchive) ParseTOC() { 201 | const CTOCEntryStructSize = 18 202 | p.fPtr.Seek(p.tableOfContentsPosition, io.SeekStart) 203 | 204 | var parsedLen int64 = 0 205 | 206 | // Parse table of contents 207 | for { 208 | if parsedLen >= p.tableOfContentsSize { 209 | break 210 | } 211 | var ctocEntry CTOCEntry 212 | 213 | data := make([]byte, CTOCEntryStructSize) 214 | p.fPtr.Read(data) 215 | restruct.Unpack(data, binary.LittleEndian, &ctocEntry) 216 | 217 | nameBuffer := make([]byte, ctocEntry.EntrySize-CTOCEntryStructSize) 218 | p.fPtr.Read(nameBuffer) 219 | 220 | nameBuffer = bytes.TrimRight(nameBuffer, "\x00") 221 | if len(nameBuffer) == 0 { 222 | ctocEntry.Name = randomString() 223 | appendLog(fmt.Sprintf("[!] Warning: Found an unamed file in CArchive. Using random name %s\n", ctocEntry.Name)) 224 | } else { 225 | ctocEntry.Name = string(nameBuffer) 226 | } 227 | 228 | p.tableOfContents = append(p.tableOfContents, ctocEntry) 229 | parsedLen += int64(ctocEntry.EntrySize) 230 | } 231 | appendLog(fmt.Sprintf("[+] Found %d files in CArchive\n", len(p.tableOfContents))) 232 | } 233 | 234 | func (p *PyInstArchive) ExtractFiles() { 235 | appendLog("[+] Beginning extraction...please standby\n") 236 | 237 | for _, entry := range p.tableOfContents { 238 | p.fPtr.Seek(p.overlayPosition+int64(entry.EntryPosition), io.SeekStart) 239 | data := make([]byte, entry.DataSize) 240 | p.fPtr.Read(data) 241 | 242 | if entry.ComressionFlag == 1 { 243 | var err error 244 | compressedData := data[:] 245 | data, err = zlibDecompress(compressedData) 246 | if err != nil { 247 | appendLog(fmt.Sprintf("[!] Error: Failed to decompress %s in CArchive, extracting as-is\n", entry.Name)) 248 | p.writeRawData(entry.Name, compressedData) 249 | continue 250 | } 251 | 252 | if uint(len(data)) != entry.UncompressedDataSize { 253 | appendLog(fmt.Sprintf("[!] Warning: Decompressed size mismatch for file %s\n", entry.Name)) 254 | } 255 | } 256 | 257 | if entry.TypeCompressedData == 'd' || entry.TypeCompressedData == 'o' { 258 | // d -> ARCHIVE_ITEM_DEPENDENCY 259 | // o -> ARCHIVE_ITEM_RUNTIME_OPTION 260 | // These are runtime options, not files 261 | continue 262 | } 263 | 264 | if entry.TypeCompressedData == 's' { 265 | // s -> ARCHIVE_ITEM_PYSOURCE 266 | // Entry point are expected to be python scripts 267 | appendLog(fmt.Sprintf("[+] Possible entry point: %s.pyc\n", entry.Name)) 268 | if !p.gotPycMagic { 269 | // if we don't have the pyc header yet, fix them in a later pass 270 | p.barePycsList = append(p.barePycsList, &barePyc{entry.Name + ".pyc", data}) 271 | } else { 272 | p.writePyc(entry.Name+".pyc", data) 273 | } 274 | } else if entry.TypeCompressedData == 'M' || entry.TypeCompressedData == 'm' { 275 | // M -> ARCHIVE_ITEM_PYPACKAGE 276 | // m -> ARCHIVE_ITEM_PYMODULE 277 | // packages and modules are pyc files with their header intact 278 | 279 | // From PyInstaller 5.3 and above pyc headers are no longer stored 280 | // https://github.com/pyinstaller/pyinstaller/commit/a97fdf 281 | if data[2] == '\r' && data[3] == '\n' { 282 | // < pyinstaller 5.3 283 | if !p.gotPycMagic { 284 | copy(p.pycMagic[:], data[0:4]) 285 | p.gotPycMagic = true 286 | } 287 | p.writeRawData(entry.Name+".pyc", data) 288 | } else { 289 | // >= pyinstaller 5.3 290 | if !p.gotPycMagic { 291 | // if we don't have the pyc header yet, fix them in a later pass 292 | p.barePycsList = append(p.barePycsList, &barePyc{entry.Name + ".pyc", data}) 293 | } else { 294 | p.writePyc(entry.Name+".pyc", data) 295 | } 296 | } 297 | } else if entry.TypeCompressedData == 'z' || entry.TypeCompressedData == 'Z' { 298 | if p.pythonMajorVersion == 3 { 299 | p.extractPYZ(entry.Name, data) 300 | } else { 301 | appendLog(fmt.Sprintf("[!] Skipping pyz extraction as Python %d.%d is not supported\n", p.pythonMajorVersion, p.pythonMinorVersion)) 302 | p.writeRawData(entry.Name, data) 303 | } 304 | } else { 305 | p.writeRawData(entry.Name, data) 306 | } 307 | } 308 | p.fixBarePycs() 309 | } 310 | 311 | func (p *PyInstArchive) fixBarePycs() { 312 | for _, pycFile := range p.barePycsList { 313 | p.writePyc(pycFile.filepath, pycFile.contents) 314 | } 315 | } 316 | 317 | func (p *PyInstArchive) extractPYZ(path string, pyzData []byte) { 318 | dirName := path + "_extracted" 319 | f := bytes.NewReader(pyzData) 320 | 321 | var pyzMagic []byte = make([]byte, 4) 322 | f.Read(pyzMagic) 323 | if !bytes.Equal(pyzMagic, []byte("PYZ\x00")) { 324 | appendLog("[!] Magic header in PYZ archive doesn't match\n") 325 | } 326 | 327 | var pyzPycMagic []byte = make([]byte, 4) 328 | f.Read(pyzPycMagic) 329 | 330 | if !p.gotPycMagic { 331 | copy(p.pycMagic[:], pyzPycMagic) 332 | p.gotPycMagic = true 333 | } else if !bytes.Equal(p.pycMagic[:], pyzPycMagic) { 334 | copy(p.pycMagic[:], pyzPycMagic) 335 | p.gotPycMagic = true 336 | appendLog("[!] Warning: pyc magic of files inside PYZ archive are different from those in CArchive\n") 337 | } 338 | 339 | var pyzTocPositionBytes []byte = make([]byte, 4) 340 | f.Read(pyzTocPositionBytes) 341 | pyzTocPosition := binary.BigEndian.Uint32(pyzTocPositionBytes) 342 | f.Seek(int64(pyzTocPosition), io.SeekStart) 343 | 344 | su := marshal.NewUnmarshaler(f) 345 | obj := su.Unmarshal() 346 | if obj == nil { 347 | appendLog("Unmarshalling failed\n") 348 | } else { 349 | listobj := obj.(*marshal.PyListObject) 350 | listobjItems := listobj.GetItems() 351 | appendLog(fmt.Sprintf("[+] Found %d files in PYZArchive\n", len(listobjItems))) 352 | 353 | for _, item := range listobjItems { 354 | item := item.(*marshal.PyListObject) 355 | name := item.GetItems()[0].(*marshal.PyStringObject).GetString() 356 | 357 | ispkg_position_length_tuple := item.GetItems()[1].(*marshal.PyListObject) 358 | ispkg := ispkg_position_length_tuple.GetItems()[0].(*marshal.PyIntegerObject).GetValue() 359 | position := ispkg_position_length_tuple.GetItems()[1].(*marshal.PyIntegerObject).GetValue() 360 | length := ispkg_position_length_tuple.GetItems()[2].(*marshal.PyIntegerObject).GetValue() 361 | 362 | // Prevent writing outside dirName 363 | filename := strings.ReplaceAll(name, "..", "__") 364 | filename = strings.ReplaceAll(filename, ".", string(os.PathSeparator)) 365 | 366 | var filenamepath string 367 | if ispkg == 1 { 368 | filenamepath = filepath.Join(dirName, filename, "__init__.pyc") 369 | } else { 370 | filenamepath = filepath.Join(dirName, filename+".pyc") 371 | } 372 | 373 | f.Seek(int64(position), io.SeekStart) 374 | 375 | var compressedData []byte = make([]byte, length) 376 | f.Read(compressedData) 377 | 378 | decompressedData, err := zlibDecompress(compressedData) 379 | if err != nil { 380 | appendLog(fmt.Sprintf("[!] Error: Failed to decompress %s in PYZArchive, likely encrypted. Extracting as is\n", filenamepath)) 381 | p.writeRawData(filenamepath+".encrypted", compressedData) 382 | } else { 383 | p.writePyc(filenamepath, decompressedData) 384 | } 385 | } 386 | } 387 | // f.Close() 388 | } 389 | 390 | func (p *PyInstArchive) writePyc(path string, data []byte) { 391 | f, err := p.outZip.CreateHeader(&zip.FileHeader{ 392 | Name: path, 393 | Method: zip.Store, 394 | }) 395 | 396 | if err != nil { 397 | appendLog(fmt.Sprintf("[!] Failed to write file %s\n", path)) 398 | return 399 | } 400 | // pyc magic 401 | f.Write(p.pycMagic[:]) 402 | 403 | if p.pythonMajorVersion >= 3 && p.pythonMinorVersion >= 7 { 404 | // PEP 552 -- Deterministic pycs 405 | f.Write([]byte{0, 0, 0, 0}) //Bitfield 406 | f.Write([]byte{0, 0, 0, 0, 0, 0, 0, 0}) //(Timestamp + size) || hash 407 | } else { 408 | f.Write([]byte{0, 0, 0, 0}) //Timestamp 409 | if p.pythonMajorVersion >= 3 && p.pythonMinorVersion >= 3 { 410 | f.Write([]byte{0, 0, 0, 0}) 411 | } 412 | } 413 | f.Write(data) 414 | p.outZip.Flush() 415 | } 416 | 417 | func (p *PyInstArchive) writeRawData(path string, data []byte) { 418 | path = strings.Trim(path, "\x00") 419 | path = strings.ReplaceAll(path, "\\", string(os.PathSeparator)) 420 | path = strings.ReplaceAll(path, "/", string(os.PathSeparator)) 421 | path = strings.ReplaceAll(path, "..", "__") 422 | 423 | f, _ := p.outZip.CreateHeader(&zip.FileHeader{ 424 | Name: path, 425 | Method: zip.Store, 426 | }) 427 | 428 | f.Write(data) 429 | } 430 | 431 | func main() { 432 | js.Global.Set("extract_exe", extract_exe) 433 | } 434 | 435 | func extract_exe(fileName string, inbuf []byte, logFn *js.Object) []byte { 436 | logFunc = logFn 437 | var zipData bytes.Buffer 438 | arch := PyInstArchive{ 439 | outZip: zip.NewWriter(&zipData), 440 | inFilePath: fileName, 441 | fPtr: bytes.NewReader(inbuf), 442 | fileSize: int64(len(inbuf)), 443 | } 444 | 445 | if arch.Open() { 446 | if arch.CheckFile() { 447 | if arch.GetCArchiveInfo() { 448 | arch.ParseTOC() 449 | arch.ExtractFiles() 450 | appendLog(fmt.Sprintf("[+] Successfully extracted pyinstaller archive: %s\n", fileName)) 451 | appendLog("\nYou can now use a python decompiler on the pyc files within the extracted directory\n") 452 | arch.outZip.Close() 453 | return zipData.Bytes() 454 | } 455 | } 456 | arch.Close() 457 | } 458 | return nil 459 | } 460 | -------------------------------------------------------------------------------- /marshal/marshal_types.go: -------------------------------------------------------------------------------- 1 | package marshal 2 | 3 | import ( 4 | ) 5 | 6 | const ( 7 | MARSHAL_VERSION = 3 8 | TYPE_NULL = '0' 9 | TYPE_NONE = 'N' 10 | TYPE_FALSE = 'F' 11 | TYPE_TRUE = 'T' 12 | TYPE_STOPITER = 'S' 13 | TYPE_ELLIPSIS = '.' 14 | TYPE_INT = 'i' 15 | TYPE_FLOAT = 'f' 16 | TYPE_BINARY_FLOAT = 'g' 17 | TYPE_COMPLEX = 'x' 18 | TYPE_BINARY_COMPLEX = 'y' 19 | TYPE_LONG = 'l' 20 | TYPE_STRING = 's' 21 | TYPE_INTERNED = 't' 22 | TYPE_REF = 'r' 23 | TYPE_TUPLE = '(' 24 | TYPE_LIST = '[' 25 | TYPE_DICT = '{' 26 | TYPE_CODE = 'c' 27 | TYPE_UNICODE = 'u' 28 | TYPE_UNKNOWN = '?' 29 | TYPE_SET = '<' 30 | TYPE_FROZENSET = '>' 31 | FLAG_REF = 0x80 // with a type, add obj to index 32 | SIZE32_MAX = 0x7FFFFFFF 33 | 34 | TYPE_ASCII = 'a' 35 | TYPE_ASCII_INTERNED = 'A' 36 | TYPE_SMALL_TUPLE = ')' 37 | TYPE_SHORT_ASCII = 'z' 38 | TYPE_SHORT_ASCII_INTERNED = 'Z' 39 | 40 | // We assume that Python ints are stored internally in base some power of 41 | // 2**15; for the sake of portability we'll always read and write them in base 42 | // exactly 2**15. 43 | 44 | PyLong_MARSHAL_SHIFT = 15 45 | PyLong_MARSHAL_BASE = (1 << PyLong_MARSHAL_SHIFT) 46 | PyLong_MARSHAL_MASK = (PyLong_MARSHAL_BASE - 1) 47 | ) 48 | 49 | 50 | -------------------------------------------------------------------------------- /marshal/objecttype.go: -------------------------------------------------------------------------------- 1 | package marshal 2 | 3 | type _object interface { 4 | r_object() _object 5 | } 6 | -------------------------------------------------------------------------------- /marshal/pyintegerobject.go: -------------------------------------------------------------------------------- 1 | package marshal 2 | 3 | import ( 4 | "encoding/binary" 5 | // "fmt" 6 | "io" 7 | ) 8 | 9 | type PyIntegerObject struct { 10 | reader io.Reader 11 | value int32 12 | } 13 | 14 | func (pio *PyIntegerObject) r_object() _object { 15 | if err := binary.Read(pio.reader, binary.LittleEndian, &pio.value); err != nil { 16 | panic("Failed to read integer object") 17 | } 18 | 19 | // fmt.Println("integer_object, value=", pio.value) 20 | return pio 21 | } 22 | 23 | func (pio *PyIntegerObject) GetValue() int { 24 | return int(pio.value) 25 | } -------------------------------------------------------------------------------- /marshal/pylistobject.go: -------------------------------------------------------------------------------- 1 | package marshal 2 | 3 | import ( 4 | "encoding/binary" 5 | // "fmt" 6 | "io" 7 | ) 8 | 9 | type PyListObject struct { 10 | reader io.Reader 11 | items []_object 12 | typecode byte 13 | } 14 | 15 | func (plo *PyListObject) r_object() _object { 16 | var nItems int 17 | if plo.typecode == TYPE_SMALL_TUPLE { 18 | var size int8 19 | if err := binary.Read(plo.reader, binary.LittleEndian, &size); err != nil { 20 | panic("Failed to read SMALl_TUPLE size") 21 | } 22 | nItems = int(size) 23 | // fmt.Println("small_tuple, size=", nItems) 24 | } else { 25 | var size int32 26 | if err := binary.Read(plo.reader, binary.LittleEndian, &size); err != nil { 27 | panic("Failed to read size") 28 | } 29 | nItems = int(size) 30 | // fmt.Println("list or tuple, size=", nItems) 31 | } 32 | 33 | for i := 0; i < nItems; i++ { 34 | po := &PyObject{plo.reader} 35 | plo.items = append(plo.items, po.r_object()) 36 | } 37 | return plo 38 | } 39 | 40 | func (plo *PyListObject) GetItems() []_object { 41 | return plo.items 42 | } 43 | -------------------------------------------------------------------------------- /marshal/pyobject.go: -------------------------------------------------------------------------------- 1 | package marshal 2 | 3 | import ( 4 | "encoding/binary" 5 | // "fmt" 6 | "io" 7 | ) 8 | 9 | type PyObject struct { 10 | reader io.Reader 11 | } 12 | 13 | func (po *PyObject) r_object() _object { 14 | var code byte 15 | if err := binary.Read(po.reader, binary.LittleEndian, &code); err != nil { 16 | panic("Failed to read code byte") 17 | } 18 | 19 | addRef := (code & FLAG_REF) != 0 20 | typecode := code &^ FLAG_REF 21 | // fmt.Printf("%c ", typecode) 22 | var obj _object 23 | 24 | switch typecode { 25 | case TYPE_LIST, TYPE_TUPLE, TYPE_SMALL_TUPLE: 26 | obj = &PyListObject{reader: po.reader, typecode: typecode} 27 | obj.r_object() 28 | 29 | case TYPE_SHORT_ASCII, TYPE_SHORT_ASCII_INTERNED, 30 | TYPE_STRING, TYPE_INTERNED, TYPE_UNICODE, 31 | TYPE_ASCII, TYPE_ASCII_INTERNED: 32 | obj = &PyStringObject{reader: po.reader, typecode: typecode} 33 | obj.r_object() 34 | 35 | case TYPE_INT: 36 | obj = &PyIntegerObject{reader: po.reader} 37 | obj.r_object() 38 | 39 | case TYPE_REF: 40 | // Reference to a previous read 41 | var n int32 42 | if err := binary.Read(po.reader, binary.LittleEndian, &n); err != nil { 43 | panic("Failed to read TYPE_REF") 44 | } 45 | 46 | if n < 1 || int(n) >= len(_unmarshaler.refs) { 47 | panic("TYPE_REF out of bounds") 48 | } 49 | n -= 1 50 | // fmt.Println("Get ref", n) 51 | obj = _unmarshaler.refs[n] 52 | 53 | default: 54 | panic("Unsupported typecode: " + string(typecode)) 55 | 56 | } 57 | if addRef { 58 | // fmt.Println("Added ref", len(_unmarshaler.refs)) 59 | _unmarshaler.refs = append(_unmarshaler.refs, obj) 60 | } 61 | return obj 62 | } 63 | -------------------------------------------------------------------------------- /marshal/pystringobject.go: -------------------------------------------------------------------------------- 1 | package marshal 2 | 3 | import ( 4 | "encoding/binary" 5 | // "fmt" 6 | "io" 7 | ) 8 | 9 | type PyStringObject struct { 10 | reader io.Reader 11 | value string 12 | typecode byte 13 | } 14 | 15 | func (pso *PyStringObject) r_object() _object { 16 | var length int 17 | 18 | switch pso.typecode { 19 | case TYPE_SHORT_ASCII, TYPE_SHORT_ASCII_INTERNED: 20 | var size uint8 21 | err := binary.Read(pso.reader, binary.LittleEndian, &size) 22 | if err != nil { 23 | return nil 24 | } 25 | length = int(size) 26 | 27 | case TYPE_STRING, TYPE_INTERNED, TYPE_UNICODE, 28 | TYPE_ASCII, TYPE_ASCII_INTERNED: 29 | var size int32 30 | err := binary.Read(pso.reader, binary.LittleEndian, &size) 31 | if err != nil { 32 | return nil 33 | } 34 | length = int(size) 35 | } 36 | 37 | buf := make([]byte, length) 38 | _, err := io.ReadFull(pso.reader, buf) 39 | if err != nil { 40 | return nil 41 | } 42 | 43 | pso.value = string(buf) 44 | // fmt.Println("string_object, value=", pso.value) 45 | return pso 46 | } 47 | 48 | func(pso *PyStringObject) GetString() string { 49 | return pso.value 50 | } 51 | -------------------------------------------------------------------------------- /marshal/unmarshal.go: -------------------------------------------------------------------------------- 1 | package marshal 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | ) 7 | 8 | type SimpleUnmarshaler struct { 9 | reader io.Reader 10 | refs []_object 11 | } 12 | 13 | var _unmarshaler *SimpleUnmarshaler 14 | 15 | func NewUnmarshaler(r io.Reader) *SimpleUnmarshaler { 16 | _unmarshaler = &SimpleUnmarshaler{reader: r} 17 | return _unmarshaler 18 | } 19 | 20 | func (su *SimpleUnmarshaler) Unmarshal() _object { 21 | defer func() { 22 | if r := recover(); r != nil { 23 | fmt.Println("Panicked during unmarshal!") 24 | fmt.Println(r) 25 | } 26 | }() 27 | 28 | pobj := PyObject{reader: su.reader} 29 | return pobj.r_object() 30 | } -------------------------------------------------------------------------------- /public/css/style.css: -------------------------------------------------------------------------------- 1 | .bottom-badges { 2 | display: block; 3 | padding: 1rem; 4 | text-align: center; 5 | } 6 | 7 | .output-box { 8 | overflow-y: scroll; 9 | resize: none; 10 | font-family: "Consolas", "Courier New", "Monospaced"; 11 | } 12 | 13 | .output-box-div { 14 | padding-top: 2rem; 15 | padding-bottom: 1rem; 16 | } 17 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | PyInstaller Extractor WEB 12 | 13 | 14 | 15 |
16 | 17 |

PyInstaller Extractor WEB🌐

18 | Pyinstxtractor running in the browser, powered by GopherJS! 19 | 20 |
21 | 22 |
23 | 24 |
25 | 26 | 27 |
28 |
29 |
    30 |
  • Runs totally client side, nothing uploaded
  • 31 |
  • Extracting large files may take some time, please be patient 😕
  • 32 |
  • Alternatively try pyinstxtractor-ng or pyinstxtractor as a desktop version of this tool
  • 33 |
34 |
35 |
36 | 37 |
38 | 39 | 40 | 41 | 42 | 43 |
44 |
45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /public/js/main.js: -------------------------------------------------------------------------------- 1 | document.onreadystatechange = function () { 2 | clearLog(); 3 | document.getElementById("file-upload-form").reset() 4 | } 5 | 6 | const downloadURL = (data, fileName) => { 7 | const a = document.createElement("a"); 8 | a.href = data; 9 | a.download = fileName; 10 | document.body.appendChild(a); 11 | a.style.display = "none"; 12 | a.click(); 13 | a.remove(); 14 | }; 15 | 16 | const appendLog = (line) => { 17 | logBox.value += line; 18 | } 19 | 20 | const clearLog = () => { 21 | const logBox = document.getElementById("logBox"); 22 | logBox.value = ""; 23 | } 24 | 25 | const downloadBlob = (data, fileName, mimeType) => { 26 | const blob = new Blob([data], { 27 | type: mimeType, 28 | }); 29 | 30 | const url = window.URL.createObjectURL(blob); 31 | downloadURL(url, fileName); 32 | setTimeout(() => window.URL.revokeObjectURL(url), 1000); 33 | }; 34 | 35 | const worker = new Worker("/js/worker.js"); 36 | 37 | document 38 | .getElementById("file-upload-form") 39 | .addEventListener("submit", function (evt) { 40 | evt.preventDefault(); 41 | const process_btn = document.getElementById("process-btn"); 42 | process_btn.innerText = "⚙️Processing..."; 43 | process_btn.disabled = true; 44 | 45 | const file = document.getElementById("file-upload-input").files[0]; 46 | clearLog(); 47 | appendLog("[+] Please stand by...\n") 48 | 49 | worker.onmessage = (evt) => { 50 | const message = evt.data; 51 | switch (message["type"]) { 52 | case "file": { 53 | const outFile = message["value"]; 54 | if (outFile.length == 0) { 55 | appendLog("[!] Extraction failed"); 56 | } 57 | else { 58 | appendLog("[+] Extraction completed successfully, downloading zip"); 59 | downloadBlob(outFile, file.name + "_extracted.zip", "application/octet-stream"); 60 | } 61 | process_btn.innerText = "⚙️Process"; 62 | process_btn.disabled = false; 63 | break; 64 | } 65 | case "log": { 66 | appendLog(message["value"]); 67 | break; 68 | } 69 | } 70 | }; 71 | worker.postMessage(file); 72 | }); 73 | -------------------------------------------------------------------------------- /public/js/worker.js: -------------------------------------------------------------------------------- 1 | importScripts("/js/pyinstxtractor-go.js"); 2 | 3 | const logFn = (line) => { 4 | postMessage({ 5 | type: "log", 6 | value: line 7 | }); 8 | } 9 | 10 | onmessage = async (evt) => { 11 | const file = evt.data; 12 | const fileData = new Uint8Array(await file.arrayBuffer()); 13 | const result = extract_exe(file.name, fileData, logFn) 14 | 15 | postMessage({ 16 | type: "file", 17 | value: result 18 | }); 19 | 20 | }; 21 | --------------------------------------------------------------------------------