diff --git a/cmd/lsconvert/fileversion.go b/cmd/lsconvert/fileversion.go new file mode 100644 index 0000000..27bc488 --- /dev/null +++ b/cmd/lsconvert/fileversion.go @@ -0,0 +1,19 @@ +package main + +import ( + "github.com/jdferrell3/peinfo-go/peinfo" +) + +func getPEFileVersion(filepath string) string { + f, err := peinfo.Initialize(filepath, false, "", false) + if err != nil { + panic(err) + } + defer f.OSFile.Close() + defer f.PEFile.Close() + v, _, err := f.GetVersionInfo() + if err != nil { + panic(err) + } + return v["FileVersion"] +} diff --git a/cmd/lsconvert/init.go b/cmd/lsconvert/init.go index a312421..431ee35 100644 --- a/cmd/lsconvert/init.go +++ b/cmd/lsconvert/init.go @@ -1,15 +1,21 @@ package main import ( + "bytes" + "errors" "flag" "fmt" "os" + "os/exec" "path/filepath" "runtime" "strings" + "sync" "github.com/go-git/go-git/v5" + "github.com/hashicorp/go-multierror" + "git.narnian.us/lordwelch/lsgo/gog" ie "git.narnian.us/lordwelch/lsgo/internal/exec" "golang.org/x/sys/windows/registry" ) @@ -19,26 +25,202 @@ var ( initFlags struct { BG3Path string divinePath string + Repo string } ) -func initRepository(arguments []string) { +const ignore = `# ignores all binary files +*.wem +*.dds +*.png +*.jpg +*.gr2 +*.bin +*.patch +*.bshd +*.tga +*.ttf +*.fnt +Assets_pak/Public/Shared/Assets/Characters/_Anims/Humans/_Female/HUM_F_Rig/HUM_F_Rig_DFLT_SPL_Somatic_MimeTalk_01 +Assets_pak/Public/Shared/Assets/Characters/_Models/_Creatures/Beholder/BEH_Spectator_A_GM_DDS +Assets_pak/Public/Shared/Assets/Consumables/CONS_GEN_Food_Turkey_ABC/CONS_GEN_Food_Turkey_ABC.ma +Assets_pak/Public/Shared/Assets/Decoration/Harbour/DEC_Harbour_Shell_ABC/DEC_Harbour_Shell_Venus_A +*.bnk +*.pak +*.data +*.osi +*.cur +*.gtp +*.gts +*.ffxanim +*.ffxactor +*.ffxbones +*.bk2 +` + +type BG3Repo struct { + git *git.Repository + gitPath string + gog gog.GOGalaxy + BG3Path string + divinePath string + Empty bool +} + +func openRepository(path string) (BG3Repo, error) { var ( - BG3Path, divinePath string - repo *git.Repository - err error + b BG3Repo + err error ) - BG3Path, divinePath = preRequisites() - if BG3Path == "" { + b.gitPath = path + b.BG3Path, b.divinePath = preRequisites() + if b.BG3Path == "" { panic("Could not locate Baldur's Gate 3") } - fmt.Println(divinePath) - panic("Baldur's Gate 3 located " + BG3Path) - repo, err = git.PlainInit(".", false) + + b.git, err = git.PlainOpen(b.gitPath) if err != nil { - panic(err) + b.Empty = true } - repo.Fetch(nil) + return b, nil +} + +func (bgr *BG3Repo) Init() error { + var err error + if bgr.Empty || bgr.git == nil { + bgr.git, err = git.PlainInit(bgr.gitPath, false) + if err == git.ErrRepositoryAlreadyExists { + bgr.git, err = git.PlainOpen(bgr.gitPath) + if err != nil { + return err + } + } + } + + if citer, err := bgr.git.CommitObjects(); err == nil { + if _, err := citer.Next(); err == nil { + // Repo contains commits, nothing to do + return nil + } + } + return bgr.AddNewVersion() +} + +func (bgr *BG3Repo) RetrieveGOGalaxy() error { + var err error + bgr.gog, err = gog.RetrieveGOGInfo("1456460669") + if err != nil { + return err + } + return nil +} + +func (bgr *BG3Repo) AddNewVersion() error { + var ( + version string + changelog gog.Change + err error + ) + version = getPEFileVersion(filepath.Join(bgr.BG3Path, "bin/bg3.exe")) + err = bgr.RetrieveGOGalaxy() + if err != nil { + return err + } + changelog, err = gog.ParseChangelog(bgr.gog.Changelog, bgr.gog.Title) + if err != nil { + return err + } + versionChanges := changelog.FindChange(version) + if versionChanges == nil { + return fmt.Errorf("no version information found for %q", version) + } + + // TODO: Replace with custom errors + t, err := bgr.git.Worktree() + if err != nil { + return err + } + var stat git.Status + stat, err = t.Status() + if err != nil { + return err + } + if !stat.IsClean() { + return errors.New("git working directory must be clean before adding a new version, please save your work:\n" + stat.String()) + } + if _, err := t.Filesystem.Stat(".gitignore"); err != nil { + gitignore, err := t.Filesystem.Create(".gitignore") + if err != nil { + return err + } + _, err = gitignore.Write([]byte(ignore)) + if err != nil { + return err + } + gitignore.Close() + } + + t.Add(".gitignore") + + wg := &sync.WaitGroup{} + mut := &sync.Mutex{} + errs := new(multierror.Error) + paths := []string{} + + err = filepath.Walk(filepath.Join(bgr.BG3Path, "Data"), func(path string, info os.FileInfo, err error) error { + rpath := strings.TrimPrefix(path, filepath.Join(bgr.BG3Path, "Data")) + fmt.Println("bg3 ", filepath.Join(bgr.BG3Path, "Data")) + fmt.Println("path ", path) + fmt.Println("rpath", rpath) + repoPath := filepath.Join(filepath.Dir(rpath), strings.TrimSuffix(filepath.Base(rpath), filepath.Ext(rpath))+strings.ReplaceAll(filepath.Ext(rpath), ".", "_")) + fmt.Println("repopath", repoPath) + if filepath.Ext(info.Name()) != ".pak" || info.IsDir() || strings.Contains(info.Name(), "Textures") { + return nil + } + + err = t.Filesystem.MkdirAll(repoPath, 0o660) + if err != nil { + return err + } + paths = append(paths, repoPath) + divine := exec.Command(bgr.divinePath, "-g", "bg3", "-s", path, "-d", filepath.Join(bgr.gitPath, repoPath), "-a", "extract-package") + var buf bytes.Buffer + divine.Stderr = &buf + divine.Stdout = &buf + err = divine.Run() + if err != nil { + return fmt.Errorf("an error occurred running divine.exe: %s", buf.String()) + } + wg.Add(1) + go func() { + if err := convert("-w", "-r", filepath.Join(bgr.gitPath, repoPath)); err != nil { + mut.Lock() + errs = multierror.Append(errs, err) + mut.Unlock() + } + wg.Done() + }() + return nil + }) + if err != nil { + return err + } + wg.Wait() + + if errs.ErrorOrNil() != nil { + return nil + } + for _, v := range paths { + _, err = t.Add(v) + if err != nil { + return err + } + } + _, err = t.Commit(strings.Replace(versionChanges.String(), versionChanges.Title, versionChanges.Title+"\n", 1), nil) + if err != nil { + return err + } + return nil } func preRequisites() (BG3Path, divinePath string) { diff --git a/cmd/lsconvert/main.go b/cmd/lsconvert/main.go index 8d12a73..c396871 100644 --- a/cmd/lsconvert/main.go +++ b/cmd/lsconvert/main.go @@ -1,15 +1,43 @@ package main -import "flag" +import ( + "fmt" + "os" +) func main() { - flag.Parse() - switch flag.Arg(0) { + // flag.Parse() + arg := "" + if len(os.Args) > 1 { + arg = os.Args[1] + } + switch arg { case "convert": - convert(flag.Args()[1:]) + err := convert(os.Args[1:]...) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } case "init": - initRepository(flag.Args()[1:]) + p := "." + if len(os.Args) > 2 { + p = os.Args[2] + } + repo, err := openRepository(p) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + err = repo.Init() + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } default: - convert(flag.Args()) + err := convert(os.Args[1:]...) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } } } diff --git a/go.mod b/go.mod index de77c1e..05b0e2c 100644 --- a/go.mod +++ b/go.mod @@ -4,10 +4,14 @@ go 1.15 replace github.com/pierrec/lz4/v4 v4.1.3 => ./third_party/lz4 +replace github.com/jdferrell3/peinfo-go v0.0.0-20191128021257-cff0d8506fb1 => ./third_party/peinfo-go + require ( github.com/go-git/go-git/v5 v5.2.0 github.com/go-kit/kit v0.10.0 github.com/google/uuid v1.1.4 + github.com/hashicorp/go-multierror v1.1.0 + github.com/jdferrell3/peinfo-go v0.0.0-20191128021257-cff0d8506fb1 github.com/kr/pretty v0.2.1 github.com/pierrec/lz4/v4 v4.1.3 golang.org/x/net v0.0.0-20200301022130-244492dfa37a diff --git a/go.sum b/go.sum index 3755dce..6fe2e1f 100644 --- a/go.sum +++ b/go.sum @@ -121,11 +121,14 @@ github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgf github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI= +github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= @@ -300,6 +303,8 @@ golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM= golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY= +golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -354,10 +359,13 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210108172913-0df2131ae363 h1:wHn06sgWHMO1VsQ8F+KzDJx/JzqfsNLnc+oEi07qD7s= golang.org/x/sys v0.0.0-20210108172913-0df2131ae363/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221 h1:/ZHdbVpdR/jk3g30/d4yUL0JU9kksj8+F/bnQUVLGDM= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= diff --git a/third_party/peinfo-go/.gitignore b/third_party/peinfo-go/.gitignore new file mode 100644 index 0000000..60a6193 --- /dev/null +++ b/third_party/peinfo-go/.gitignore @@ -0,0 +1,15 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Mac files +.DS_Store diff --git a/third_party/peinfo-go/LICENSE b/third_party/peinfo-go/LICENSE new file mode 100644 index 0000000..aa91541 --- /dev/null +++ b/third_party/peinfo-go/LICENSE @@ -0,0 +1,49 @@ +MIT License + +Copyright (c) 2019 John Ferrell + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +------------------------------------------------------------------------------ +peinfo/revoked.go +Copyright (c) 2014 CloudFlare Inc. +Copyright (c) 2019 John Ferrell + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/third_party/peinfo-go/README.md b/third_party/peinfo-go/README.md new file mode 100644 index 0000000..c2aeafc --- /dev/null +++ b/third_party/peinfo-go/README.md @@ -0,0 +1,62 @@ +# peinfo-go + +This is a PE (Portable Executable) parser written in GoLang. I wanted to learn more about the PE format, specifically how the certificates were stored. What better way is there than to write some code? + +_This is a work in progress and will continue to change._ + +This leverages the `debug/pe` package for parsing of the common headers/sections. + +Current state: +- Displays some PE details +- Validates certificate, verifies certificate chain, checks against CRL +- Parses Version Info struct +- Displays imports + +TODO: +- ~~Actually Parse Version Info struct (currently displayed as raw binary)~~ +- Re-write function for finding Version Info (currently written so I could better understand the structure) +- ~~Custom certificate stores~~ + +## Example +``` +[user:~/peinfo-go\ > go run cmd/main.go -certdir ~/RootCerts -versioninfo ~/Downloads/PsExec.exe +type: pe32 +TimeDateStamp: 2016-06-28 18:43:09 +0000 UTC +Characteristics: [Executable 32bit] +Subsystem: IMAGE_SUBSYSTEM_WINDOWS_CUI + +Cert: + subject: CN=Microsoft Corporation,OU=MOPR,O=Microsoft Corporation,L=Redmond,ST=Washington,C=US + issuer: CN=Microsoft Code Signing PCA,O=Microsoft Corporation,L=Redmond,ST=Washington,C=US + not before: 2015-06-04 17:42:45 +0000 UTC + not after: 2016-09-04 17:42:45 +0000 UTC + CRL: [http://crl.microsoft.com/pki/crl/products/MicCodSigPCA_08-31-2010.crl] + verified: true (chain expired: true) + +Version Info: + BuildDate : + BuildVersion : + Comments : + CompanyName : Sysinternals - www.sysinternals.com + Copyright : + FileDescription : Execute processes remotely + FileVersion : 2.2 + InternalName : PsExec + LegalCopyright : Copyright (C) 2001-2016 Mark Russinovich + LegalTrademarks : + OriginalFilename : psexec.c + PrivateBuild : + ProductName : Sysinternals PsExec + ProductVersion : 2.2 + SpecialBuild : + langCharSet : 040904b0h$ + ``` + +## References +- https://golang.org/pkg/debug/pe/ +- http://www.pelib.com/resources/luevel.txt +- https://github.com/exiftool/exiftool/blob/master/lib/Image/ExifTool/EXE.pm +- https://github.com/deptofdefense/SalSA/blob/master/pe.py +- https://docs.microsoft.com/en-us/windows/desktop/debug/pe-format#resource-directory-entries +- https://github.com/quarkslab/dreamboot/blob/31e155b06802dce94367c38ea93316f7cb86cb15/QuarksUBootkit/PeCoffLib.c +- https://docs.microsoft.com/en-us/windows/desktop/Debug/pe-format#the-attribute-certificate-table-image-only diff --git a/third_party/peinfo-go/go.mod b/third_party/peinfo-go/go.mod new file mode 100644 index 0000000..811fd01 --- /dev/null +++ b/third_party/peinfo-go/go.mod @@ -0,0 +1,7 @@ +module github.com/jdferrell3/peinfo-go + +require ( + golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad +) + +go 1.13 diff --git a/third_party/peinfo-go/go.sum b/third_party/peinfo-go/go.sum new file mode 100644 index 0000000..d8386d4 --- /dev/null +++ b/third_party/peinfo-go/go.sum @@ -0,0 +1,10 @@ +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY= +golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/third_party/peinfo-go/peinfo/consts.go b/third_party/peinfo-go/peinfo/consts.go new file mode 100644 index 0000000..f626b65 --- /dev/null +++ b/third_party/peinfo-go/peinfo/consts.go @@ -0,0 +1,17 @@ +package peinfo + +var FileOS = map[uint32]string{ + 0x00001: "Win16", + 0x00002: "PM-16", + 0x00003: "PM-32", + 0x00004: "Win32", + 0x10000: "DOS", + 0x20000: "OS/2 16-bit", + 0x30000: "OS/2 32-bit", + 0x40000: "Windows NT", + 0x10001: "Windows 16-bit", + 0x10004: "Windows 32-bit", + 0x20002: "OS/2 16-bit PM-16", + 0x30003: "OS/2 32-bit PM-32", + 0x40004: "Windows NT 32-bit", +} diff --git a/third_party/peinfo-go/peinfo/initialize.go b/third_party/peinfo-go/peinfo/initialize.go new file mode 100644 index 0000000..d9633c4 --- /dev/null +++ b/third_party/peinfo-go/peinfo/initialize.go @@ -0,0 +1,31 @@ +package peinfo + +import ( + "debug/pe" + "os" + "path/filepath" +) + +// Initialize returns the config for execution +func Initialize(filePath string, verbose bool, rootCertDir string, extractCert bool) (ConfigT, error) { + fh, err := os.Open(filePath) + if nil != err { + return ConfigT{}, err + } + + tempPE, err := pe.NewFile(fh) + if nil != err { + return ConfigT{}, err + } + + file := ConfigT{ + FileName: filepath.Base(filePath), + OSFile: fh, + PEFile: tempPE, + Verbose: verbose, + ExtractCert: extractCert, + RootCertDir: rootCertDir, + } + + return file, nil +} diff --git a/third_party/peinfo-go/peinfo/peinfo.go b/third_party/peinfo-go/peinfo/peinfo.go new file mode 100644 index 0000000..68d4eec --- /dev/null +++ b/third_party/peinfo-go/peinfo/peinfo.go @@ -0,0 +1,124 @@ +package peinfo + +import ( + "debug/pe" + "encoding/binary" + "fmt" + "os" + "time" +) + +// http://www.pelib.com/resources/luevel.txt +// https://github.com/exiftool/exiftool/blob/master/lib/Image/ExifTool/EXE.pm +// https://github.com/deptofdefense/SalSA/blob/master/pe.py +// https://docs.microsoft.com/en-us/windows/desktop/debug/pe-format#resource-directory-entries +// https://github.com/quarkslab/dreamboot/blob/31e155b06802dce94367c38ea93316f7cb86cb15/QuarksUBootkit/PeCoffLib.c +// https://docs.microsoft.com/en-us/windows/desktop/Debug/pe-format#the-attribute-certificate-table-image-only +// https://docs.microsoft.com/en-us/windows/desktop/menurc/vs-versioninfo + +var ( + sizeofOptionalHeader32 = uint16(binary.Size(pe.OptionalHeader32{})) + sizeofOptionalHeader64 = uint16(binary.Size(pe.OptionalHeader64{})) +) + +type ImageDirectoryT struct { + Type int + VirtualAddress uint32 + Size uint32 + ImageBase uint64 +} + +func (cfg *ConfigT) HeaderMagic() uint16 { + pe64 := cfg.PEFile.Machine == pe.IMAGE_FILE_MACHINE_AMD64 + + if pe64 { + return cfg.PEFile.OptionalHeader.(*pe.OptionalHeader64).Magic + } + + return cfg.PEFile.OptionalHeader.(*pe.OptionalHeader32).Magic +} + +func (cfg *ConfigT) GetPEType() string { + t := "pe32" + if cfg.PEFile.Machine == pe.IMAGE_FILE_MACHINE_AMD64 { + t = "pe32+" + } + return t +} + +func (cfg *ConfigT) GetImageSubSystem() string { + pe64 := cfg.PEFile.Machine == pe.IMAGE_FILE_MACHINE_AMD64 + + subsystem := map[uint16]string{ + 0: "IMAGE_SUBSYSTEM_UNKNOWN", + 1: "IMAGE_SUBSYSTEM_NATIVE", + 2: "IMAGE_SUBSYSTEM_WINDOWS_GUI", + 3: "IMAGE_SUBSYSTEM_WINDOWS_CUI", + 4: "IMAGE_SUBSYSTEM_OS2_CUI", + 5: "IMAGE_SUBSYSTEM_POSIX_CUI", + 9: "IMAGE_SUBSYSTEM_WINDOWS_CE_GUI", + 10: "IMAGE_SUBSYSTEM_EFI_APPLICATION", + 11: "IMAGE_SUBSYSTEM_EFI_BOOT_SERVICE_DRIVER", + 12: "IMAGE_SUBSYSTEM_EFI_RUNTIME_DRIVER", + 13: "IMAGE_SUBSYSTEM_EFI_ROM", + 14: "IMAGE_SUBSYSTEM_XBOX", + 15: "IMAGE_SUBSYSTEM_WINDOWS_BOOT_APPLICATION", + } + + if pe64 { + return subsystem[cfg.PEFile.OptionalHeader.(*pe.OptionalHeader64).Subsystem] + } + + return subsystem[cfg.PEFile.OptionalHeader.(*pe.OptionalHeader32).Subsystem] +} + +// GetCharacteristics returns a list of PE characteristics +func (cfg *ConfigT) GetCharacteristics() []string { + characteristics := []string{} + + if (cfg.PEFile.FileHeader.Characteristics & 0x0002) > 1 { + characteristics = append(characteristics, "Executable") + } + + if (cfg.PEFile.FileHeader.Characteristics & 0x0100) > 1 { + characteristics = append(characteristics, "32bit") + } + + if (cfg.PEFile.FileHeader.Characteristics & 0x2000) > 1 { + characteristics = append(characteristics, "DLL") + } + + return characteristics +} + +// GetTimeDateStamp returns the date-time stamp in the PE's header +func (cfg *ConfigT) GetTimeDateStamp() string { + tm := time.Unix(int64(cfg.PEFile.FileHeader.TimeDateStamp), 0) + return fmt.Sprintf("%s", tm.UTC()) +} + +// FindDataDirectory +func (cfg *ConfigT) FindDataDirectory(imageDirectoryEntryType int) (idd ImageDirectoryT) { + pe64 := cfg.PEFile.Machine == pe.IMAGE_FILE_MACHINE_AMD64 + + var dd pe.DataDirectory + if pe64 { + dd = cfg.PEFile.OptionalHeader.(*pe.OptionalHeader64).DataDirectory[imageDirectoryEntryType] + idd.ImageBase = cfg.PEFile.OptionalHeader.(*pe.OptionalHeader64).ImageBase + } else { + dd = cfg.PEFile.OptionalHeader.(*pe.OptionalHeader32).DataDirectory[imageDirectoryEntryType] + idd.ImageBase = uint64(cfg.PEFile.OptionalHeader.(*pe.OptionalHeader32).ImageBase) + } + + idd.VirtualAddress = dd.VirtualAddress + idd.Size = dd.Size + idd.Type = imageDirectoryEntryType + + return idd +} + +// Tell is a wrapper for Seek() +func (cfg *ConfigT) Tell() int64 { + pos, _ := cfg.OSFile.Seek(0, os.SEEK_CUR) + return pos +} diff --git a/third_party/peinfo-go/peinfo/structs.go b/third_party/peinfo-go/peinfo/structs.go new file mode 100644 index 0000000..f60d2e8 --- /dev/null +++ b/third_party/peinfo-go/peinfo/structs.go @@ -0,0 +1,90 @@ +package peinfo + +import ( + "debug/pe" + "os" +) + +type ConfigT struct { + FileName string + OSFile *os.File + PEFile *pe.File + ExtractCert bool + Verbose bool + RootCertDir string +} + +type ResourceDirectoryD struct { + Characteristics uint32 + TimeDateStamp uint32 + MajorVersion uint16 + MinorVersion uint16 + NumberOfNamedEntries uint16 + NumberOfIdEntries uint16 +} + +type CertDetails struct { + Length uint32 + Revision uint16 + CertificateType uint16 + DER []byte +} + +// typedef struct _IMAGE_RESOURCE_DIRECTORY_ENTRY { +// union { +// struct { +// DWORD NameOffset:31; +// DWORD NameIsString:1; +// }; +// DWORD Name; +// WORD Id; +// }; +// union { +// DWORD OffsetToData; +// struct { +// DWORD OffsetToDirectory:31; +// DWORD DataIsDirectory:1; +// }; +// }; +// } IMAGE_RESOURCE_DIRECTORY_ENTRY, *PIMAGE_RESOURCE_DIRECTORY_ENTRY; + +type ResourceDirectoryEntry struct { + Name uint32 + OffsetToData uint32 +} + +type ResourceDirectoryEntryNamed struct { + Name uint32 + OffsetToData uint32 +} + +/* Resource Directory Entry */ +// type ResourceDirectoryEntryT struct { +// ResourceDirectoryEntry ResourceDirectoryEntry +// FileOffset uint32 +// Size uint32 +// DataIsDirectory bool +// } + +type _IMAGE_RESOURCE_DATA_ENTRY struct { + OffsetToData uint32 + Size uint32 + CodePage uint32 + Reserved uint32 +} + +type VS_FIXEDFILEINFO struct { + DwSignature uint32 + DwStrucVersion uint32 + DwFileVersionMS uint32 + DwFileVersionLS uint32 + DwProductVersionMS uint32 + DwProductVersionLS uint32 + DwFileFlagsMask uint32 + DwFileFlags uint32 + DwFileOS uint32 + DwFileType uint32 + DwFileSubtype uint32 + DwFileDateMS uint32 + DwFileDateLS uint32 +} diff --git a/third_party/peinfo-go/peinfo/verinfo.go b/third_party/peinfo-go/peinfo/verinfo.go new file mode 100644 index 0000000..f95fe5f --- /dev/null +++ b/third_party/peinfo-go/peinfo/verinfo.go @@ -0,0 +1,267 @@ +package peinfo + +import ( + "bytes" + "debug/pe" + "encoding/binary" + "encoding/hex" + "fmt" + "os" +) + +const ( + RT_VERSION = 16 +) + +func (cfg *ConfigT) FindVerInfoOffset(fileOffset int64, sectionOffset uint32, sectionVirtualAddress uint32) (verInfoOffset int64, len uint32, err error) { + pos, _ := cfg.OSFile.Seek(fileOffset, os.SEEK_SET) + if pos != fileOffset { + return 0, 0, fmt.Errorf("did not seek to offset") + } + type VerInfoDetailsT struct { + Off uint32 + Len uint32 + D1 uint32 + D2 uint32 + } + var peoff VerInfoDetailsT + err = binary.Read(cfg.OSFile, binary.LittleEndian, &peoff) + if nil != err { + return verInfoOffset, len, err + } + + // $filePos = $off + $$section{Base} - $$section{VirtualAddress}; + verInfoOffset = int64(peoff.Off + sectionOffset - sectionVirtualAddress) + return verInfoOffset, peoff.Len, nil +} + +func (cfg *ConfigT) GetVersionInfo() (vi map[string]string, keys []string, err error) { + vi = map[string]string{ + "BuildDate": "", + "BuildVersion": "", + "Comments": "", + "CompanyName": "", + "Copyright": "", + "FileDescription": "", + "FileVersion": "", + "InternalName": "", + "LegalCopyright": "", + "LegalTrademarks": "", + "OriginalFilename": "", + "PrivateBuild": "", + "ProductName": "", + "ProductVersion": "", + "SpecialBuild": "", + "langCharSet": "", + // "varFileInfo": "", + } + keys = []string{ + "BuildDate", + "BuildVersion", + "Comments", + "CompanyName", + "Copyright", + "FileDescription", + "FileVersion", + "InternalName", + "LegalCopyright", + "LegalTrademarks", + "OriginalFilename", + "PrivateBuild", + "ProductName", + "ProductVersion", + "SpecialBuild", + "langCharSet", + // "varFileInfo" + } + + section := cfg.PEFile.Section(".rsrc") + if section == nil { + return vi, keys, fmt.Errorf("resource section not found") + } + // fmt.Printf("%+v\n", section) + + // Resource + _, err = cfg.OSFile.Seek(int64(0), os.SEEK_SET) + if nil != err { + return vi, keys, err + } + + idd := cfg.FindDataDirectory(pe.IMAGE_DIRECTORY_ENTRY_RESOURCE) + idd.VirtualAddress -= (section.VirtualAddress - section.Offset) + if cfg.Verbose { + fmt.Printf("IMAGE_DIRECTORY_ENTRY_RESOURCE virtual address: %d\n", idd.VirtualAddress) + fmt.Printf("IMAGE_DIRECTORY_ENTRY_RESOURCE size: %d\n", idd.Size) + fmt.Printf("IMAGE_DIRECTORY_ENTRY_RESOURCE image base: %d\n", idd.ImageBase) + } + + pos, err := cfg.OSFile.Seek(int64(idd.VirtualAddress), os.SEEK_SET) + if nil != err { + return vi, keys, err + } + if pos != int64(idd.VirtualAddress) { + fmt.Errorf("did not seek to VirtualAddress") + } + + var table ResourceDirectoryD + err = binary.Read(cfg.OSFile, binary.LittleEndian, &table) + if nil != err { + return vi, keys, err + } + // fmt.Printf("table %+v\n", table) + + x := 0 + for x < int(table.NumberOfNamedEntries+table.NumberOfIdEntries) { + var entry ResourceDirectoryEntry + err = binary.Read(cfg.OSFile, binary.LittleEndian, &entry) + if nil != err { + return vi, keys, err + } + + if entry.Name == RT_VERSION { + // Directory + if (entry.OffsetToData&0x80000000)>>31 == 1 { + new := entry.OffsetToData&0x7fffffff + idd.VirtualAddress + cfg.OSFile.Seek(int64(new), os.SEEK_SET) + + var innerDir ResourceDirectoryD + err = binary.Read(cfg.OSFile, binary.LittleEndian, &innerDir) + if nil != err { + return vi, keys, err + } + // pos := f.Tell() + // fmt.Printf("level 1 innerDir %+v (file offset=%d)\n", innerDir, pos) + + y := 0 + for y < int(innerDir.NumberOfNamedEntries+innerDir.NumberOfIdEntries) { + var entry ResourceDirectoryEntry + err = binary.Read(cfg.OSFile, binary.LittleEndian, &entry) + if nil != err { + return vi, keys, err + } + // pos := f.Tell() + // fmt.Printf("item %d - level 2 buff %s (file offset=%d)\n", y, entry, pos) + + if (entry.OffsetToData&0x80000000)>>31 == 1 { + new := entry.OffsetToData&0x7fffffff + idd.VirtualAddress + // fmt.Printf("level 2 DirStart 0x%x (%d)\n", new, new) + cfg.OSFile.Seek(int64(new), os.SEEK_SET) + } + + var innerDir ResourceDirectoryD + err = binary.Read(cfg.OSFile, binary.LittleEndian, &innerDir) + if nil != err { + return vi, keys, err + } + // pos = f.Tell() + // fmt.Printf("level 3 innerDir %+v (file offset=%d)\n", innerDir, pos) + + z := 0 + for z < int(innerDir.NumberOfNamedEntries+innerDir.NumberOfIdEntries) { + var entry ResourceDirectoryEntry + err = binary.Read(cfg.OSFile, binary.LittleEndian, &entry) + if nil != err { + return vi, keys, err + } + // pos := f.Tell() + // fmt.Printf("item %d - level 3 buff %s (file offset=%d)\n", y, entry, pos) + // fmt.Printf("ver: 0x%x\n", entry.OffsetToData+idd.VirtualAddress) + + // find offset of VS_VERSION_INFO + off := int64(entry.OffsetToData + idd.VirtualAddress) + viPos, viLen, err := cfg.FindVerInfoOffset(off, section.SectionHeader.Offset, section.SectionHeader.VirtualAddress) + if nil != err { + return vi, keys, err + } + // fmt.Printf("VerInfo Struct filePos: 0x%x (%d)\n", viPos, viPos) + + cfg.OSFile.Seek(viPos, os.SEEK_SET) + b := make([]byte, viLen) + err = binary.Read(cfg.OSFile, binary.LittleEndian, &b) + if nil != err { + return vi, keys, err + } + // fmt.Printf("%s\n", b) + + if cfg.Verbose { + fmt.Printf(" %s\n", hex.Dump(b)) + } + + vi, err = parseVersionInfo(b, vi) + if nil != err { + return vi, keys, err + } + return vi, keys, nil + } + y++ + } + } + } + x++ + } + + return vi, keys, fmt.Errorf("no version info found") +} + +func parseVersionInfo(vi []byte, versionInfo map[string]string) (map[string]string, error) { + // Grab everything after "StringFileInfo" + stringFileInfoTemp := bytes.Split(vi, []byte{0x53, 0x0, 0x74, 0x0, 0x72, 0x0, 0x69, 0x0, 0x6e, 0x0, 0x67, 0x0, 0x46, 0x0, 0x69, 0x0, 0x6c, 0x0, 0x65, 0x0, 0x49, 0x0, 0x6e, 0x0, 0x66, 0x0, 0x6f}) + + if len(stringFileInfoTemp) < 2 { + err := fmt.Errorf("can't find StringFileInfo bytes") + return versionInfo, err + } + + stringFileInfo := stringFileInfoTemp[1] + + divide := bytes.Split(stringFileInfo, []byte{0x0, 0x1, 0x0}) + + langCharSet := trimSlice(divide[1]) + versionInfo["langCharSet"] = string(langCharSet) + + // check for slice out of bounds + if len(divide) < 3 { + err := fmt.Errorf("VersionInfo slice too small") + return versionInfo, err + } + + end := len(divide) - 1 + if end < 2 { + err := fmt.Errorf("slice end less than start") + return versionInfo, err + } + + values := divide[2:end] + + // TODO: handle varFileInfo, currently contains binary information which chrome does not display + // varFileInfo := divide[len(divide)-1] + // versionInfo["varFileInfo"] = string(trimSlice(varFileInfo)) + + for _, element := range values { + temp := bytes.Split(element, []byte{0x0, 0x0, 0x0}) + valueInfo := temp[:len(temp)-1] + + if len(valueInfo) > 1 { + name := string(trimSlice(valueInfo[0])) + value := string(trimSlice(valueInfo[1])) + + versionInfo[name] = value + } + } + + return versionInfo, nil +} + +func trimSlice(nonTrimmed []byte) (trimmed []byte) { + for bytes.HasPrefix(nonTrimmed, []byte{0x0}) { + nonTrimmed = nonTrimmed[1:] + } + + for i, val := range nonTrimmed { + if i%2 == 0 && val != 0x0 { + trimmed = append(trimmed, val) + } + } + + return trimmed +}