mirror of
https://github.com/tmpest127/enc_pic_str
synced 2026-06-08 17:51:05 +00:00
497 lines
12 KiB
Go
497 lines
12 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/binary"
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
var (
|
|
mode = flag.String("mode", "single", "Mode: single or batch")
|
|
inputFile = flag.String("i", "", "Input C file(s) - single file for single mode, comma-separated list for batch mode")
|
|
outputFile = flag.String("o", "", "Output C file (single mode) or output directory (batch mode)")
|
|
counter = 0
|
|
)
|
|
|
|
func main() {
|
|
flag.Usage = func() {
|
|
fmt.Fprintf(os.Stderr, "String Obfuscator - Obfuscate C string literals\n\n")
|
|
fmt.Fprintf(os.Stderr, "Usage:\n")
|
|
fmt.Fprintf(os.Stderr, " Single file mode:\n")
|
|
fmt.Fprintf(os.Stderr, " %s -mode single -i <input.c> -o <output.c>\n\n", os.Args[0])
|
|
fmt.Fprintf(os.Stderr, " Batch mode:\n")
|
|
fmt.Fprintf(os.Stderr, " %s -mode batch -i <file1.c,file2.c,file3.c> -o <output_dir>\n\n", os.Args[0])
|
|
fmt.Fprintf(os.Stderr, "Flags:\n")
|
|
flag.PrintDefaults()
|
|
}
|
|
|
|
flag.Parse()
|
|
|
|
switch *mode {
|
|
case "single":
|
|
processSingleMode()
|
|
case "batch":
|
|
processBatchMode()
|
|
default:
|
|
fmt.Fprintf(os.Stderr, "Error: Invalid mode '%s'. Use 'single' or 'batch'\n", *mode)
|
|
flag.Usage()
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func processSingleMode() {
|
|
if *inputFile == "" || *outputFile == "" {
|
|
fmt.Fprintln(os.Stderr, "Error: Single mode requires both -i and -o flags")
|
|
flag.Usage()
|
|
os.Exit(1)
|
|
}
|
|
|
|
counter = 0
|
|
data, err := os.ReadFile(*inputFile)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error reading input file: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
content := string(data)
|
|
obfuscated := obfuscateStrings(content)
|
|
|
|
err = os.WriteFile(*outputFile, []byte(obfuscated), 0644)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error writing output file: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
fmt.Printf("[+] Obfuscated %d strings in %s -> %s\n", counter, *inputFile, *outputFile)
|
|
}
|
|
|
|
func processBatchMode() {
|
|
if *inputFile == "" || *outputFile == "" {
|
|
fmt.Fprintln(os.Stderr, "Error: Batch mode requires both -i and -o flags")
|
|
flag.Usage()
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Parse comma-separated input files
|
|
inputFiles := strings.Split(*inputFile, ",")
|
|
for i := range inputFiles {
|
|
inputFiles[i] = strings.TrimSpace(inputFiles[i])
|
|
}
|
|
|
|
if len(inputFiles) == 0 {
|
|
fmt.Fprintln(os.Stderr, "Error: Batch mode requires at least one input file")
|
|
flag.Usage()
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Check if output path exists and is a directory, or can be created
|
|
info, err := os.Stat(*outputFile)
|
|
if err == nil {
|
|
// Path exists, check if it's a directory
|
|
if !info.IsDir() {
|
|
fmt.Fprintf(os.Stderr, "Error: Output path '%s' exists but is not a directory\n", *outputFile)
|
|
os.Exit(1)
|
|
}
|
|
} else if os.IsNotExist(err) {
|
|
// Directory doesn't exist, create it
|
|
err = os.MkdirAll(*outputFile, 0755)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error creating output directory: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
} else {
|
|
// Some other error
|
|
fmt.Fprintf(os.Stderr, "Error checking output directory: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
totalStrings := 0
|
|
successCount := 0
|
|
|
|
for _, inputPath := range inputFiles {
|
|
counter = 0
|
|
|
|
data, err := os.ReadFile(inputPath)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "[!] Error reading %s: %v\n", inputPath, err)
|
|
continue
|
|
}
|
|
|
|
content := string(data)
|
|
obfuscated := obfuscateStrings(content)
|
|
|
|
// Generate output filename preserving the original name
|
|
outputPath := filepath.Join(*outputFile, filepath.Base(inputPath))
|
|
|
|
err = os.WriteFile(outputPath, []byte(obfuscated), 0644)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "[!] Error writing %s: %v\n", outputPath, err)
|
|
continue
|
|
}
|
|
|
|
fmt.Printf("[+] %s -> %s (%d strings)\n", inputPath, outputPath, counter)
|
|
totalStrings += counter
|
|
successCount++
|
|
}
|
|
|
|
fmt.Printf("\n[+] Batch complete: %d/%d files processed, %d total strings obfuscated\n",
|
|
successCount, len(inputFiles), totalStrings)
|
|
}
|
|
|
|
func obfuscateStrings(content string) string {
|
|
// Inject memcpy implementation after includes
|
|
memcpyImpl := `
|
|
// Provide memcpy implementation for -nostdlib builds
|
|
#if !defined(memcpy) && !defined(__memcpy_defined)
|
|
#define __memcpy_defined
|
|
|
|
__attribute__((weak))
|
|
void *memcpy(void *dst, const void *src, size_t n) {
|
|
unsigned char *d = (unsigned char *)dst;
|
|
const unsigned char *s = (const unsigned char *)src;
|
|
while (n--) {
|
|
*d++ = *s++;
|
|
}
|
|
return dst;
|
|
}
|
|
#endif
|
|
|
|
`
|
|
|
|
// Find the last #include or the first non-preprocessor line
|
|
lines := strings.Split(content, "\n")
|
|
insertIdx := 0
|
|
for i, line := range lines {
|
|
trimmed := strings.TrimSpace(line)
|
|
if strings.HasPrefix(trimmed, "#include") {
|
|
insertIdx = i + 1
|
|
} else if insertIdx > 0 && !strings.HasPrefix(trimmed, "#") && trimmed != "" && !strings.HasPrefix(trimmed, "//") {
|
|
break
|
|
}
|
|
}
|
|
|
|
// Insert memcpy implementation
|
|
if insertIdx > 0 && insertIdx < len(lines) {
|
|
before := strings.Join(lines[:insertIdx], "\n")
|
|
after := strings.Join(lines[insertIdx:], "\n")
|
|
content = before + "\n" + memcpyImpl + after
|
|
} else {
|
|
content = memcpyImpl + content
|
|
}
|
|
|
|
// Regex to match pic_str("...") or pic_str(L"...")
|
|
// Updated regex to capture content including escape sequences
|
|
reChar := regexp.MustCompile(`pic_str\s*\(\s*"((?:[^"\\]|\\.)*)"\s*\)`)
|
|
reWide := regexp.MustCompile(`pic_str\s*\(\s*L"((?:[^"\\]|\\.)*)"\s*\)`)
|
|
|
|
// Replace wide strings first
|
|
content = reWide.ReplaceAllStringFunc(content, func(match string) string {
|
|
submatches := reWide.FindStringSubmatch(match)
|
|
if len(submatches) > 1 {
|
|
return obfuscateWideString(submatches[1])
|
|
}
|
|
return match
|
|
})
|
|
|
|
// Then replace regular strings
|
|
content = reChar.ReplaceAllStringFunc(content, func(match string) string {
|
|
submatches := reChar.FindStringSubmatch(match)
|
|
if len(submatches) > 1 {
|
|
return obfuscateCharString(submatches[1])
|
|
}
|
|
return match
|
|
})
|
|
|
|
return content
|
|
}
|
|
|
|
// unescapeString converts C escape sequences to actual bytes
|
|
// It properly handles Unicode escapes (\u and \U) by converting them to UTF-8
|
|
func unescapeString(s string) []byte {
|
|
result := make([]byte, 0, len(s))
|
|
i := 0
|
|
for i < len(s) {
|
|
if s[i] == '\\' && i+1 < len(s) {
|
|
switch s[i+1] {
|
|
case 'n':
|
|
result = append(result, '\n')
|
|
i += 2
|
|
case 'r':
|
|
result = append(result, '\r')
|
|
i += 2
|
|
case 't':
|
|
result = append(result, '\t')
|
|
i += 2
|
|
case '\\':
|
|
result = append(result, '\\')
|
|
i += 2
|
|
case '"':
|
|
result = append(result, '"')
|
|
i += 2
|
|
case '\'':
|
|
result = append(result, '\'')
|
|
i += 2
|
|
case '0':
|
|
result = append(result, 0)
|
|
i += 2
|
|
case 'a':
|
|
result = append(result, '\a')
|
|
i += 2
|
|
case 'b':
|
|
result = append(result, '\b')
|
|
i += 2
|
|
case 'f':
|
|
result = append(result, '\f')
|
|
i += 2
|
|
case 'v':
|
|
result = append(result, '\v')
|
|
i += 2
|
|
case 'x':
|
|
// Hex escape sequence \xHH
|
|
if i+3 < len(s) {
|
|
var val byte
|
|
fmt.Sscanf(s[i+2:i+4], "%02x", &val)
|
|
result = append(result, val)
|
|
i += 4
|
|
} else {
|
|
result = append(result, s[i])
|
|
i++
|
|
}
|
|
case 'u':
|
|
// Unicode escape sequence \uHHHH (4 hex digits)
|
|
if i+5 < len(s) {
|
|
var val uint32
|
|
_, err := fmt.Sscanf(s[i+2:i+6], "%04x", &val)
|
|
if err == nil {
|
|
// Convert to UTF-8
|
|
utf8Bytes := []byte(string(rune(val)))
|
|
result = append(result, utf8Bytes...)
|
|
i += 6
|
|
} else {
|
|
result = append(result, s[i])
|
|
i++
|
|
}
|
|
} else {
|
|
result = append(result, s[i])
|
|
i++
|
|
}
|
|
case 'U':
|
|
// Unicode escape sequence \UHHHHHHHH (8 hex digits)
|
|
if i+9 < len(s) {
|
|
var val uint32
|
|
_, err := fmt.Sscanf(s[i+2:i+10], "%08x", &val)
|
|
if err == nil {
|
|
// Convert to UTF-8
|
|
utf8Bytes := []byte(string(rune(val)))
|
|
result = append(result, utf8Bytes...)
|
|
i += 10
|
|
} else {
|
|
result = append(result, s[i])
|
|
i++
|
|
}
|
|
} else {
|
|
result = append(result, s[i])
|
|
i++
|
|
}
|
|
default:
|
|
// Unknown escape, keep the backslash
|
|
result = append(result, s[i])
|
|
i++
|
|
}
|
|
} else {
|
|
result = append(result, s[i])
|
|
i++
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func randomKey() uint64 {
|
|
buf := make([]byte, 8)
|
|
if _, err := rand.Read(buf); err != nil {
|
|
panic(err)
|
|
}
|
|
return binary.LittleEndian.Uint64(buf)
|
|
}
|
|
|
|
func obfuscateCharString(s string) string {
|
|
counter++
|
|
// Unescape the string first
|
|
plaintext := unescapeString(s)
|
|
length := len(plaintext)
|
|
|
|
// Generate random 64-bit XOR key
|
|
key := randomKey()
|
|
|
|
// XOR encrypt
|
|
encrypted := make([]byte, length)
|
|
for i := 0; i < length; i++ {
|
|
keyByte := byte((key >> ((i % 8) * 8)) & 0xFF)
|
|
encrypted[i] = plaintext[i] ^ keyByte
|
|
}
|
|
|
|
// Generate unique variable name using counter
|
|
varName := fmt.Sprintf("_str_struct_%d", counter)
|
|
instanceName := fmt.Sprintf("_str_inst_%d", counter)
|
|
|
|
// Build byte array
|
|
byteArray := ""
|
|
for i, b := range encrypted {
|
|
if i > 0 {
|
|
byteArray += ", "
|
|
}
|
|
byteArray += fmt.Sprintf("0x%02x", b)
|
|
}
|
|
|
|
// Generate the inline assembly obfuscation with multiline formatting
|
|
code := fmt.Sprintf(`((const char*)(({
|
|
struct %s { char buf[%d]; } %s;
|
|
unsigned char *_data_ptr;
|
|
volatile uint64_t _key_val;
|
|
volatile size_t _len = %d;
|
|
__asm__ volatile(
|
|
"jmp skip_str_%%=\n"
|
|
"str_data_%%=:\n"
|
|
".byte %s\n"
|
|
"str_key_%%=:\n"
|
|
".quad 0x%016x\n"
|
|
"skip_str_%%=:\n"
|
|
"lea str_data_%%=(%%%%rip), %%0\n"
|
|
"movq str_key_%%=(%%%%rip), %%1\n"
|
|
: "=r"(_data_ptr), "=r"(_key_val) : :
|
|
);
|
|
for (volatile size_t _i = 0; _i < _len; _i++) {
|
|
volatile unsigned char _key_byte = (_key_val >> ((_i %% 8) * 8)) & 0xFF;
|
|
%s.buf[_i] = _data_ptr[_i] ^ _key_byte;
|
|
}
|
|
%s.buf[%d] = 0;
|
|
%s;
|
|
}).buf))`,
|
|
varName,
|
|
length+1,
|
|
instanceName,
|
|
length,
|
|
byteArray,
|
|
key,
|
|
instanceName,
|
|
instanceName,
|
|
length,
|
|
instanceName,
|
|
)
|
|
|
|
return code
|
|
}
|
|
|
|
func obfuscateWideString(s string) string {
|
|
counter++
|
|
// Unescape the string first - this converts \uXXXX to UTF-8 bytes
|
|
unescaped := unescapeString(s)
|
|
|
|
// Convert UTF-8 bytes to UTF-16LE (wide char)
|
|
// First convert bytes to string, then to runes, then to UTF-16LE
|
|
utf8String := string(unescaped)
|
|
wideBytes := toUTF16LE(utf8String)
|
|
length := len(wideBytes)
|
|
|
|
// Generate random 64-bit XOR key
|
|
key := randomKey()
|
|
|
|
// XOR encrypt
|
|
encrypted := make([]byte, length)
|
|
for i := 0; i < length; i++ {
|
|
keyByte := byte((key >> ((i % 8) * 8)) & 0xFF)
|
|
encrypted[i] = wideBytes[i] ^ keyByte
|
|
}
|
|
|
|
// Generate unique variable name using counter
|
|
varName := fmt.Sprintf("_wstr_struct_%d", counter)
|
|
instanceName := fmt.Sprintf("_wstr_inst_%d", counter)
|
|
|
|
// Build byte array
|
|
byteArray := ""
|
|
for i, b := range encrypted {
|
|
if i > 0 {
|
|
byteArray += ", "
|
|
}
|
|
byteArray += fmt.Sprintf("0x%02x", b)
|
|
}
|
|
|
|
// Generate the inline assembly obfuscation with multiline formatting (wide string termination - 2 null bytes)
|
|
code := fmt.Sprintf(`((const wchar_t*)(({
|
|
struct %s { char buf[%d]; } %s;
|
|
unsigned char *_data_ptr;
|
|
volatile uint64_t _key_val;
|
|
volatile size_t _len = %d;
|
|
__asm__ volatile(
|
|
"jmp skip_str_%%=\n"
|
|
"str_data_%%=:\n"
|
|
".byte %s\n"
|
|
"str_key_%%=:\n"
|
|
".quad 0x%016x\n"
|
|
"skip_str_%%=:\n"
|
|
"lea str_data_%%=(%%%%rip), %%0\n"
|
|
"movq str_key_%%=(%%%%rip), %%1\n"
|
|
: "=r"(_data_ptr), "=r"(_key_val) : :
|
|
);
|
|
for (volatile size_t _i = 0; _i < _len; _i++) {
|
|
volatile unsigned char _key_byte = (_key_val >> ((_i %% 8) * 8)) & 0xFF;
|
|
%s.buf[_i] = _data_ptr[_i] ^ _key_byte;
|
|
}
|
|
%s.buf[%d] = 0;
|
|
%s.buf[%d] = 0;
|
|
%s;
|
|
}).buf))`,
|
|
varName,
|
|
length+2,
|
|
instanceName,
|
|
length,
|
|
byteArray,
|
|
key,
|
|
instanceName,
|
|
instanceName,
|
|
length,
|
|
instanceName,
|
|
length+1,
|
|
instanceName,
|
|
)
|
|
|
|
return code
|
|
}
|
|
|
|
// toUTF16LE converts a UTF-8 string to UTF-16LE bytes
|
|
// This properly handles all Unicode characters including those outside BMP
|
|
func toUTF16LE(s string) []byte {
|
|
runes := []rune(s)
|
|
result := make([]byte, 0, len(runes)*2)
|
|
|
|
for _, r := range runes {
|
|
if r <= 0xFFFF {
|
|
// Basic Multilingual Plane (BMP) - single UTF-16 code unit
|
|
result = append(result, byte(r&0xFF))
|
|
result = append(result, byte((r>>8)&0xFF))
|
|
} else {
|
|
// Supplementary Planes - surrogate pair needed
|
|
// Subtract 0x10000 and split into high/low surrogates
|
|
r -= 0x10000
|
|
high := 0xD800 + ((r >> 10) & 0x3FF)
|
|
low := 0xDC00 + (r & 0x3FF)
|
|
|
|
// Write high surrogate
|
|
result = append(result, byte(high&0xFF))
|
|
result = append(result, byte((high>>8)&0xFF))
|
|
|
|
// Write low surrogate
|
|
result = append(result, byte(low&0xFF))
|
|
result = append(result, byte((low>>8)&0xFF))
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|