mirror of
https://github.com/xweiba/location-spoofer.git
synced 2026-09-21 22:30:46 +08:00
fix: harden startup proxy and coordinate handling
This commit is contained in:
+25
-7
@@ -64,13 +64,12 @@ func wloccore_startproxy(certData, keyData *C.char, lat, lon C.double, enabled C
|
||||
//export wloccore_stopproxy
|
||||
func wloccore_stopproxy(h C.uintptr_t) C.int {
|
||||
logEvent("stopproxy requested")
|
||||
handle := cgo.Handle(h)
|
||||
srv, ok := handle.Value().(*http.Server)
|
||||
handle.Delete()
|
||||
srv, handle, ok := proxyForHandle(h)
|
||||
if !ok {
|
||||
logEvent("stopproxy failed: invalid handle")
|
||||
return 1
|
||||
}
|
||||
handle.Delete()
|
||||
if err := stopProxy(srv); err != nil {
|
||||
logEvent("stopproxy failed: " + err.Error())
|
||||
return 2
|
||||
@@ -79,6 +78,20 @@ func wloccore_stopproxy(h C.uintptr_t) C.int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func proxyForHandle(h C.uintptr_t) (server *http.Server, handle cgo.Handle, ok bool) {
|
||||
if h == 0 {
|
||||
return nil, 0, false
|
||||
}
|
||||
defer func() {
|
||||
if recover() != nil {
|
||||
server, handle, ok = nil, 0, false
|
||||
}
|
||||
}()
|
||||
handle = cgo.Handle(h)
|
||||
server, ok = handle.Value().(*http.Server)
|
||||
return server, handle, ok
|
||||
}
|
||||
|
||||
//export wloccore_setcoords
|
||||
func wloccore_setcoords(lat, lon C.double, enabled C.int, accuracy C.int) {
|
||||
stateMu.Lock()
|
||||
@@ -87,7 +100,7 @@ func wloccore_setcoords(lat, lon C.double, enabled C.int, accuracy C.int) {
|
||||
currentEnabled = enabled != 0
|
||||
currentAccuracy = int(accuracy)
|
||||
stateMu.Unlock()
|
||||
logEvent("setcoords enabled=" + strconv.FormatBool(enabled != 0) + " lat=" + strconv.FormatFloat(float64(lat), 'f', 6, 64) + " lon=" + strconv.FormatFloat(float64(lon), 'f', 6, 64) + " accuracy=" + strconv.Itoa(int(accuracy)))
|
||||
logEvent("setcoords enabled=" + strconv.FormatBool(enabled != 0) + " accuracy=" + strconv.Itoa(int(accuracy)))
|
||||
}
|
||||
|
||||
//export wloccore_getcoords
|
||||
@@ -127,12 +140,17 @@ func wloccore_startcertserver(certData, keyData *C.char) C.uintptr_t {
|
||||
return C.uintptr_t(cgo.NewHandle(server))
|
||||
}
|
||||
|
||||
func certificateServerForHandle(h C.uintptr_t) (*certificateServer, cgo.Handle, bool) {
|
||||
func certificateServerForHandle(h C.uintptr_t) (server *certificateServer, handle cgo.Handle, ok bool) {
|
||||
if h == 0 {
|
||||
return nil, 0, false
|
||||
}
|
||||
handle := cgo.Handle(h)
|
||||
server, ok := handle.Value().(*certificateServer)
|
||||
defer func() {
|
||||
if recover() != nil {
|
||||
server, handle, ok = nil, 0, false
|
||||
}
|
||||
}()
|
||||
handle = cgo.Handle(h)
|
||||
server, ok = handle.Value().(*certificateServer)
|
||||
return server, handle, ok
|
||||
}
|
||||
|
||||
|
||||
+13
-12
@@ -1,34 +1,35 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/binary"
|
||||
"encoding/pem"
|
||||
"io"
|
||||
"math/big"
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
func deterministicReader() io.Reader {
|
||||
seed := sha256.Sum256([]byte("paopao-location-spoofer-ca-v1"))
|
||||
src := rand.NewSource(int64(binary.BigEndian.Uint64(seed[:8])))
|
||||
return rand.New(src)
|
||||
func randomSerialNumber() (*big.Int, error) {
|
||||
// Keep the serial positive and within the RFC 5280 recommended 20-octet bound.
|
||||
limit := new(big.Int).Lsh(big.NewInt(1), 159)
|
||||
return rand.Int(rand.Reader, limit)
|
||||
}
|
||||
|
||||
func generateCA() (certPEM, keyPEM []byte, err error) {
|
||||
rng := deterministicReader()
|
||||
privateKey, err := rsa.GenerateKey(rng, 2048)
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
serialNumber, err := randomSerialNumber()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
template := x509.Certificate{
|
||||
SerialNumber: big.NewInt(1),
|
||||
SerialNumber: serialNumber,
|
||||
Subject: pkix.Name{
|
||||
Organization: []string{"WLOC"},
|
||||
CommonName: "WLOC CA " + time.Now().In(time.FixedZone("CST", 8*3600)).Format("2006.01.02 15:04"),
|
||||
@@ -41,7 +42,7 @@ func generateCA() (certPEM, keyPEM []byte, err error) {
|
||||
IsCA: true,
|
||||
}
|
||||
|
||||
certDER, err := x509.CreateCertificate(rng, &template, &template, &privateKey.PublicKey, privateKey)
|
||||
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
@@ -33,3 +33,17 @@ func TestGenerateCA(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateCAUsesUniquePrivateKeys(t *testing.T) {
|
||||
_, firstKey, err := generateCA()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, secondKey, err := generateCA()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(firstKey) == string(secondKey) {
|
||||
t.Fatal("generated CA private keys must not be deterministic")
|
||||
}
|
||||
}
|
||||
|
||||
+25
-57
@@ -11,7 +11,6 @@ import (
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -95,10 +94,10 @@ func newProxy(cert *tls.Certificate) *goproxy.ProxyHttpServer {
|
||||
}
|
||||
if r.URL.Path == "/coords" {
|
||||
stateMu.Lock()
|
||||
enabled, lat, lon := currentEnabled, currentLat, currentLon
|
||||
enabled, lat, lon, accuracy := currentEnabled, currentLat, currentLon, currentAccuracy
|
||||
stateMu.Unlock()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(fmt.Sprintf(`{"enabled":%t,"lat":%.6f,"lon":%.6f,"accuracy":%d}`, enabled, lat, lon, currentAccuracy)))
|
||||
w.Write([]byte(fmt.Sprintf(`{"enabled":%t,"lat":%.6f,"lon":%.6f,"accuracy":%d}`, enabled, lat, lon, accuracy)))
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/proxy.mobileconfig" || r.URL.Path == "/proxy.mobileconfig/" {
|
||||
@@ -152,14 +151,8 @@ func newProxy(cert *tls.Certificate) *goproxy.ProxyHttpServer {
|
||||
}
|
||||
|
||||
proxy.OnRequest().DoFunc(func(req *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) {
|
||||
body := []byte(nil)
|
||||
if req.Body != nil {
|
||||
body, _ = io.ReadAll(req.Body)
|
||||
req.Body.Close()
|
||||
req.Body = io.NopCloser(bytes.NewReader(body))
|
||||
}
|
||||
logEvent(fmt.Sprintf("proxy request target=%s method=%s path=%s size=%d body_prefix=%s",
|
||||
req.Host, req.Method, req.URL.Path, len(body), hexPrefix(body, 64)))
|
||||
// Do not buffer or log arbitrary global-proxy traffic. Keep requests streaming
|
||||
// and do not persist unrelated request content in diagnostics.
|
||||
return serveLocalRequests(req, ctx)
|
||||
})
|
||||
proxy.OnResponse().DoFunc(patchWlocResponse)
|
||||
@@ -176,7 +169,7 @@ func serveLocalRequests(req *http.Request, ctx *goproxy.ProxyCtx) (*http.Request
|
||||
}
|
||||
if (h == "baidu.com" || h == "www.baidu.com") && strings.HasPrefix(req.URL.Path, "/paopao-verify-") {
|
||||
token := strings.TrimPrefix(req.URL.Path, "/paopao-verify-")
|
||||
logEvent("verify request path=" + req.URL.Path + " token=" + token)
|
||||
logEvent("verify request received")
|
||||
if checkVerifyToken(token) {
|
||||
resp := goproxy.NewResponse(req, "text/plain", http.StatusOK, token)
|
||||
resp.Header.Set("Cache-Control", "no-store")
|
||||
@@ -229,41 +222,36 @@ func patchWlocResponse(resp *http.Response, ctx *goproxy.ProxyCtx) *http.Respons
|
||||
enabled, lat, lon, accuracy := currentEnabled, currentLat, currentLon, currentAccuracy
|
||||
stateMu.Unlock()
|
||||
|
||||
const maxPatchBodyBytes int64 = 1 << 20
|
||||
if resp.ContentLength > maxPatchBodyBytes {
|
||||
logEvent(fmt.Sprintf("wloc response passed through: body exceeds patch limit (%d bytes)", resp.ContentLength))
|
||||
return resp
|
||||
}
|
||||
|
||||
originalBody := resp.Body
|
||||
body, err := io.ReadAll(originalBody)
|
||||
originalBody.Close()
|
||||
body, err := io.ReadAll(io.LimitReader(originalBody, maxPatchBodyBytes+1))
|
||||
if err != nil {
|
||||
logEvent("wloc response read failed: " + err.Error())
|
||||
resp.Body = io.NopCloser(bytes.NewReader(body))
|
||||
resp.Body = io.NopCloser(io.MultiReader(bytes.NewReader(body), originalBody))
|
||||
return resp
|
||||
}
|
||||
logEvent(fmt.Sprintf("wloc upstream response status=%d size=%d headers=[%s] body_prefix=%s",
|
||||
resp.StatusCode, len(body), summarizeHeaders(resp.Header), hexPrefix(body, 64)))
|
||||
if int64(len(body)) > maxPatchBodyBytes {
|
||||
logEvent("wloc response passed through: body exceeds patch limit")
|
||||
resp.Body = io.NopCloser(io.MultiReader(bytes.NewReader(body), originalBody))
|
||||
return resp
|
||||
}
|
||||
originalBody.Close()
|
||||
|
||||
if !enabled {
|
||||
logEvent("wloc upstream response passed through (spoofing disabled)")
|
||||
resp.Body = io.NopCloser(bytes.NewReader(body))
|
||||
return resp
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
logEvent(fmt.Sprintf("wloc upstream response passed through (status %d != 200)", resp.StatusCode))
|
||||
resp.Body = io.NopCloser(bytes.NewReader(body))
|
||||
return resp
|
||||
}
|
||||
if len(body) == 0 {
|
||||
logEvent("wloc upstream response empty, passed through")
|
||||
if !enabled || resp.StatusCode != http.StatusOK || len(body) == 0 {
|
||||
resp.Body = io.NopCloser(bytes.NewReader(body))
|
||||
return resp
|
||||
}
|
||||
|
||||
patched, stats, err := patchResponseBody(body, wlocCoords{Latitude: lat, Longitude: lon, Accuracy: accuracy})
|
||||
if err != nil {
|
||||
logEvent("wloc patch skipped: " + err.Error())
|
||||
resp.Body = io.NopCloser(bytes.NewReader(body))
|
||||
return resp
|
||||
}
|
||||
if bytes.Equal(patched, body) {
|
||||
logEvent("wloc patch produced identical body, passed through")
|
||||
if err != nil || bytes.Equal(patched, body) {
|
||||
if err != nil {
|
||||
logEvent("wloc patch skipped: " + err.Error())
|
||||
}
|
||||
resp.Body = io.NopCloser(bytes.NewReader(body))
|
||||
return resp
|
||||
}
|
||||
@@ -273,30 +261,10 @@ func patchWlocResponse(resp *http.Response, ctx *goproxy.ProxyCtx) *http.Respons
|
||||
resp.Header.Del("Content-Encoding")
|
||||
resp.Header.Del("Transfer-Encoding")
|
||||
resp.Header.Set("Content-Length", strconv.Itoa(len(patched)))
|
||||
logEvent(fmt.Sprintf("wloc patched target=%.6f,%.6f accuracy=%d locations=%d wifi=%d cell=%d skipped=%d in=%d out=%d body_prefix=%s",
|
||||
lat, lon, accuracy, stats.Locations, stats.WiFi, stats.Cell, stats.Skipped, len(body), len(patched), hexPrefix(patched, 64)))
|
||||
logEvent(fmt.Sprintf("wloc patched locations=%d wifi=%d cell=%d skipped=%d in=%d out=%d", stats.Locations, stats.WiFi, stats.Cell, stats.Skipped, len(body), len(patched)))
|
||||
return resp
|
||||
}
|
||||
|
||||
func hexPrefix(b []byte, n int) string {
|
||||
if len(b) > n {
|
||||
b = b[:n]
|
||||
}
|
||||
return fmt.Sprintf("%x", b)
|
||||
}
|
||||
|
||||
func summarizeHeaders(h http.Header) string {
|
||||
if len(h) == 0 {
|
||||
return ""
|
||||
}
|
||||
parts := make([]string, 0, len(h))
|
||||
for k, v := range h {
|
||||
parts = append(parts, k+"="+strings.Join(v, ","))
|
||||
}
|
||||
sort.Strings(parts)
|
||||
return strings.Join(parts, "; ")
|
||||
}
|
||||
|
||||
func generateProxyMobileConfig() string {
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
|
||||
@@ -4,7 +4,13 @@ import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -108,3 +114,109 @@ func TestTransparentBodyUnchanged(t *testing.T) {
|
||||
t.Fatal("expected non-patchable body to error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPatchWlocResponsePassesThroughOversizedBody(t *testing.T) {
|
||||
payload := bytes.Repeat([]byte("x"), (1<<20)+1)
|
||||
req := httptest.NewRequest(http.MethodPost, "https://gs-loc.apple.com/clls/wloc", nil)
|
||||
resp := &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Request: req,
|
||||
Header: make(http.Header),
|
||||
Body: io.NopCloser(bytes.NewReader(payload)),
|
||||
ContentLength: int64(len(payload)),
|
||||
}
|
||||
|
||||
patched := patchWlocResponse(resp, nil)
|
||||
got, err := io.ReadAll(patched.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(got, payload) {
|
||||
t.Fatal("oversized WLOC response was changed or truncated")
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeLocalRequestsKeepsUnrelatedRequestBodyStreaming(t *testing.T) {
|
||||
const secret = "body-must-not-be-buffered-or-logged"
|
||||
req := httptest.NewRequest(http.MethodPost, "https://example.com/upload", bytes.NewBufferString(secret))
|
||||
returned, response := serveLocalRequests(req, nil)
|
||||
if response != nil {
|
||||
t.Fatalf("unexpected local response: %d", response.StatusCode)
|
||||
}
|
||||
got, err := io.ReadAll(returned.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(got) != secret {
|
||||
t.Fatalf("request body changed: got %q", got)
|
||||
}
|
||||
if logs := drainLogs(); bytes.Contains([]byte(logs), []byte(secret)) {
|
||||
t.Fatal("request body leaked into diagnostic logs")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoordsEndpointReturnsAtomicSnapshot(t *testing.T) {
|
||||
stateMu.Lock()
|
||||
previousLat, previousLon := currentLat, currentLon
|
||||
previousEnabled, previousAccuracy := currentEnabled, currentAccuracy
|
||||
currentLat, currentLon, currentEnabled, currentAccuracy = 0, 0, false, 0
|
||||
stateMu.Unlock()
|
||||
t.Cleanup(func() {
|
||||
stateMu.Lock()
|
||||
currentLat, currentLon = previousLat, previousLon
|
||||
currentEnabled, currentAccuracy = previousEnabled, previousAccuracy
|
||||
stateMu.Unlock()
|
||||
})
|
||||
|
||||
handler := newProxy(nil).NonproxyHandler
|
||||
const updates = 20_000
|
||||
const readers = 8
|
||||
const readsPerReader = 2_500
|
||||
|
||||
var writers sync.WaitGroup
|
||||
writers.Add(1)
|
||||
go func() {
|
||||
defer writers.Done()
|
||||
for i := 1; i <= updates; i++ {
|
||||
stateMu.Lock()
|
||||
currentLat = float64(i)
|
||||
currentLon = -float64(i)
|
||||
currentEnabled = i%2 == 0
|
||||
currentAccuracy = i
|
||||
stateMu.Unlock()
|
||||
}
|
||||
}()
|
||||
|
||||
errs := make(chan error, readers)
|
||||
var readersGroup sync.WaitGroup
|
||||
for range readers {
|
||||
readersGroup.Add(1)
|
||||
go func() {
|
||||
defer readersGroup.Done()
|
||||
for i := 0; i < readsPerReader; i++ {
|
||||
recorder := httptest.NewRecorder()
|
||||
handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "http://proxy.local/coords", nil))
|
||||
var snapshot struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Lat float64 `json:"lat"`
|
||||
Lon float64 `json:"lon"`
|
||||
Accuracy int `json:"accuracy"`
|
||||
}
|
||||
if err := json.Unmarshal(recorder.Body.Bytes(), &snapshot); err != nil {
|
||||
errs <- err
|
||||
return
|
||||
}
|
||||
if snapshot.Lat != 0 && (snapshot.Lon != -snapshot.Lat || snapshot.Accuracy != int(snapshot.Lat)) {
|
||||
errs <- fmt.Errorf("torn coordinate snapshot: %+v", snapshot)
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
readersGroup.Wait()
|
||||
writers.Wait()
|
||||
close(errs)
|
||||
for err := range errs {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user