Skip to content

Commit e5001ec

Browse files
committed
Initial
0 parents  commit e5001ec

File tree

5 files changed

+321
-0
lines changed

5 files changed

+321
-0
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/mitm-ca.crt
2+
/mitm-ca.key

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Рабочий ютуб без как такового ну или явного дурения ТСПУ
2+
3+
__Как это работает?__
4+
5+
Браузер подключается к прокси, прокси делает MITM с браузером и открывает новое TLS соединение с сервером устанавливая SNI в соответствии с маппингами в __spoof.list__, НО, дальнейший HTTP трафик полностью проксируется без изменений, то есть заголовок `Host:` остается неизменным.
6+
7+
ТСПУ видит `SNI = www.google.com`, сервер гугла видит HTTP заголовок `Host: www.youtube.com`, и каким то чудом возвращает ответ для YouTube.com а не Google.com.
8+
9+
При попытке прикрыть этот способ, РКН полностью убьет Google, для РКН сломать гугл пока-что дорогое удовольствие, поэтому это будет работать до тех пор, пока Россия окончательно не отрезалась от глобальной сети.
10+
11+
Код тут полностью навайбкоден нейронкой, так как задача была проверить теорию а не написать что-то производительное.
12+
13+
Сертификат возвращаемый сервером имеет альтернативные доменные имена, и чтобы не было ошибок валидности сертификата - домены для спуфа надо подбирать, например сертификат при подключении к www.google.com подойдет и для www.youtube.com. В случае с GGC (ну или googlevideo.com) нужно подбирать домен для спуфа из альтернативных имен этого самого GGC. Подбирать домены можно ориентируясь этому `❌ Handshake with www.google.com failed: tls: failed to verify certificate: x509: certificate is valid for *.c.docs.google.com, *.a1.googlevideo.com, *.bdn.dev, *.oo
14+
rigin-test.bdn.dev, *.c.2mdn.net, *.c.bigcache.googleapis.com, *.c.chat.google.com, *.c.doc-0-0-sj.sj.googleusercontent.com, *.c.drive.google.com, *.c.googlesyndication.com, *.c.google
15+
video.com, *.c.mail.google.com, *.c.mail.googleusercontent.com, *.c.pack.google.com, *.c.play.google.com, *.c.youtube.com, *.dai.googlevideo.com, *.googlevideo.com, *.googlezip.net, *.gvt1.com, *.offline-maps.gvt1.com, *.snap.gvt1.com, *.gcpcdn.gvt1.com, xn--ngstr-lra8j.com, *.xn--ngstr-lra8j.com, not www.google.com
16+
`, звёздочка в домене это Wildcard сертификат, можно и нужно ее заменить на любой текст либо существующее доменное имя GGC, проще говоря: если `*.c.docs.google.com` то `c.docs.google.com` -> failed to verify, однако `aboba.c.docs.google.com` -> сработает.
17+
18+
А что если просто менять SNI и не делать MITM? Мне не удалось заставить это работать, увы, но видимо генерируемые ключи зависят от SNI, и при попытке подменить SNI я получаю `curl: (35) schannel: next InitializeSecurityContext failed: SEC_E_DECRYPT_FAILURE (0x80090330) - Указанные данные не могут быть расшифрованы.`
19+
20+
Однако это явно имеет какой-либо потенциал.
21+
22+
__If you're a Google engineer, I kindly ask you not to change anything on the servers. They are working perfectly, thx.__

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module YouTubeByMITM
2+
3+
go 1.25

main.go

Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
package main
2+
3+
import (
4+
"bufio"
5+
"crypto/rand"
6+
"crypto/rsa"
7+
"crypto/tls"
8+
"crypto/x509"
9+
"crypto/x509/pkix"
10+
"encoding/pem"
11+
"fmt"
12+
"io"
13+
"log"
14+
"math/big"
15+
"net"
16+
"net/http"
17+
"os"
18+
"strings"
19+
"sync"
20+
"time"
21+
)
22+
23+
const (
24+
caCertFile = "mitm-ca.crt"
25+
caKeyFile = "mitm-ca.key"
26+
spoofFile = "spoof.list"
27+
proxyPort = ":8080"
28+
)
29+
30+
var (
31+
caCert *x509.Certificate
32+
caKey *rsa.PrivateKey
33+
certCache = make(map[string]tls.Certificate)
34+
certMu sync.Mutex
35+
spoofMap = make(map[string]string) // origin -> spoof_to
36+
)
37+
38+
func loadSpoofMap() error {
39+
file, err := os.Open(spoofFile)
40+
if os.IsNotExist(err) {
41+
// Создаём дефолтный файл
42+
defaultContent := `# Format: origin_domain -> spoof_to_domain
43+
youtube.com -> www.google.com
44+
ytimg.com -> www.google.com
45+
googlevideo.com -> c.drive.google.com
46+
`
47+
os.WriteFile(spoofFile, []byte(defaultContent), 0644)
48+
log.Printf("✅ Created default %s", spoofFile)
49+
file, _ = os.Open(spoofFile)
50+
} else if err != nil {
51+
return err
52+
}
53+
defer file.Close()
54+
55+
scanner := bufio.NewScanner(file)
56+
lineNum := 0
57+
for scanner.Scan() {
58+
lineNum++
59+
line := strings.TrimSpace(scanner.Text())
60+
if line == "" || strings.HasPrefix(line, "#") {
61+
continue
62+
}
63+
64+
parts := strings.Split(line, "->")
65+
if len(parts) != 2 {
66+
log.Printf("⚠️ Line %d: invalid format (expected 'origin -> spoof')", lineNum)
67+
continue
68+
}
69+
70+
origin := strings.TrimSpace(parts[0])
71+
spoof := strings.TrimSpace(parts[1])
72+
spoofMap[origin] = spoof
73+
log.Printf(" 📥 %s → %s", origin, spoof)
74+
}
75+
76+
if len(spoofMap) == 0 {
77+
log.Printf("⚠️ No mappings found in %s, using defaults", spoofFile)
78+
spoofMap["youtube.com"] = "www.google.com"
79+
spoofMap["ytimg.com"] = "www.google.com"
80+
spoofMap["googlevideo.com"] = "c.drive.google.com"
81+
}
82+
83+
return scanner.Err()
84+
}
85+
86+
func getSpoofTarget(host string) (string, bool) {
87+
host = strings.ToLower(strings.Split(host, ":")[0])
88+
for origin, spoof := range spoofMap {
89+
if strings.HasSuffix(host, origin) || host == origin {
90+
return spoof, true
91+
}
92+
}
93+
return "", false
94+
}
95+
96+
func loadOrCreateCA() error {
97+
if _, err := os.Stat(caCertFile); err == nil {
98+
certData, _ := os.ReadFile(caCertFile)
99+
keyData, _ := os.ReadFile(caKeyFile)
100+
101+
block, _ := pem.Decode(certData)
102+
caCert, _ = x509.ParseCertificate(block.Bytes)
103+
104+
block, _ = pem.Decode(keyData)
105+
caKey, _ = x509.ParsePKCS1PrivateKey(block.Bytes)
106+
return nil
107+
}
108+
109+
priv, _ := rsa.GenerateKey(rand.Reader, 2048)
110+
template := &x509.Certificate{
111+
SerialNumber: big.NewInt(1),
112+
Subject: pkix.Name{
113+
Organization: []string{"SNI Spoof Proxy"},
114+
CommonName: "SNI Spoof CA",
115+
},
116+
NotBefore: time.Now(),
117+
NotAfter: time.Now().Add(10 * 365 * 24 * time.Hour),
118+
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
119+
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
120+
BasicConstraintsValid: true,
121+
IsCA: true,
122+
}
123+
124+
certBytes, _ := x509.CreateCertificate(rand.Reader, template, template, &priv.PublicKey, priv)
125+
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certBytes})
126+
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
127+
128+
os.WriteFile(caCertFile, certPEM, 0644)
129+
os.WriteFile(caKeyFile, keyPEM, 0600)
130+
131+
caCert, _ = x509.ParseCertificate(certBytes)
132+
caKey = priv
133+
return nil
134+
}
135+
136+
func getCertForHost(host string) tls.Certificate {
137+
certMu.Lock()
138+
defer certMu.Unlock()
139+
140+
if cert, ok := certCache[host]; ok {
141+
return cert
142+
}
143+
144+
cleanHost := strings.Split(host, ":")[0]
145+
priv, _ := rsa.GenerateKey(rand.Reader, 2048)
146+
template := &x509.Certificate{
147+
SerialNumber: big.NewInt(time.Now().UnixNano()),
148+
Subject: pkix.Name{
149+
CommonName: cleanHost,
150+
},
151+
DNSNames: []string{cleanHost},
152+
NotBefore: time.Now(),
153+
NotAfter: time.Now().Add(365 * 24 * time.Hour),
154+
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
155+
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
156+
}
157+
158+
certBytes, _ := x509.CreateCertificate(rand.Reader, template, caCert, &priv.PublicKey, caKey)
159+
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certBytes})
160+
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
161+
162+
cert, _ := tls.X509KeyPair(certPEM, keyPEM)
163+
certCache[host] = cert
164+
return cert
165+
}
166+
167+
func handleNormal(w http.ResponseWriter, r *http.Request) {
168+
dest := r.URL.Host
169+
if dest == "" {
170+
dest = r.Host
171+
}
172+
173+
hj, ok := w.(http.Hijacker)
174+
if !ok {
175+
return
176+
}
177+
178+
clientConn, _, err := hj.Hijack()
179+
if err != nil {
180+
return
181+
}
182+
defer clientConn.Close()
183+
184+
clientConn.Write([]byte("HTTP/1.1 200 Connection Established\r\n\r\n"))
185+
186+
rawConn, err := net.Dial("tcp", dest)
187+
if err != nil {
188+
return
189+
}
190+
defer rawConn.Close()
191+
192+
go io.Copy(rawConn, clientConn)
193+
io.Copy(clientConn, rawConn)
194+
}
195+
196+
func handleTunnel(w http.ResponseWriter, r *http.Request) {
197+
dest := r.URL.Host
198+
if dest == "" {
199+
dest = r.Host
200+
}
201+
202+
hj, ok := w.(http.Hijacker)
203+
if !ok {
204+
return
205+
}
206+
207+
clientConn, _, err := hj.Hijack()
208+
if err != nil {
209+
return
210+
}
211+
defer clientConn.Close()
212+
213+
clientConn.Write([]byte("HTTP/1.1 200 Connection Established\r\n\r\n"))
214+
215+
tlsConn := tls.Server(clientConn, &tls.Config{
216+
Certificates: []tls.Certificate{getCertForHost(dest)},
217+
MinVersion: tls.VersionTLS12,
218+
})
219+
defer tlsConn.Close()
220+
221+
spoofTarget, ok := getSpoofTarget(dest)
222+
if !ok {
223+
spoofTarget = "www.google.com"
224+
log.Printf("⚠️ No spoof target for %s, using default: %s", dest, spoofTarget)
225+
} else {
226+
log.Printf("🔌 %s → %s (SNI spoof)", dest, spoofTarget)
227+
}
228+
229+
rawConn, err := net.Dial("tcp", dest)
230+
if err != nil {
231+
log.Printf("❌ Dial to %s failed: %v", spoofTarget, err)
232+
return
233+
}
234+
defer rawConn.Close()
235+
236+
serverConn := tls.Client(rawConn, &tls.Config{
237+
ServerName: spoofTarget,
238+
MinVersion: tls.VersionTLS12,
239+
InsecureSkipVerify: false, // Валидация сертификата включена.
240+
})
241+
242+
if err := serverConn.Handshake(); err != nil {
243+
log.Printf("❌ Handshake with %s failed: %v", spoofTarget, err)
244+
return
245+
}
246+
defer serverConn.Close()
247+
248+
go io.Copy(serverConn, tlsConn)
249+
io.Copy(tlsConn, serverConn)
250+
}
251+
252+
func main() {
253+
if err := loadSpoofMap(); err != nil {
254+
log.Fatalf("❌ Failed to load %s: %v", spoofFile, err)
255+
}
256+
257+
if err := loadOrCreateCA(); err != nil {
258+
log.Fatalf("❌ Failed to load/create CA: %v", err)
259+
}
260+
261+
abs, _ := os.Getwd()
262+
fmt.Println(strings.Repeat("=", 60))
263+
fmt.Println("✅ MitM Proxy with SNI spoofing (spoof.list)")
264+
fmt.Println(strings.Repeat("=", 60))
265+
fmt.Printf("Proxy: localhost%s\n", proxyPort)
266+
fmt.Printf("CA cert: %s\\%s\n", abs, caCertFile)
267+
fmt.Printf("Spoof map: %s\\%s\n", abs, spoofFile)
268+
fmt.Println("\n⚠️ УСТАНОВИ mitm-ca.crt В ДОВЕРЕННЫЕ КОРНЕВЫЕ ЦЕНТРЫ СЕРТИФИКАЦИИ")
269+
fmt.Println(" (certmgr.msc → Доверенные корневые центры сертификации → Импорт). Либо в самом браузере.")
270+
fmt.Println(strings.Repeat("=", 60))
271+
fmt.Println("Press Ctrl+C to stop\n")
272+
273+
http.ListenAndServe(proxyPort, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
274+
if r.Method == "CONNECT" {
275+
dest := r.URL.Host
276+
if dest == "" {
277+
dest = r.Host
278+
}
279+
280+
if _, ok := getSpoofTarget(dest); ok {
281+
handleTunnel(w, r)
282+
} else {
283+
handleNormal(w, r)
284+
}
285+
}
286+
}))
287+
}

spoof.list

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Format: origin_domain -> spoof_to_domain
2+
3+
ytimg.com -> edgestatic.com
4+
ggpht.com -> www.google.com
5+
googlevideo.com -> a.gvt1.com
6+
c.youtube.com -> a.gvt1.com
7+
youtube.com -> www.google.com

0 commit comments

Comments
 (0)