CoinMarketCap captcha: Part 2, creating a solver
August 12, 2022
• obfio
In this post, we will be continuing what we did in part 1, focusing on making a captcha solver for the CoinMarketCap mobile captcha. This will be split into a few parts:
This article may have been republished from another source and might not have been originally written for this site.⚠️ Some information, tools, or techniques discussed may have changed or evolved since the publishing of this article.
Originally published at https://antibot.blog/posts/1741831465176
Intro
In this post, we will be continuing what we did in part 1, focusing on making a captcha solver for the CoinMarketCap mobile captcha. This will be split into a few parts:
Setting up the enviornment
First it's important to say which HTTP library we will be using, I prefer to use github.com/bogdanfinn/fhttp and github.com/bogdanfinn/tls-client to get valid TLS signatures.
Next is the general file structure, I prefer to make a folder named coinmarketcap, making 3 files called client.go, http.go, and structs.go. This is the file structure I use for every project, I find it easier to keep a consistent file structure between projects.
First, this is client.go:
1package coinmarketcap
2
3import (
4 tlsclient "github.com/bogdanfinn/tls-client"
5 "github.com/bogdanfinn/tls-client/profiles"
6)
7
8type Client struct {
9 HTTPClient tlsclient.HttpClient
10 Proxy string
11}
12
13func MakeClient(proxy string) *Client {
14 opts := []tlsclient.HttpClientOption{
15 tlsclient.WithTimeoutSeconds(10),
16 tlsclient.WithProxyUrl(proxy),
17 tlsclient.WithInsecureSkipVerify(),
18 tlsclient.WithClientProfile(profiles.Okhttp4Android12),
19 }
20 h, err := tlsclient.NewHttpClient(nil, opts...)
21 if err != nil {
22 panic(err)
23 }
24 c := &Client{
25 HTTPClient: h,
26 Proxy: proxy,
27 }
28 return c
29}The other 2 files will stay empty for now, we will also not be using a proxy in this project but my setup ofc is default supporting proxies. Now we're all setup, let's move on.
Getting example images
Referring back to part 1, the request that gives us our image is https://api.commonservice.io/gateway-api/v1/public/antibot/getCaptcha, let's setup this request in http.go. For now, we will use the default Device-Info header:
1// file: ./coinmarketcap/http.go
2package coinmarketcap
3
4import (
5 "encoding/json"
6 "errors"
7 http "github.com/bogdanfinn/fhttp"
8 "io"
9 "strings"
10)
11
12func (c *Client) GetCaptcha() (*Captcha, error) {
13
14 req, err := http.NewRequest("POST", "https://api.commonservice.io/gateway-api/v1/public/antibot/getCaptcha", strings.NewReader("bizId=CMC_register&sv=20220812&snv=1.4.7&lang=en&securityCheckResponseValidateId=CMC_register&clientType=android"))
15 if err != nil {
16 return nil, err
17 }
18 req.Header.Set("Fvideo-Id", "xxx")
19 req.Header.Set("User-Agent", "Binance/1.0.0 Mozilla/5.0 (Linux; Android 12; SM-A528B Build/V417IR; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/91.0.4472.114 Safari/537.36 Captcha/1.0.0 (Android 1.0.0) SecVersion/1.4.7")
20 req.Header.Set("Content-Type", "text/plain; charset=UTF-8")
21 req.Header.Set("Captcha-Sdk-Version", "1.0.0")
22 req.Header.Set("Device-Info", "eyJzY3JlZW5fcmVzb2x1dGlvbiI6IjYwMCwxMDY3IiwiYXZhaWxhYmxlX3NjcmVlbl9yZXNvbHV0aW9uIjoiNjAwLDEwNjciLCJzeXN0ZW1fdmVyc2lvbiI6InVua25vd24iLCJicmFuZF9tb2RlbCI6InVua25vd24iLCJ0aW1lem9uZSI6IkFtZXJpY2EvTmV3X1lvcmsiLCJ0aW1lem9uZU9mZnNldCI6MjQwLCJ1c2VyX2FnZW50IjoiQmluYW5jZS8xLjAuMCBNb3ppbGxhLzUuMCAoTGludXg7IEFuZHJvaWQgMTI7IFNNLUE1MjhCIEJ1aWxkL1Y0MTdJUjsgd3YpIEFwcGxlV2ViS2l0LzUzNy4zNiAoS0hUTUwsIGxpa2UgR2Vja28pIFZlcnNpb24vNC4wIENocm9tZS85MS4wLjQ0NzIuMTE0IFNhZmFyaS81MzcuMzYgQ2FwdGNoYS8xLjAuMCAoQW5kcm9pZCAxLjAuMCkgU2VjVmVyc2lvbi8xLjQuNyIsImxpc3RfcGx1Z2luIjoiIiwicGxhdGZvcm0iOiJMaW51eCBpNjg2Iiwid2ViZ2xfdmVuZG9yIjoidW5rbm93biIsIndlYmdsX3JlbmRlcmVyIjoidW5rbm93biJ9")
23 req.Header.Set("Bnc-Uuid", "xxx")
24 req.Header.Set("Clienttype", "android")
25 req.Header.Set("X-Captcha-Se", "true")
26 req.Header.Set("Accept", "*/*")
27 req.Header.Set("Origin", "https://staticrecap.cgicgi.io")
28 req.Header.Set("X-Requested-With", "com.coinmarketcap.android")
29 req.Header.Set("Sec-Fetch-Site", "cross-site")
30 req.Header.Set("Sec-Fetch-Mode", "cors")
31 req.Header.Set("Sec-Fetch-Dest", "empty")
32 req.Header.Set("Referer", "https://static://staticrecap.cgicgi.io/")
33 req.Header.Set("Accept-Language", "en,en-US;q=0.9")
34 resp, err := c.HTTPClient.Do(req)
35 if err != nil {
36 return nil, err
37 }
38 defer resp.Body.Close()
39 b, err := io.ReadAll(resp.Body)
40 if err != nil {
41 return nil, err
42 }
43 if resp.StatusCode != 200 {
44 return nil, errors.New(resp.Status)
45 }
46 f := &Captcha{}
47 err = json.Unmarshal(b, f)
48 if err != nil {
49 return nil, err
50 }
51 return f, nil
52}
53
54func (c *Client) GetImage(path string) ([]byte, error) {
55 req, err := http.NewRequest("GET", "https://staticrecap.cgicgi.io"+path, nil)
56 if err != nil {
57 return nil, err
58 }
59 req.Header.Set("User-Agent", "Binance/1.0.0 Mozilla/5.0 (Linux; Android 12; SM-A528B Build/V417IR; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/91.0.4472.114 Safari/537.36 Captcha/1.0.0 (Android 1.0.0) SecVersion/1.4.7")
60 req.Header.Set("Accept", "*/*")
61 req.Header.Set("Origin", "https://staticrecap.cgicgi.io")
62 resp, err := http.DefaultClient.Do(req)
63 if err != nil {
64 return nil, err
65 }
66 defer resp.Body.Close()
67 b, err := io.ReadAll(resp.Body)
68 if err != nil {
69 return nil, err
70 }
71 return b, nil
72}
73// file: ./coinmarketcap/structs.go
74package coinmarketcap
75
76type Captcha struct {
77 Code string `json:"code"`
78 Data struct {
79 Sig string `json:"sig"`
80 Salt string `json:"salt"`
81 Path2 string `json:"path2"`
82 Ek string `json:"ek"`
83 CaptchaType string `json:"captchaType"`
84 Tag string `json:"tag"`
85 Fb string `json:"fb"`
86 I18N string `json:"i18n"`
87 } `json:"data"`
88 Success bool `json:"success"`
89}Now we add these 2 methods to main.go and save our image to a folder named ./examples.go:
1package main
2
3import (
4 "fmt"
5 "github.com/obfio/cmc-solve-image/coinmarketcap"
6 "os"
7 "strings"
8)
9
10func main() {
11 client := coinmarketcap.MakeClient("")
12 captcha, err := client.GetCaptcha()
13 if err != nil {
14 panic(err)
15 }
16 fmt.Printf("%+v\n", captcha)
17 imageBytes, err := client.GetImage(captcha.Data.Path2)
18 if err != nil {
19 panic(err)
20 }
21 os.WriteFile(fmt.Sprintf("./examples/%s", strings.Split(captcha.Data.Path2, "/")[7]), imageBytes, 0666)
22}Great, now we can get images in real time to get quite a few samples. Now let's get 1 sample and work on making a solver for it.
Solving without OCR
My first idea for this was that the captcha is split into 2 pieces, one with the puzzle piece, one with the actual image we need to solve. This is very helpful, the puzzle piece is always the same size and the left side is always going to be transparent everywhere but the puzzle piece. This means we can get the y position of where the puzzle piece needs to be.
The first thing I'm going to do is make a new package named solve with a file named solve.go, this will have an exported function called SolveImage that takes in a type image.Image and outputs an int. This int will be how far we need to move the puzzle piece to solve the image. This value will be used in the final payload:
1// file: ./solve/solve.go
2func SolveImage(img image.Image) int {
3
4 return 0
5}Now I'm going to make a function that takes in that image.Image and splits it into 2 images, as well as getting the middle x of the puzzle piece, we will use this value later to identify the y of the puzzle piece. We will be saving the puzzle piece image and the solving image as well to piece.png and target.png:
1// file: ./solve/solve.go
2func SolveImage(img image.Image) int {
3 // Split the image into puzzle piece image and target image, and save them
4 puzzlePieceImage, targetImage, puzzlePieceMiddle := splitImages(img)
5 saveImage(puzzlePieceImage, "./examples/piece.png")
6 saveImage(targetImage, "./examples/target.png")
7 fmt.Println(puzzlePieceMiddle)
8 return 0
9}
10
11func splitImages(img image.Image) (image.Image, image.Image, int) {
12 bounds := img.Bounds()
13
14 var splitX int
15 for x := bounds.Min.X; x < bounds.Max.X; x++ {
16 r, g, b, _ := img.At(x, 0).RGBA()
17 if r != 0 && g != 0 && b != 0 {
18 splitX = x
19 break
20 }
21 }
22 puzzleRect := image.Rect(bounds.Min.X, bounds.Min.Y, splitX, bounds.Max.Y)
23 targetRect := image.Rect(splitX, bounds.Min.Y, bounds.Max.X, bounds.Max.Y)
24
25 puzzleImg := image.NewRGBA(puzzleRect)
26 draw.Draw(puzzleImg, puzzleRect, img, bounds.Min, draw.Src)
27
28 targetImg := image.NewRGBA(targetRect)
29 draw.Draw(targetImg, targetRect, img, image.Point{X: splitX, Y: 0}, draw.Src)
30
31 return puzzleImg, targetImg, splitX / 2
32}
33
34func saveImage(img image.Image, path string) error {
35 file, err := os.Create(path)
36 if err != nil {
37 return err
38 }
39 defer file.Close()
40
41 return png.Encode(file, img)
42}The way this works is by finding the starting point of the non-transparent part of the image on the x axis, then it splits the image into 2 images at that point. It then returns that x position /2 to get the middle of the puzzle piece image.
Next, we need to get the y of the puzzle piece in the puzzle piece image. Realistically, we only have to get any part of the puzzle piece but the deeper into the puzzle piece that we get, the better. To do this, we will find which row in the image has the most color difference from 0, aka transparent:
1// file: ./solve/solve.go
2func SolveImage(img image.Image) int {
3 // Split the image into puzzle piece image and target image, and save them
4 puzzlePieceImage, targetImage, puzzlePieceMiddle := splitImages(img)
5 saveImage(puzzlePieceImage, "./examples/piece.png")
6 saveImage(targetImage, "./examples/target.png")
7 fmt.Println(puzzlePieceMiddle)
8
9 // Find the dominant row in the puzzle piece image
10 dominantRow := findDominantRow(puzzlePieceImage, puzzlePieceMiddle)
11 fmt.Println(dominantRow)
12 return 0
13}
14
15func findDominantRow(img image.Image, middle int) int {
16 bounds := img.Bounds()
17 rowPixelCount := make(map[int]int)
18 maxPixels := 0
19 dominantRow := 0
20 for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
21 r, g, b, _ := img.At(middle, y).RGBA()
22 if r != 0 && g != 0 && b != 0 {
23 rowPixelCount[y]++
24 if rowPixelCount[y] > maxPixels {
25 maxPixels = rowPixelCount[y]
26 dominantRow = y
27 }
28 }
29 }
30 return dominantRow
31}To confirm our output, we can write code to put a red pixel on the x,y position:
As you can see, the pixel it's getting it very close to the top of the image, however, it is on the image which is good. Instead of adding complexity, let's just add 20 to dominantRow:
1func SolveImage(img image.Image) int {
2 // Split the image into puzzle piece image and target image, and save them
3 puzzlePieceImage, targetImage, puzzlePieceMiddle := splitImages(img)
4 saveImage(puzzlePieceImage, "./examples/piece.png")
5 saveImage(targetImage, "./examples/target.png")
6 fmt.Println(puzzlePieceMiddle)
7
8 // Find the dominant row in the puzzle piece image
9 dominantRow := findDominantRow(puzzlePieceImage, puzzlePieceMiddle)
10 fmt.Println(dominantRow)
11
12 // add 20 to dominantRow
13 dominantRow += 20
14
15 // make red pixel at `x,y` of puzzlePieceImage
16 bounds := puzzlePieceImage.Bounds()
17 rgba := image.NewRGBA(bounds)
18 draw.Draw(rgba, bounds, img, bounds.Min, draw.Src)
19 rgba.Set(puzzlePieceMiddle, dominantRow, color.RGBA{R: 255, G: 0, B: 0, A: 255})
20 saveImage(rgba, "./examples/piece.png")
21 return 0
22}Now we see this:
This is much better, having the x,y deeper into the image should increase the solve rate.
The final thing we need to do is determine where on the image the piece needs to go. To do this, let's look at the image and see what we can do. The first thing I notice is that the space the piece needs to go is shaded, as a gray color. My first thought when I saw this is that I should look on that y axis and scan which pixel on the x axis is darkest, let's try that, again we're going to place a red pixel at the x,y it found:
1// file: ./solve/solve.go
2func SolveImage(img image.Image) int {
3 // Split the image into puzzle piece image and target image, and save them
4 puzzlePieceImage, targetImage, puzzlePieceMiddle := splitImages(img)
5 saveImage(puzzlePieceImage, "./examples/piece.png")
6 saveImage(targetImage, "./examples/target.png")
7 fmt.Println(puzzlePieceMiddle)
8
9 // Find the dominant row in the puzzle piece image
10 dominantRow := findDominantRow(puzzlePieceImage, puzzlePieceMiddle)
11 fmt.Println(dominantRow)
12
13 // add 20 to dominantRow
14 dominantRow += 20
15
16 // make red pixel at `x,y` of puzzlePieceImage
17 bounds := puzzlePieceImage.Bounds()
18 rgba := image.NewRGBA(bounds)
19 draw.Draw(rgba, bounds, img, bounds.Min, draw.Src)
20 rgba.Set(puzzlePieceMiddle, dominantRow, color.RGBA{R: 255, G: 0, B: 0, A: 255})
21 saveImage(rgba, "./examples/piece.png")
22
23 // get the place on the image where the shading is most defined
24 shadedX := findShadedArea(targetImage, dominantRow)
25 bounds = targetImage.Bounds()
26 rgba = image.NewRGBA(bounds)
27 draw.Draw(rgba, bounds, img, bounds.Min, draw.Src)
28 rgba.Set(shadedX, dominantRow, color.RGBA{R: 255, G: 0, B: 0, A: 255})
29 saveImage(rgba, "./examples/target.png")
30 return shadedX
31}
32
33func findShadedArea(img image.Image, row int) int {
34 bounds := img.Bounds()
35 xStart := -1
36 minIntensity := math.MaxFloat64
37 for x := bounds.Min.X + 60; x < bounds.Max.X; x++ {
38 c := img.At(x, row)
39 r, g, b, _ := c.RGBA()
40 r8 := float64(r >> 8)
41 g8 := float64(g >> 8)
42 b8 := float64(b >> 8)
43 intensity := r8 + g8 + b8
44 if intensity < minIntensity {
45 if intensity+60 < minIntensity {
46 xStart = x
47 }
48 minIntensity = intensity
49 }
50 }
51 return xStart
52}After running this code, I see this: You can see that it is finding a pixel on the target. This is great! Now we can move onto generating the payload.
Payload Generation
In order to generate our payload, let's create a struct for it and give that struct a FillPayload method that will fill the payload with everything we need. First I will show you all the code I got, then I will explain what I think needs explaining:
1// file: ./main.go
2package main
3
4import (
5 "bytes"
6 "fmt"
7 "github.com/obfio/cmc-solve-image/coinmarketcap"
8 "github.com/obfio/cmc-solve-image/solve"
9 "image/png"
10 "os"
11 "strings"
12)
13
14func main() {
15 client := coinmarketcap.MakeClient("")
16 captcha, err := client.GetCaptcha()
17 if err != nil {
18 panic(err)
19 }
20 fmt.Printf("%+v\n", captcha)
21 imageBytes, err := client.GetImage(captcha.Data.Path2)
22 if err != nil {
23 panic(err)
24 }
25 os.WriteFile(fmt.Sprintf("./examples/%s", strings.Split(captcha.Data.Path2, "/")[7]), imageBytes, 0666)
26 // convert bytes to png
27 img, err := png.Decode(bytes.NewReader(imageBytes))
28 if err != nil {
29 panic(err)
30 }
31 solvedX := solve.SolveImage(img)
32 payload := &coinmarketcap.Payload{}
33 payload.FillPayload(solvedX)
34 fmt.Printf("%+v\n", payload)
35}
36
37// file: ./coinmarketcap/payload.go
38package coinmarketcap
39
40import (
41 "fmt"
42 "math"
43 "math/rand"
44 "time"
45)
46
47const (
48 Platform = "Linux i686"
49 InnerHeight = 355
50 OuterHeight = "355"
51 PuzzlePieceWidth = 44
52 PuzzlePieceHeight = 44
53 ImageWidth = "310"
54)
55
56func (p *Payload) FillPayload(solvedX int) {
57 // ENVIRONMENT
58 p.Ev.Wd = genOddNum()
59 p.Ev.Im = genEvenNum()
60 p.Ev.Prde = fmt.Sprintf("%v,%v,%v,%v", genOddNum(), genOddNum(), genOddNum(), genOddNum())
61 p.Ev.Brla = genOddNum()
62 p.Ev.Pl = Platform
63 p.Ev.Wiinhe = InnerHeight
64 p.Ev.Wiouhe = OuterHeight
65
66 MACT, finalDist := generateMACT(solvedX, 290)
67 p.Be.El = MACT
68 p.Be.Ec.Ts = 1
69 p.Be.Ec.Tm = len(MACT) - 2
70 p.Be.Ec.Te = 1
71
72 // PUZZLE PIECE
73 p.Be.Th.El = []string{}
74 p.Be.Th.Si.W = PuzzlePieceWidth
75 p.Be.Th.Si.H = PuzzlePieceHeight
76
77 // FINAL X WHERE THE SLIDER LANDS, MINUS THE OFFSET
78 p.Dist = finalDist
79
80 // IMAGE WIDTH, HARD CODED
81 p.ImageWidth = ImageWidth
82}
83
84func generateMACT(xEnd, yDom int) ([]string, int) {
85 xStart := rand.Intn(40) + 20
86 xStartCopy := xStart + 1
87 xEndMinusOffset := xEnd - 60
88 yStart := yDom + 1
89 eventsNeeded := int(math.Ceil(float64(xEnd) / 6.0))
90 timestampStart := time.Now().UnixMilli()
91 totalTimeUsed := 0
92 output := []string{}
93 for i := 0; i < eventsNeeded; i++ {
94 yStart = newY(yStart)
95 xStart = newX(xStart)
96 timeTaken := getTimeTaken(i)
97
98 totalTimeUsed += timeTaken
99
100 output = append(output, fmt.Sprintf("|tm|%v,%v|%v|1", xStart, yStart, timeTaken))
101
102 if xStart >= xEnd {
103 break
104 }
105 }
106 timeTaken := getTimeTaken(999)
107 output = append(output, fmt.Sprintf("|te||%v|1", timeTaken))
108 totalTimeUsed += timeTaken
109 time.Sleep(time.Duration(totalTimeUsed*2) * time.Millisecond)
110 finalOutput := []string{fmt.Sprintf("|ts|%v,%v|%v|1", xStartCopy, yDom, timestampStart)}
111 for _, o := range output {
112 finalOutput = append(finalOutput, o)
113 }
114 return finalOutput, xEndMinusOffset
115}
116
117func getTimeTaken(i int) int {
118 if i == 0 {
119 return rand.Intn(20) + 80
120 }
121 if i == 999 {
122 return rand.Intn(50) + 200
123 }
124 return rand.Intn(7) + 13
125}
126
127func newX(xStart int) int {
128 amtToAdd := rand.Intn(6) + 3
129 return xStart + amtToAdd
130}
131
132func newY(yStart int) int {
133 // get if change y
134 changeY := randBool()
135 // get if change up or down
136 var changeYUp bool
137 if changeY {
138 changeYUp = randBool()
139 }
140 // get how much to change
141 var changeYAmount int
142 if changeY {
143 changeYAmount = rand.Intn(3)
144 if changeYUp {
145 yStart += changeYAmount
146 } else {
147 yStart -= changeYAmount
148 }
149 }
150 return yStart
151}
152
153func randBool() bool {
154 return rand.Intn(2) == 1
155}
156
157func genOddNum() int {
158 return (rand.Intn(50) * 2) + 1
159}
160
161func genEvenNum() int {
162 return rand.Intn(50) * 2
163}
164
165// file: ./coinmarketcap/structs.go
166type Payload struct {
167 Ev struct {
168 Wd int `json:"wd"`
169 Im int `json:"im"`
170 De string `json:"de"`
171 Prde string `json:"prde"`
172 Brla int `json:"brla"`
173 Pl string `json:"pl"`
174 Wiinhe int `json:"wiinhe"`
175 Wiouhe string `json:"wiouhe"`
176 } `json:"ev"`
177 Be struct {
178 Ec struct {
179 Ts int `json:"ts"`
180 Tm int `json:"tm"`
181 Te int `json:"te"`
182 } `json:"ec"`
183 El []string `json:"el"`
184 Th struct {
185 El []string `json:"el"`
186 Si struct {
187 W int `json:"w"`
188 H int `json:"h"`
189 } `json:"si"`
190 } `json:"th"`
191 } `json:"be"`
192 Dist int `json:"dist"`
193 ImageWidth string `json:"imageWidth"`
194}The first thing I think that needs explaining is generateMACT. This is basically just randomness. There should be 1 touch_start event, x touch_move event, and 1 touch_end event to simulate what a user does with the touch the slider, move it to the desired location, then let go. First we need to start somewhere on the slider, that is why xStart has a minimum of 20 and a maximum of 60, 20 is the point on the image that the slider starts and 60 is where the slider ends. Next we give the xEnd an offset of 60, this is because the x position we got earlier is including the 60 pixels of blank space, that should not be included in the x position of the touch event. To decide how many events to use, we will use xEnd / 6 to get this value, this means that every 6 pixels will result in a touch_move event. Everything else is just randomness, we get a random time between events, a random movement, rather that's moving x or y, and rather to move y up or down for that event. Last, we just add out touch_end and touch_start events, as well as waiting x ms, this is because if the final MS timestamp we have is > the current time, it will ofc detect us as a bot.
That was a lot ot take in at once, however, I hope I explained it well enough. The only other thing I think needs explaining is how PuzzlePieceWidth/Height and ImageWidth are hard coded, I took a few samples and found out that these values are always the same, therefore, we can hardcode them! Now we can move on to encoding the payload!
Encoding the payload
When it comes to encoding the payload, I actually wrote this code a long time ago and wasn't 100% confident that the long functions we saw that ended up doing nothing to the payload in part 1 were actually doing nothing, therefore, I coded all that into it the payload encoding.
In general, this code does not need explaining, it's really just taking the JavaScript code and making it GoLang. The only part that I will explain is my use of this trippleShift function. When reverse engineering, you can sometimes find differences between languages, this time being that golang has no >>> operator, therefore, I made my own function that acts the same.
Now this is the code:
1// file: ./coinmarketcap/encoding.go
2package cmc
3
4import (
5 "encoding/json"
6 "strings"
7)
8
9func (p *Payload) Encode(key string) string {
10 key = reverseString(key)
11 key = key + encodeXORKey(key)
12 payloadBytes, _ := json.Marshal(p)
13 UTF8PayloadBytes := UTF8(payloadBytes)
14 XORDBytes := []byte{}
15 for i := 0; i < len(UTF8PayloadBytes); i++ {
16 XORDBytes = append(XORDBytes, []byte(fromCharCode(int(UTF8PayloadBytes[i]) ^ charCodeAt(key, i%len(key))))[0])
17 }
18 return btoa(XORDBytes)
19}
20
21// =======================================
22// == payload enc stuff ==
23// =======================================
24func UTF8(payload []byte) []byte {
25 manipulatedBytes := K(payload)
26 out := []byte{}
27 for i := 0; i < len(manipulatedBytes); i++ {
28 char := manipulatedBytes[i]
29 out = append(out, L(char)...)
30 }
31 return out
32}
33
34func L(c byte) []byte {
35 char := int(c)
36 test := 1
37 if 0&(trippleShift(char, 16)&65535) == 0 {
38 test = 0
39 }
40 if (char & 65408) == test {
41 return []byte(fromCharCode(char))
42 }
43 out := []byte{}
44 if (char&63488) == 0 && (trippleShift(char, 16)&65535) == 0 {
45 out = []byte(fromCharCode(char>>6&31 | 192))
46 } else if (char&0) == 0 && (trippleShift(char, 16)&65535) == 0 {
47 J(char)
48 out = []byte(fromCharCode(char>>12&15 | 224))
49 out = append(out, I(char, 6))
50 } else if (char&0) == 0 && (trippleShift(char, 16)&65504) == 0 {
51 out = []byte(fromCharCode(char>>18&7 | 240))
52 out = append(out, I(char, 12))
53 out = append(out, I(char, 6))
54 }
55 out = append(out, []byte(fromCharCode(char&63|128))...)
56 return out
57}
58
59func I(char, num int) byte {
60 return byte(char>>num&63 | 128)
61}
62
63func J(char int) {
64 if char >= 55296 && char <= 57343 {
65 panic("not a scalar value")
66 }
67}
68
69func K(payload []byte) []byte {
70 out := []byte{}
71 bytesAdded := 0
72 payloadLen := len(payload)
73 byteValue := 0
74 nextByte := 0
75 for bytesAdded < payloadLen {
76 byteValue = int(payload[bytesAdded])
77 bytesAdded++
78 if byteValue >= 55296 && byteValue <= 56319 && bytesAdded < payloadLen {
79 nextByte = int(payload[bytesAdded])
80 bytesAdded++
81 bytesAdded++
82 if (nextByte & 64512) == 56320 {
83 out = append(out, byte(((byteValue&1023)<<10)+(nextByte&1023)+65536))
84 } else {
85 out = append(out, byte(byteValue))
86 bytesAdded--
87 }
88 } else {
89 out = append(out, byte(byteValue))
90 }
91 }
92 return out
93}
94
95// =======================================
96// == XOR key stuff ==
97// =======================================
98
99func encodeXORKey(key string) string {
100 output := []string{}
101 chars := strings.Split("abcdhijkxy", "")
102 for i := 0; i < 4; i++ {
103 charPos := 0
104 keyPos := 1
105 if i == 3 {
106 keyPos = 1 + len(key)%4
107 }
108 for x := 0; x < keyPos; x++ {
109 charCodePos := i + x
110 if x < len(key) {
111 charPos = charPos + charCodeAt(key, charCodePos)
112 }
113 }
114 charPos = charPos * 31
115 output = append(output, chars[charPos%len(chars)])
116 }
117 return strings.Join(output, "")
118}
119
120func reverseString(str string) string {
121 runes := []rune(str)
122 for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
123 runes[i], runes[j] = runes[j], runes[i]
124 }
125 return string(runes)
126}
127
128// file: ./coinmarketcap/encodingUtils.go
129package cmc
130
131import (
132 "crypto/sha512"
133 "encoding/base64"
134 "encoding/hex"
135)
136
137func btoa(str []byte) string {
138 return base64.StdEncoding.EncodeToString(str)
139}
140
141func charCodeAt(a string, i int) int {
142 return int(a[i])
143}
144
145// FromCharCode https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/FromCharCode
146func fromCharCode(c int) string {
147 return string(rune(c))
148}
149
150// >>> operator in golang
151func trippleShift(num, t int) int {
152 overflow := int32(num)
153 return int(uint32(overflow) >> t)
154}
155
156func shaPassword(password string) string {
157 sha512Hasher := sha512.New()
158 sha512Hasher.Write([]byte(password))
159 return hex.EncodeToString(sha512Hasher.Sum(nil))
160}You can see that, simply to make the process of porting code easier, I made helper functions like fromCharCode, charCodeAt and btoa. I would highly recommend doing this in your own projects, it makes porting code much much easier in my opinion.
There's one more thing that we need to discuss here that I actually missed in part 1. There is a value in validateCaptcha that I never went over called s. We can pretend this is a challenge for the readers. I challenge you to, without looking at the code on the GitHub, to reverse engineer this s value and maybe even port the code yourself. For this reason, I won't show the code for it in this blog, it will be in the GitHub repository though!
Hooking it all together
First, we need to add code to main.go to get the encoded payload and the s value:
1// file: ./main.go
2func main() {
3 client := coinmarketcap.MakeClient("")
4 captcha, err := client.GetCaptcha()
5 if err != nil {
6 panic(err)
7 }
8 fmt.Printf("%+v\n", captcha)
9 if captcha.Data.CaptchaType != "SLIDE" {
10 panic("captcha type is " + captcha.Data.CaptchaType)
11 }
12 imageBytes, err := client.GetImage(captcha.Data.Path2)
13 if err != nil {
14 panic(err)
15 }
16 os.WriteFile(fmt.Sprintf("./examples/%s", strings.Split(captcha.Data.Path2, "/")[7]), imageBytes, 0666)
17
18 // convert bytes to png
19 img, err := png.Decode(bytes.NewReader(imageBytes))
20 if err != nil {
21 panic(err)
22 }
23
24 // solve image
25 solvedX := solve.SolveImage(img)
26
27 // make the payload
28 payload := &coinmarketcap.Payload{}
29 payload.FillPayload(solvedX)
30 fmt.Printf("%+v\n", payload)
31
32 // get encoded payload
33 encodedPayload := payload.Encode(captcha.Data.Ek)
34
35 // get S value
36 sValue := payload.GenSValue(encodedPayload, captcha.Data.Sig, captcha.Data.Salt)
37}Next, we need to add a method to ./coinmarketcap/http.go to do validateCaptcha:
1// file: ./coinmarketcap/http.go
2func (c *Client) SolveCaptcha(sig, encodedPayload string, sValue int) (*SolveResponse, error) {
3 //bizId=CMC_register&sv=20220812&snv=1.4.7&lang=en&securityCheckResponseValidateId=CMC_register&clientType=android&sig=pUBDZytKUt6D1SSxq7H8JQEtYSiU4ywACNPeOnJT1DwXKt1c&data
4 t := url.Values{}
5 t.Set("data", encodedPayload)
6 t.Set("bizId", "CMC_register")
7 t.Set("sv", "20220812")
8 t.Set("snv", "1.4.7")
9 t.Set("lang", "en")
10 t.Set("securityCheckResponseValidateId", "CMC_register")
11 t.Set("clientType", "android")
12 t.Set("sig", sig)
13 t.Set("s", fmt.Sprintf("%v", sValue))
14 body := fmt.Sprintf(`bizId=CMC_register&sv=20220812&snv=1.4.7&lang=en&securityCheckResponseValidateId=CMC_register&clientType=android&sig=%s&data=%s&s=%v`, sig, strings.Split(t.Encode(), "=")[1], sValue)
15 req, err := http.NewRequest("POST", "https://api.commonservice.io/gateway-api/v1/public/antibot/validateCaptcha", strings.NewReader(body))
16 if err != nil {
17 return nil, err
18 }
19 req.Header.Set("Fvideo-Id", "xxx")
20 req.Header.Set("User-Agent", "Binance/1.0.0 Mozilla/5.0 (Linux; Android 12; SM-A528B Build/V417IR; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/91.0.4472.114 Safari/537.36 Captcha/1.0.0 (Android 1.0.0) SecVersion/1.4.7")
21 req.Header.Set("Content-Type", "text/plain; charset=UTF-8")
22 req.Header.Set("Captcha-Sdk-Version", "1.0.0")
23 req.Header.Set("Device-Info", "eyJzY3JlZW5fcmVzb2x1dGlvbiI6IjYwMCwxMDY3IiwiYXZhaWxhYmxlX3NjcmVlbl9yZXNvbHV0aW9uIjoiNjAwLDEwNjciLCJzeXN0ZW1fdmVyc2lvbiI6InVua25vd24iLCJicmFuZF9tb2RlbCI6InVua25vd24iLCJ0aW1lem9uZSI6IkFtZXJpY2EvTmV3X1lvcmsiLCJ0aW1lem9uZU9mZnNldCI6MjQwLCJ1c2VyX2FnZW50IjoiQmluYW5jZS8xLjAuMCBNb3ppbGxhLzUuMCAoTGludXg7IEFuZHJvaWQgMTI7IFNNLUE1MjhCIEJ1aWxkL1Y0MTdJUjsgd3YpIEFwcGxlV2ViS2l0LzUzNy4zNiAoS0hUTUwsIGxpa2UgR2Vja28pIFZlcnNpb24vNC4wIENocm9tZS85MS4wLjQ0NzIuMTE0IFNhZmFyaS81MzcuMzYgQ2FwdGNoYS8xLjAuMCAoQW5kcm9pZCAxLjAuMCkgU2VjVmVyc2lvbi8xLjQuNyIsImxpc3RfcGx1Z2luIjoiIiwicGxhdGZvcm0iOiJMaW51eCBpNjg2Iiwid2ViZ2xfdmVuZG9yIjoidW5rbm93biIsIndlYmdsX3JlbmRlcmVyIjoidW5rbm93biJ9")
24 req.Header.Set("Bnc-Uuid", "xxx")
25 req.Header.Set("Clienttype", "android")
26 req.Header.Set("X-Captcha-Se", "true")
27 req.Header.Set("Accept", "*/*")
28 req.Header.Set("Origin", "https://staticrecap.cgicgi.io")
29 req.Header.Set("X-Requested-With", "com.coinmarketcap.android")
30 req.Header.Set("Sec-Fetch-Site", "cross-site")
31 req.Header.Set("Sec-Fetch-Mode", "cors")
32 req.Header.Set("Sec-Fetch-Dest", "empty")
33 req.Header.Set("Referer", "https://static://staticrecap.cgicgi.io/")
34 req.Header.Set("Accept-Language", "en,en-US;q=0.9")
35 resp, err := c.HTTPClient.Do(req)
36 if err != nil {
37 return nil, err
38 }
39 defer resp.Body.Close()
40 if resp.StatusCode != 200 {
41 return nil, errors.New("invalid status code")
42 }
43 b, err := io.ReadAll(resp.Body)
44 if err != nil {
45 return nil, err
46 }
47 f := &SolveResponse{}
48 err = json.Unmarshal(b, &f)
49 if err != nil {
50 return nil, err
51 }
52 if !f.Success || f.Code != "000000" || f.Data.Token == "" {
53 return nil, errors.New(string(b))
54 }
55 return f, nil
56}
57
58
59// file: ./coinmarketcap/structs.go
60type SolveResponse struct {
61 Code string `json:"code"`
62 Data struct {
63 Result int `json:"result"`
64 Tag string `json:"tag"`
65 I18N string `json:"i18n"`
66 Token string `json:"token"`
67 } `json:"data"`
68 Success bool `json:"success"`
69}Now, to get the output captcha, let's just hook this up to main.go:
1// file: ./main.go
2func main() {
3 client := coinmarketcap.MakeClient("")
4 captcha, err := client.GetCaptcha()
5 if err != nil {
6 panic(err)
7 }
8 fmt.Printf("%+v\n", captcha)
9 if captcha.Data.CaptchaType != "SLIDE" {
10 panic("captcha type is " + captcha.Data.CaptchaType)
11 }
12 imageBytes, err := client.GetImage(captcha.Data.Path2)
13 if err != nil {
14 panic(err)
15 }
16 os.WriteFile(fmt.Sprintf("./examples/%s", strings.Split(captcha.Data.Path2, "/")[7]), imageBytes, 0666)
17
18 // convert bytes to png
19 img, err := png.Decode(bytes.NewReader(imageBytes))
20 if err != nil {
21 panic(err)
22 }
23
24 // solve image
25 solvedX := solve.SolveImage(img)
26
27 // make the payload
28 payload := &coinmarketcap.Payload{}
29 payload.FillPayload(solvedX)
30 fmt.Printf("%+v\n", payload)
31
32 // get encoded payload
33 encodedPayload := payload.Encode(captcha.Data.Ek)
34
35 // get S value
36 sValue := payload.GenSValue(encodedPayload, captcha.Data.Sig, captcha.Data.Salt)
37 solvedCaptcha, err := client.SolveCaptcha(captcha.Data.Sig, encodedPayload, sValue)
38 if err != nil {
39 panic(err)
40 }
41 fmt.Printf("%+v\n", solvedCaptcha)
42}When I ran this, I got this response:
1{
2 "code": "000000",
3 "data": {
4 "result": 3,
5 "tag": "cmc_new",
6 "i18n": "{\"cap_timeout\":\"Timeout\",\"cap_try_again\":\"Please try again\",\"cap_select_all_match_images\":\"Please select all images with\",\"cap_verify_fail\":\"Please try again\",\"cap_success\":\"Success\",\"cap_too_many_attempts\":\"Too many attempts\",\"cap_verify\":\"Verify\",\"cap_verify_success\":\"Success\",\"networkerror\":\"change to a new captcha image\",\"cap_loading\":\"Loading\",\"cap_security_verification\":\"Security Verification\",\"describe\":\"select all image with {{tag}}\",\"cap_complete_puzzle\":\"Slide to complete the puzzle\",\"cap_network_error\":\"Network error\",\"cap_next\":\"Next\",\"cap_system_error\":\"System error\"}",
7 "token": ""
8 },
9 "success": true
10}At first, I was confused, then I realized something. In golang, url.Values doesn't keep the order that you add keys. To test if this was really the problem, I changed the way we get the body to this code:
1// file: ./coinmarketcap/http.go
2t := url.Values{}
3t.Set("data", encodedPayload)
4body := fmt.Sprintf(`bizId=CMC_register&sv=20220812&snv=1.4.7&lang=en&securityCheckResponseValidateId=CMC_register&clientType=android&sig=%s&data=%s&s=%v`, sig, strings.Split(t.Encode(), "=")[1], sValue)When running the code now, I get this output:
1&{Code:000000 Data:{Result:0 Tag:cmc_new I18N:{"cap_timeout":"Timeout","cap_try_again":"Please try again","cap_select_all_match_images":"Please select all images with","cap_verify_fail":"Please try again","cap_success":"Success","cap_too_many_attempts":"Too many attempts","cap_verify":"Verify","cap_verify_success":"Success","networkerror":"change to a new captcha image","cap_loading":"Loading","cap_security_verification":"Security Verification","describe":"select all image with {{tag}}","cap_complete_puzzle":"Slide to complete the puzzle","cap_network_error":"Network error","cap_next":"Next","cap_system_error":"System error"} Token:captcha#zzzzzzzzzzzzzzzzzzzzz} Success:true}Great, we got a captcha! I will say though, it was surprising that they validate the order of the keys in the payload. It was the first thing I tried to fix this problem, however, I was not expecting it at all. Fair play to them. Now we get to write a little http function to try to register an account.
Signup
First, let's get a disposable email from mail.gw, this will be used to validate that the signup is succeeding by seeing if we get a signup code sent to the email.
Next, we can use Burp Suite's repeater function to see if our captcha works. Right click the signUp request and select Send to Repeater, then at the top you should see the Repeater tab, click that. Now we can generate a new captcha and replace the Cmc-Captcha-Token header with our generated captcha and the email with our disposable email. I'll record a little demo video of this to show you all! You can view that here.
As you can see, it all works as intended! I had a really great time making doing this project and and even better time writing the 2 blog posts on it and I hope you, the reader, enjoyed it as well! Thank you for reading and feel free to contact me if you have any questions about anything I've done here.
References
GitHub Repository: github.com/obfio/coinmarketcap-captcha-solver HTTP Client: github.com/bogdanfinn/fhttp TLS Profiles: github.com/bogdanfinn/tls-client Disposable Email: mail.gw