Files
tmpest127-enc_pic_str/obfuscator/main.go
T
2026-01-29 14:33:44 +04:00

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
}