├── .github └── workflows │ └── go.yml ├── .gitignore ├── LICENSE ├── README.md ├── assets ├── real-aliyun.png └── self-signed-aliyun.png ├── go.mod └── main.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | tags: ["v*.*.*"] 7 | pull_request: 8 | branches: [ main ] 9 | 10 | jobs: 11 | 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | 17 | - name: Set up Go 18 | uses: actions/setup-go@v2 19 | with: 20 | go-version: "1.21.0" 21 | 22 | - name: Build 23 | run: | 24 | build() { 25 | export GOOS=$1 26 | export GOARCH=$2 27 | go build -ldflags "-s -w" -o build/copy-cert-${1}-${2}${3} main.go 28 | } 29 | build linux amd64 30 | build windows amd64 .exe 31 | build windows 386 .exe 32 | build darwin amd64 33 | build darwin arm64 34 | - name: Upload a Build Artifact 35 | uses: actions/upload-artifact@v4 36 | with: 37 | path: build/* 38 | - name: Release 39 | uses: softprops/action-gh-release@v1 40 | if: startsWith(github.ref, 'refs/tags/') 41 | with: 42 | files: build/* 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.crt 2 | *.key 3 | .idea/ 4 | certs/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 LiYang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # copy-cert 2 | 3 | 在资产测绘和应急响应的时候,大家可能更加关注 ssl 证书的签发者、有效期、序列号、域名等信息,并作为威胁情报采集的依据,而忽略了去校验证书的有效性。 4 | 本工具可以基于已知网站 ssl 证书的信息生成新的自签名证书,保持签发者、有效期、序列号、域名等一致,用于伪装流量。 5 | 6 | 参考资料 [C2基础设施威胁情报对抗策略](https://www.anquanke.com/post/id/291324) 7 | 8 | ## 用法 9 | 10 | 在 Release 中下载二进制或者自行编译,然后 `copy-cert $addr`,比如 `copy-cert baidu.com:443` 然后就可以得到几个证书和私钥文件。 11 | 12 | ``` 13 | ➜ 2024_10_03_14_28_41 git:(main) ✗ tree 14 | . 15 | ├── DigiCert_Secure_Site_Pro_CN_CA_G3.crt 16 | ├── DigiCert_Secure_Site_Pro_CN_CA_G3.key 17 | ├── bundle.crt 18 | ├── bundle.key 19 | ├── www.baidu.cn.crt 20 | └── www.baidu.cn.key 21 | ``` 22 | 23 | 其中 bundle 为合并证书链之后的文件。 24 | 25 | ## demo 26 | 27 | ![](assets/real-aliyun.png) 28 | 29 | ![](assets/self-signed-aliyun.png) 30 | -------------------------------------------------------------------------------- /assets/real-aliyun.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virusdefender/copy-cert/97e4d139583c19a46de2720092db20e5213bfae4/assets/real-aliyun.png -------------------------------------------------------------------------------- /assets/self-signed-aliyun.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virusdefender/copy-cert/97e4d139583c19a46de2720092db20e5213bfae4/assets/self-signed-aliyun.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/virusdefender/copy-cert 2 | 3 | go 1.21 4 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | "crypto/rand" 6 | "crypto/rsa" 7 | "crypto/tls" 8 | "crypto/x509" 9 | "encoding/pem" 10 | "fmt" 11 | "log" 12 | "os" 13 | "path/filepath" 14 | "regexp" 15 | "slices" 16 | "strings" 17 | "time" 18 | ) 19 | 20 | type certPair struct { 21 | originCert *x509.Certificate 22 | newCert *x509.Certificate 23 | newCertPem []byte 24 | priv interface{} 25 | privPem []byte 26 | } 27 | 28 | func getCertsFromNetwork(addr string) ([]*x509.Certificate, error) { 29 | conf := &tls.Config{ 30 | InsecureSkipVerify: false, 31 | } 32 | conn, err := tls.Dial("tcp", addr, conf) 33 | if err != nil { 34 | return nil, err 35 | } 36 | defer conn.Close() 37 | return conn.ConnectionState().PeerCertificates, nil 38 | } 39 | 40 | func makeCerts(originCerts []*x509.Certificate) ([]*certPair, error) { 41 | certs := make([]*certPair, len(originCerts)) 42 | // the origin order: website cert, intermediate ca, root ca 43 | for idx, cert := range originCerts { 44 | log.Printf("got cert: %s", cert.Subject.CommonName) 45 | certs[idx] = &certPair{originCert: cert} 46 | } 47 | slices.Reverse(certs) 48 | 49 | for idx, pair := range certs { 50 | var pub interface{} 51 | switch pair.originCert.PublicKey.(type) { 52 | case *rsa.PublicKey: 53 | p, err := rsa.GenerateKey(rand.Reader, pair.originCert.PublicKey.(*rsa.PublicKey).Size()*8) 54 | if err != nil { 55 | return nil, fmt.Errorf("generate rsa key: %w", err) 56 | } 57 | pub = &p.PublicKey 58 | pair.priv = p 59 | pair.privPem = pem.EncodeToMemory(&pem.Block{Bytes: x509.MarshalPKCS1PrivateKey(p), Type: "RSA PRIVATE KEY"}) 60 | case *ecdsa.PublicKey: 61 | p, err := ecdsa.GenerateKey(pair.originCert.PublicKey.(*ecdsa.PublicKey).Curve, rand.Reader) 62 | if err != nil { 63 | return nil, fmt.Errorf("generate ec key: %w", err) 64 | } 65 | pub = &p.PublicKey 66 | pair.priv = p 67 | data, err := x509.MarshalPKCS8PrivateKey(p) 68 | if err != nil { 69 | return nil, fmt.Errorf("MarshalPKCS8PrivateKey: %w", err) 70 | } 71 | pair.privPem = pem.EncodeToMemory(&pem.Block{Bytes: data, Type: "EC PRIVATE KEY"}) 72 | default: 73 | return nil, fmt.Errorf("unknown key type: %T", pair.originCert.PublicKey) 74 | } 75 | 76 | // remove the old public key (from the origin website cert) 77 | pair.originCert.PublicKey = nil 78 | // wo do not generate the root ca, the intermediate ca will be self-signed, 79 | // so the origin signature algorithm may be wrong 80 | pair.originCert.SignatureAlgorithm = x509.UnknownSignatureAlgorithm 81 | pair.newCert = pair.originCert 82 | var parent *certPair 83 | 84 | if idx > 0 { 85 | parent = certs[idx-1] 86 | } else { 87 | parent = pair 88 | } 89 | 90 | derBytes, err := x509.CreateCertificate(rand.Reader, pair.originCert, parent.newCert, pub, parent.priv) 91 | if err != nil { 92 | return nil, fmt.Errorf("CreateCertificate: %w", err) 93 | } 94 | pair.newCertPem = pem.EncodeToMemory(&pem.Block{Bytes: derBytes, Type: "CERTIFICATE"}) 95 | cert, err := x509.ParseCertificate(derBytes) 96 | if err != nil { 97 | return nil, fmt.Errorf("ParseCertificate: %w", err) 98 | } 99 | pair.newCert = cert 100 | } 101 | return certs, nil 102 | } 103 | 104 | var fileNameRegex = regexp.MustCompile(`[^a-zA-Z0-9_\-.]`) 105 | 106 | func main() { 107 | if len(os.Args) != 2 { 108 | name := filepath.Base(os.Args[0]) 109 | log.Fatalf("usage: %s $addr, for example: %s github.com:443", name, name) 110 | } 111 | certs, err := getCertsFromNetwork(os.Args[1]) 112 | if err != nil { 113 | log.Fatal(err) 114 | } 115 | newCerts, err := makeCerts(certs) 116 | if err != nil { 117 | log.Fatal(err) 118 | } 119 | slices.Reverse(newCerts) 120 | 121 | dir := filepath.Join("certs", time.Now().Local().Format("2006_01_02_15_04_05")) 122 | err = os.MkdirAll(dir, 0o744) 123 | if err != nil { 124 | log.Fatal(err) 125 | } 126 | 127 | bundleCert, err := os.OpenFile(filepath.Join(dir, "bundle.crt"), os.O_WRONLY|os.O_CREATE, 0o744) 128 | if err != nil { 129 | log.Fatal(err) 130 | } 131 | defer bundleCert.Close() 132 | bundleKey, err := os.OpenFile(filepath.Join(dir, "bundle.key"), os.O_WRONLY|os.O_CREATE, 0o744) 133 | if err != nil { 134 | log.Fatal(err) 135 | } 136 | defer bundleKey.Close() 137 | 138 | for _, pair := range newCerts { 139 | log.Printf("going to write new cert and key: %s", pair.newCert.Subject.CommonName) 140 | // 担心星号在 Windows 上是不合法的文件名(当然我也没测试),但是被替换为下换线又很奇怪,所以替换成 __wildcard__ 141 | pathBase := strings.ReplaceAll(pair.newCert.Subject.CommonName, "*", "__wildcard__") 142 | pathBase = fileNameRegex.ReplaceAllString(pathBase, "_") 143 | err = os.WriteFile(filepath.Join(dir, pathBase+".crt"), pair.newCertPem, 0o744) 144 | if err != nil { 145 | log.Fatal(err) 146 | } 147 | _, err = bundleCert.Write(pair.newCertPem) 148 | if err != nil { 149 | log.Fatal(err) 150 | } 151 | 152 | err = os.WriteFile(filepath.Join(dir, pathBase+".key"), pair.privPem, 0o744) 153 | if err != nil { 154 | log.Fatal(err) 155 | } 156 | _, err = bundleKey.Write(pair.privPem) 157 | if err != nil { 158 | log.Fatal(err) 159 | } 160 | } 161 | log.Printf("certs save to %s", dir) 162 | } 163 | --------------------------------------------------------------------------------