mitm/main.go
2025-09-28 22:17:24 +08:00

781 lines
21 KiB
Go

package main
import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"flag"
"fmt"
"io"
"log"
"net"
"net/http"
"net/url"
"os"
"os/exec"
"os/signal"
"path/filepath"
"runtime"
"strings"
"syscall"
"time"
"github.com/elazarl/goproxy"
"golang.org/x/sys/windows/registry"
)
type Config struct {
DomainsOfInterest []string `toml:"domains_of_interest"`
DictFile string `toml:"dict_file"`
Proxy struct {
Port int `toml:"port"`
} `toml:"proxy"`
Dump struct {
OutputDir string `toml:"output_dir"`
DOIDir string `toml:"DOI_dir"`
} `toml:"dump"`
ASR struct {
ReplacePercentage int `toml:"replace_percentage"`
} `toml:"asr"`
}
type UserData struct {
RequestBody []byte
ModifiedBody []byte
}
type ProxyServer struct {
config *Config
tlsConfig *tls.Config
proxy *goproxy.ProxyHttpServer
server *http.Server
originalProxy string
verbose bool
quiet bool
}
func main() {
// Parse command line flags
var testConnectivity = flag.Bool("test", false, "Test proxy connectivity")
var verbose = flag.Bool("v", false, "Enable verbose mode - dump all traffic instead of only modified requests/responses")
var debugMode = flag.Bool("d", false, "Debug mode - dump modified requests/responses")
flag.Parse()
// Set console to UTF-8 on Windows to prevent garbled text
if runtime.GOOS == "windows" {
setConsoleUTF8()
}
fmt.Println("MITM Proxy Server v1.5")
// Load configuration
config, err := loadConfig("config.toml")
if err != nil {
log.Fatalf("Failed to load configuration: %v", err)
}
log.Println(config.String())
fmt.Printf("Reading dictionary from %s...\n", config.DictFile)
dict, err := NewChineseDict(config.DictFile)
if err != nil {
log.Fatalf("Failed to load dictionary: %v", err)
}
Dict = dict
fmt.Printf("Dictionary loaded successfully, size=%d\n", Dict.GetCharacterCount())
fmt.Println("Starting MITM proxy server...")
// Create output directories
if err := os.MkdirAll(config.Dump.OutputDir, 0755); err != nil {
log.Fatalf("Failed to create output directory: %v", err)
}
if config.Dump.DOIDir != "" {
if err := os.MkdirAll(config.Dump.DOIDir, 0755); err != nil {
log.Fatalf("Failed to create DOI directory: %v", err)
}
}
// Create proxy server
proxy, err := NewProxyServer(config, *verbose, *debugMode)
if err != nil {
log.Fatalf("Failed to create proxy server: %v", err)
}
// Install CA certificate
if err := proxy.installCACert(); err != nil {
log.Printf("Failed to install CA certificate: %v", err)
} else {
fmt.Println("CA certificate installed successfully")
}
// Set system proxy
if err := proxy.setSystemProxy(); err != nil {
log.Fatalf("Failed to set system proxy: %v", err)
}
fmt.Printf("System proxy set to 127.0.0.1:%d\n", config.Proxy.Port)
// Verify proxy settings were applied
if err := proxy.verifyProxySettings(); err != nil {
fmt.Printf("Warning: Could not verify proxy settings: %v\n", err)
} else {
fmt.Printf("Proxy settings verified in Windows registry\n")
}
// Show current proxy configuration for debugging
proxy.showProxyConfiguration()
// Start proxy server
serverStarted := make(chan error, 1)
go func() {
fmt.Printf("Starting proxy server on port %d...\n", config.Proxy.Port)
if err := proxy.Start(); err != nil && err != http.ErrServerClosed {
serverStarted <- err
return
}
serverStarted <- nil
}()
// Wait for server to start or fail
fmt.Printf("Waiting for server to start...\n")
select {
case err := <-serverStarted:
if err != nil {
log.Fatalf("Failed to start proxy server: %v", err)
}
fmt.Printf("Proxy server successfully started on port %d\n", config.Proxy.Port)
case <-time.After(5 * time.Second):
fmt.Printf("Proxy server appears to be starting (no immediate errors)\n")
}
// Run full connectivity test if requested
if *testConnectivity {
proxyAddr := fmt.Sprintf("127.0.0.1:%d", config.Proxy.Port)
testProxyConnectivity(proxyAddr)
}
// Wait for interrupt signal
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan
fmt.Println("\nShutting down proxy server...")
proxy.Shutdown()
fmt.Println("Proxy server closed")
}
func loadConfig(filename string) (*Config, error) {
return parseConfig(filename)
}
func NewProxyServer(config *Config, verbose bool, debugMode bool) (*ProxyServer, error) {
// Load hardcoded P12 certificate for MITM
tlsConfig, err := loadHardcodedCertificate()
if err != nil {
return nil, fmt.Errorf("failed to load certificate: %v", err)
}
// Create goproxy instance
goProxy := goproxy.NewProxyHttpServer()
goProxy.Verbose = verbose
ps := &ProxyServer{
config: config,
tlsConfig: tlsConfig,
proxy: goProxy,
verbose: verbose,
quiet: !debugMode,
}
// Configure MITM for HTTPS traffic
ps.setupMITM()
// Setup request/response handlers
ps.setupHandlers()
// Create HTTP server
ps.server = &http.Server{
Addr: fmt.Sprintf(":%d", config.Proxy.Port),
Handler: ps.proxy,
ErrorLog: log.New(os.Stdout, "GOPROXY-SERVER: ", log.LstdFlags),
}
return ps, nil
}
// setupMITM configures HTTPS MITM using goproxy
func (p *ProxyServer) setupMITM() {
// Set the CA certificate for MITM
if len(p.tlsConfig.Certificates) > 0 {
cert := p.tlsConfig.Certificates[0]
goproxy.GoproxyCa = cert
goproxy.OkConnect = &goproxy.ConnectAction{Action: goproxy.ConnectMitm, TLSConfig: goproxy.TLSConfigFromCA(&cert)}
}
// Enable MITM for all HTTPS connections
p.proxy.OnRequest().HandleConnect(goproxy.AlwaysMitm)
}
// setupHandlers configures request/response handlers for logging and filtering
func (p *ProxyServer) setupHandlers() {
// Add health check endpoint
p.proxy.OnRequest().DoFunc(func(r *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) {
if r.Host == "127.0.0.1:"+fmt.Sprintf("%d", p.config.Proxy.Port) && r.URL.Path == "/proxy-health" {
return r, goproxy.NewResponse(r, goproxy.ContentTypeText, http.StatusOK, "MITM Proxy is running")
}
return r, nil
})
// Log all HTTP requests and capture request body
p.proxy.OnRequest().DoFunc(func(r *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) {
if !p.isDomainOfInterest(r.Host) && p.quiet {
return r, nil
}
// Read request body once and recreate it for both dumping and forwarding
if r.Body != nil {
reqBody, err := io.ReadAll(r.Body)
if err != nil {
log.Printf("Failed to read request body: %v", err)
return r, nil
}
r.Body.Close()
var newReqBody []byte = nil
if p.isDomainOfInterest(r.Host) {
newReqBody, err = asrResultObfuscate(r, reqBody, p.config.ASR.ReplacePercentage)
if err != nil && err.Error() != "not an asr request" {
log.Printf("Failed to obfuscate request body: %v", err)
}
if p.quiet && err == nil {
log.Println("[INFO] ASR Request Body Modified")
}
}
if newReqBody != nil {
r.Body = io.NopCloser(bytes.NewReader(newReqBody))
r.ContentLength = int64(len(newReqBody))
} else {
// Recreate the request body so it can be forwarded to the server
r.Body = io.NopCloser(bytes.NewReader(reqBody))
r.ContentLength = int64(len(reqBody))
}
// Store request body in context for later use in response handler
if len(reqBody) > 0 {
ctx.UserData = UserData{
RequestBody: reqBody,
ModifiedBody: newReqBody,
}
}
}
return r, nil
})
// Log all HTTP responses and dump traffic
p.proxy.OnResponse().DoFunc(func(r *http.Response, ctx *goproxy.ProxyCtx) *http.Response {
if p.quiet {
return r
}
timestamp := time.Now().Format("20060102T15:04:05.000000")
if r != nil {
if p.verbose || p.isDomainOfInterest(ctx.Req.Host) {
fmt.Printf(
"[%s][INFO][Interest=%v] HTTP Response: %s %s\n",
timestamp,
p.isDomainOfInterest(ctx.Req.Host),
r.Status,
ctx.Req.URL.String(),
)
}
// Get request body from context (if available)
var reqBody []byte
var modifiedBody []byte
if ctx.UserData != nil {
if userData, ok := ctx.UserData.(UserData); ok {
reqBody = userData.RequestBody
modifiedBody = userData.ModifiedBody
}
}
// Read response body once and recreate it for both dumping and returning
if r.Body != nil {
respBody, err := io.ReadAll(r.Body)
if err != nil {
log.Printf("Failed to read response body: %v", err)
return r
}
r.Body.Close()
// Recreate the response body so it can be returned to client
r.Body = io.NopCloser(bytes.NewReader(respBody))
r.ContentLength = int64(len(respBody))
// Dump traffic to file with both request and response bodies
// Only dump if verbose mode is enabled OR if the request was modified
if p.verbose || modifiedBody != nil {
p.dumpHTTPTrafficWithBodies(ctx.Req, r, reqBody, modifiedBody, respBody)
}
} else {
// No response body, but may have request body
// Only dump if verbose mode is enabled OR if the request was modified
if p.verbose || modifiedBody != nil {
p.dumpHTTPTrafficWithBodies(ctx.Req, r, reqBody, modifiedBody, nil)
}
}
}
return r
})
}
func (p *ProxyServer) Start() error {
fmt.Printf("Starting HTTP server on %s\n", p.server.Addr)
// Test if port is available
listener, err := net.Listen("tcp", p.server.Addr)
if err != nil {
return fmt.Errorf("failed to bind to port %s: %v", p.server.Addr, err)
}
fmt.Printf("Successfully bound to port %s\n", p.server.Addr)
// Start serving with our listener
return p.server.Serve(listener)
}
func (p *ProxyServer) Shutdown() {
// Restore system proxy settings
if err := p.restoreSystemProxy(); err != nil {
log.Printf("Failed to restore system proxy: %v", err)
} else {
fmt.Println("System proxy restored")
}
// Close HTTP server
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
p.server.Shutdown(ctx)
}
// dumpHTTPTrafficWithBodies dumps HTTP request and response with both bodies to file
func (p *ProxyServer) dumpHTTPTrafficWithBodies(req *http.Request, resp *http.Response, reqBody []byte, modifiedBody []byte, respBody []byte) {
file, err := os.Create(p.getFilePath(req))
if err != nil {
log.Printf("Failed to create dump file: %v", err)
return
}
defer file.Close()
// Write request information
fmt.Fprintf(file, "=== REQUEST ===\n")
fmt.Fprintf(file, "%s %s %s\n", req.Method, req.URL.String(), req.Proto)
fmt.Fprintf(file, "Host: %s\n", req.Host)
// Write all request headers
for name, values := range req.Header {
for _, value := range values {
fmt.Fprintf(file, "%s: %s\n", name, value)
}
}
fmt.Fprintf(file, "\n")
// Write request body
if len(reqBody) > 0 {
fmt.Fprintf(file, "%s\n", string(reqBody))
}
if modifiedBody != nil {
fmt.Fprintf(file, "\n=== MODIFIED REQUEST BODY ===\n")
fmt.Fprintf(file, "%s\n", string(modifiedBody))
}
// Write response information
if resp != nil {
fmt.Fprintf(file, "\n=== RESPONSE ===\n")
fmt.Fprintf(file, "%s %s\n", resp.Proto, resp.Status)
// Write all response headers
for name, values := range resp.Header {
for _, value := range values {
fmt.Fprintf(file, "%s: %s\n", name, value)
}
}
fmt.Fprintf(file, "\n")
// Write response body
if len(respBody) > 0 {
fmt.Fprintf(file, "%s\n", string(respBody))
}
}
}
func (p *ProxyServer) getFilePath(req *http.Request) string {
timestamp := time.Now().Format("20060102.150405.000000000")
filename := fmt.Sprintf("%s_%s.txt", timestamp, sanitizeFilename(req.URL.String()))
if p.isDomainOfInterest(req.Host) {
return filepath.Join(p.config.Dump.DOIDir, filename)
}
return filepath.Join(p.config.Dump.OutputDir, filename)
}
// Keep the isDomainOfInterest method as it's still needed
func (p *ProxyServer) isDomainOfInterest(host string) bool {
// Remove port number
if colonIndex := strings.Index(host, ":"); colonIndex != -1 {
host = host[:colonIndex]
}
for _, domain := range p.config.DomainsOfInterest {
if strings.Contains(host, domain) {
return true
}
}
return false
}
// sanitizeFilename replaces unsafe characters for file names
func sanitizeFilename(filename string) string {
// Replace unsafe characters
replacer := strings.NewReplacer(
":", "_",
"/", "_",
"\\", "_",
"*", "_",
"?", "_",
"\"", "_",
"<", "_",
">", "_",
"|", "_",
)
s := replacer.Replace(filename)
if len(s) > 80 {
return s[:80]
}
return s
}
func (p *ProxyServer) setSystemProxy() error {
// Get current proxy settings
k, err := registry.OpenKey(registry.CURRENT_USER, `Software\Microsoft\Windows\CurrentVersion\Internet Settings`, registry.QUERY_VALUE)
if err != nil {
return err
}
defer k.Close()
// Read current ProxyServer settings
currentProxy, _, err := k.GetStringValue("ProxyServer")
if err == nil {
p.originalProxy = currentProxy
}
// Set new proxy
k, err = registry.OpenKey(registry.CURRENT_USER, `Software\Microsoft\Windows\CurrentVersion\Internet Settings`, registry.SET_VALUE)
if err != nil {
return err
}
defer k.Close()
proxyAddr := fmt.Sprintf("127.0.0.1:%d", p.config.Proxy.Port)
// Enable proxy
if err := k.SetDWordValue("ProxyEnable", 1); err != nil {
return err
}
// Set proxy server address for BOTH HTTP and HTTPS
// Format: "http=proxy:port;https=proxy:port" or just "proxy:port" for both
if err := k.SetStringValue("ProxyServer", proxyAddr); err != nil {
return err
}
// Ensure no proxy bypass for local addresses
if err := k.SetStringValue("ProxyOverride", "<local>"); err != nil {
// This is not critical, continue anyway
fmt.Printf("Warning: Could not set ProxyOverride: %v\n", err)
}
// Refresh IE settings and notify system
return p.refreshIESettings()
}
func (p *ProxyServer) restoreSystemProxy() error {
k, err := registry.OpenKey(registry.CURRENT_USER, `Software\Microsoft\Windows\CurrentVersion\Internet Settings`, registry.SET_VALUE)
if err != nil {
return err
}
defer k.Close()
// Disable proxy
if err := k.SetDWordValue("ProxyEnable", 0); err != nil {
return err
}
// Restore original proxy settings
if p.originalProxy != "" {
if err := k.SetStringValue("ProxyServer", p.originalProxy); err != nil {
return err
}
}
// Refresh IE settings
return p.refreshIESettings()
}
func (p *ProxyServer) refreshIESettings() error {
// Multiple methods to ensure Windows recognizes the proxy change
// Method 1: Reset WinHTTP proxy
cmd1 := exec.Command("cmd", "/c", "netsh winhttp reset proxy")
if err := cmd1.Run(); err != nil {
fmt.Printf("Warning: netsh winhttp reset failed: %v\n", err)
}
// Method 2: Import IE proxy settings to WinHTTP
cmd2 := exec.Command("cmd", "/c", "netsh winhttp import proxy source=ie")
if err := cmd2.Run(); err != nil {
fmt.Printf("Warning: netsh winhttp import failed: %v\n", err)
}
// Method 3: Force refresh Internet Options
cmd3 := exec.Command("cmd", "/c", "rundll32.exe inetcpl.cpl,ClearMyTracksByProcess 8")
if err := cmd3.Run(); err != nil {
fmt.Printf("Warning: Internet Options refresh failed: %v\n", err)
}
return nil
}
func (p *ProxyServer) verifyProxySettings() error {
k, err := registry.OpenKey(registry.CURRENT_USER, `Software\Microsoft\Windows\CurrentVersion\Internet Settings`, registry.QUERY_VALUE)
if err != nil {
return fmt.Errorf("failed to open registry key: %v", err)
}
defer k.Close()
// Check if proxy is enabled
proxyEnable, _, err := k.GetIntegerValue("ProxyEnable")
if err != nil {
return fmt.Errorf("failed to read ProxyEnable: %v", err)
}
if proxyEnable != 1 {
return fmt.Errorf("proxy is not enabled (ProxyEnable = %d)", proxyEnable)
}
// Check proxy server setting
proxyServer, _, err := k.GetStringValue("ProxyServer")
if err != nil {
return fmt.Errorf("failed to read ProxyServer: %v", err)
}
expectedProxy := fmt.Sprintf("127.0.0.1:%d", p.config.Proxy.Port)
if proxyServer != expectedProxy {
return fmt.Errorf("proxy server mismatch: expected %s, got %s", expectedProxy, proxyServer)
}
return nil
}
func (p *ProxyServer) showProxyConfiguration() {
fmt.Printf("Current Proxy Configuration:\n")
// Show Windows proxy settings
k, err := registry.OpenKey(registry.CURRENT_USER, `Software\Microsoft\Windows\CurrentVersion\Internet Settings`, registry.QUERY_VALUE)
if err != nil {
fmt.Printf("Could not read proxy settings: %v\n", err)
return
}
defer k.Close()
if proxyEnable, _, err := k.GetIntegerValue("ProxyEnable"); err == nil {
fmt.Printf(" ProxyEnable: %d\n", proxyEnable)
}
if proxyServer, _, err := k.GetStringValue("ProxyServer"); err == nil {
fmt.Printf(" ProxyServer: %s\n", proxyServer)
}
if proxyOverride, _, err := k.GetStringValue("ProxyOverride"); err == nil {
fmt.Printf(" ProxyOverride: %s\n", proxyOverride)
}
// Show WinHTTP proxy settings
fmt.Printf(" WinHTTP Proxy Settings:\n")
cmd := exec.Command("cmd", "/c", "netsh winhttp show proxy")
if output, err := cmd.CombinedOutput(); err == nil {
fmt.Printf(" %s\n", string(output))
} else {
fmt.Printf(" ❌ Could not get WinHTTP proxy settings: %v\n", err)
}
}
func (p *ProxyServer) installCACert() error {
// Use hardcoded CA certificate data
certData := []byte(getHardcodedCACert())
// Parse certificate
var cert *x509.Certificate
var err error
// Try parsing DER format directly
cert, err = x509.ParseCertificate(certData)
if err != nil {
// Try parsing PEM format
block, _ := pem.Decode(certData)
if block == nil {
return fmt.Errorf("unable to parse certificate file")
}
cert, err = x509.ParseCertificate(block.Bytes)
if err != nil {
return err
}
}
// Write certificate to temporary file
tempFile, err := os.CreateTemp("", "ca_cert_*.crt")
if err != nil {
return err
}
tempFileName := tempFile.Name()
defer os.Remove(tempFileName)
if err := pem.Encode(tempFile, &pem.Block{
Type: "CERTIFICATE",
Bytes: cert.Raw,
}); err != nil {
tempFile.Close()
return err
}
// Close the file before certutil accesses it to avoid sharing violation
tempFile.Close()
// Use certutil to install certificate to trusted root certificate authorities
cmd := exec.Command("certutil", "-addstore", "-f", "Root", tempFileName)
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("failed to install certificate: %v, output: %s", err, string(output))
}
return nil
}
// setConsoleUTF8 sets the Windows console to UTF-8 code page to prevent garbled text
func setConsoleUTF8() {
// Set console input and output code page to UTF-8 (65001)
cmd := exec.Command("cmd", "/c", "chcp 65001 >nul")
cmd.Run()
}
// testProxyConnectivity tests the proxy with simple HTTP and HTTPS requests
func testProxyConnectivity(proxyAddr string) {
fmt.Println("Testing proxy connectivity...")
// Create proxy URL
proxyURL, err := url.Parse(fmt.Sprintf("http://%s", proxyAddr))
if err != nil {
fmt.Printf("Failed to parse proxy URL: %v\n", err)
return
}
// Create HTTP client with proxy
transport := &http.Transport{
Proxy: http.ProxyURL(proxyURL),
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
client := &http.Client{
Transport: transport,
Timeout: 30 * time.Second,
}
// Test HTTP request
fmt.Println("Testing HTTP request...")
testHTTPRequest(client, "http://httpbin.org/ip")
// Test HTTPS request
fmt.Println("Testing HTTPS request...")
testHTTPSRequest(client, "https://httpbin.org/ip")
// Test redirects (should be transparent)
fmt.Println("Testing HTTP redirect...")
testRedirectTransparency(client, "http://httpbin.org/redirect/1")
// Test domain that might cause issues
fmt.Println("Testing Google...")
testHTTPSRequest(client, "https://www.google.com")
}
func testHTTPRequest(client *http.Client, testURL string) {
resp, err := client.Get(testURL)
if err != nil {
fmt.Printf("HTTP test failed: %v\n", err)
return
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Printf("Failed to read HTTP response: %v\n", err)
return
}
fmt.Printf("HTTP test successful: %s\n", resp.Status)
fmt.Printf(" Response: %s\n", string(body)[:min(len(body), 200)])
}
func testHTTPSRequest(client *http.Client, testURL string) {
resp, err := client.Get(testURL)
if err != nil {
fmt.Printf("HTTPS test failed: %v\n", err)
return
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Printf("Failed to read HTTPS response: %v\n", err)
return
}
fmt.Printf("HTTPS test successful: %s\n", resp.Status)
fmt.Printf(" Response: %s\n", string(body)[:min(len(body), 200)])
}
func testRedirectTransparency(client *http.Client, testURL string) {
// Create a client that doesn't follow redirects to test transparency
testClient := &http.Client{
Transport: client.Transport,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
Timeout: client.Timeout,
}
resp, err := testClient.Get(testURL)
if err != nil {
fmt.Printf("Redirect transparency test failed: %v\n", err)
return
}
defer resp.Body.Close()
if resp.StatusCode >= 300 && resp.StatusCode < 400 {
location := resp.Header.Get("Location")
fmt.Printf("Redirect transparency test successful: %s -> %s\n", resp.Status, location)
} else {
fmt.Printf("Expected redirect, got: %s (this may be normal)\n", resp.Status)
}
}
func min(a, b int) int {
if a < b {
return a
}
return b
}