В данной статье покажу, как обойти проверку SSL-сертификатов в скомпилированных приложениях Go (Golang). Эта задача часто возникает при анализе безопасности и перехвате HTTPS-запросов, когда нет доступа к исходному коду. Я рассмотрю способы изменения бинарника для отключения проверки сертификатов, а также автоматизацию этого процесса с помощью Python-скрипта.
Еще по теме: Обход SSL Pinning в Android-приложениях
Как обойти проверку SSL в Golang
Когда впервые столкнулся с задачей перехвата HTTPS-запросов приложения Go, я попытался использовать Burp Suite, настроив переменную окружения HTTPS_PROXY. Однако, при попытке перехвата приложения столкнулся с ошибкой «Certificate signed by unknown authority (неизвестный сертификат)».
Первая мысль — добавить сертификат Burp в хранилище сертификатов компьютера, чтобы устранить ошибку, но это не сработало. Дело в том, что Go не полагается на хранилище сертификатов компьютера и проверяет каждый сертификат самостоятельно. Попытка выполнить атаку «человек посередине» (MITM) на приложении Go была бы затруднительной из-за этой самопроверки.
В сетевых библиотеках и при обработке HTTP-запросов программист обычно может отключить проверку SSL, изменив конфигурацию или добавив флаги в обработчик HTTP. Я решил, что это может сработать и в этом случае. В процессе поиска обнаружил параметр InsecureSkipVerify в конфигурации, который по умолчанию установлен на false. Чтобы отключить проверку SSL, нужно было добавить следующий код в приложение:
1 2 |
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} _, err := http.Get("https://golang.org/") |
или так:
1 2 3 4 5 |
tr := &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, } client := &http.Client{Transport: tr} _, err := client.Get("https://golang.org/") |
Но поскольку я работал с уже скомпилированным приложением и не имел доступа к исходному коду, этот метод оказался непригоден. Пришлось искать другой подход.
Следующим шагом было выяснить, где в бинарном файле программы используется флаг InsecureSkipVerify и как его можно обойти. Вместо того чтобы пытаться понять формат бинарного файла приложения и ассемблерный код, решил изучить исходный код библиотеки net/http.
В результате поиска по коду Go, обнаружил, что флаг InsecureSkipVerify используется в файле crypto/tls/handshake_client.go в функции verifyServerCertificate:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
func (c *Conn) verifyServerCertificate(certificates [][]byte) error { // ... if !c.config.InsecureSkipVerify { opts := x509.VerifyOptions{ Roots: c.config.RootCAs, CurrentTime: c.config.time(), DNSName: c.config.ServerName, Intermediates: x509.NewCertPool(), } // ... } // ... } |
Изучив эту функцию, стало ясно, что при установке флага в true, проверка серверного сертификата пропускается. Таким образом, я мог обойти проверку сертификата, изменив соответствующее условие или ассемблерный код.
Для демонстрации я написали простое приложение на Go, которое создает GET-запрос на ipinfo.io и выводит результат:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
package main import ( "io/ioutil" "log" "net/http" ) func main() { resp, err := http.Get("https://ipinfo.io/") if err != nil { log.Fatalln(err) } body, err := ioutil.ReadAll(resp.Body) if err != nil { log.Fatalln(err) } sb := string(body) log.Printf(sb) } |
Часто программисты удаляют отладочные символы из своих приложений, чтобы скрыть ненужную информацию, такую как строки и имена функций. Но для демонстрации я оставил бинарник без изменений, чтобы облегчить процесс реверс-инжиниринга.
Перед тем как приступить к анализу ассемблерного кода, давайте посмотрим на исходный код функции verifyServerCertificate. В нем код делится на три части:
- Проверка наличия сервера в кэше.
- Проверка флага InsecureSkipVerify.
- Проверка публичного ключа и сертификата.
Нас интересует вторая часть. В бинарнике это условие может быть представлено двумя командами ассемблера: cmp и jn.
Моя цель заключалась в том, чтобы изменить условие таким образом, чтобы программа пропускала проверку SSL. Это можно сделать, изменив команду jnz на jz, которая означает «переход, если ноль». В ассемблере x86 это изменение представляет собой замену одного байта (с 85 на 84).
Для выполнения этого патча использовал IDA Pro. В IDA нашел соответствующий кусок кода и изменили байт 85 на 84, затем применили изменения к файлу. После этого программа перестала выполнять проверку SSL.
Для автоматизации процесса был написан небольшой Python-скрипт, который ищет инструкции cmp и jnz в бинарнике и заменяет их на jz. Вот код скрипта:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
#!/usr/bin/env python3 import subprocess import argparse supported_versions_to_bytes = { '11': [b"\x00\x0F\x85\xB3\x04\x00\x00", b"\x00\x0F\x84\xB3\x04\x00\x00"], '12': [b"\x00\x00\x0F\x85\x43\x05\x00\x00", b"\x00\x00\x0F\x84\x43\x05\x00\x00"], '13': [b"\x00\x00\x0F\x85\x32\x05\x00\x00", b"\x00\x00\x0F\x84\x32\x05\x00\x00"], '14': [b"\x00\x00\x0F\x85\x48\x05\x00\x00", b"\x00\x00\x0F\x84\x48\x05\x00\x00"], '15': [b"\x00\x00\x0F\x85\x3A\x06\x00\x00", b"\x00\x00\x0F\x84\x3A\x06\x00\x00"], '16': [b"\x00\x00\x0F\x85\x5A\x06\x00\x00", b"\x00\x00\x0F\x84\x5A\x06\x00\x00"], '17': [b"\x00\x00\x0F\x85\x7F\x01\x00\x00", b"\x00\x00\x0F\x84\x7F\x01\x00\x00"], '18': [b"\x00\x00\x0F\x85\x7C\x01\x00\x00", b"\x00\x00\x0f\x84\x7C\x01\x00\x00"], '19': [b"\x00\x00\x0F\x85\x7B\x01\x00\x00", b"\x00\x00\x0f\x84\x7B\x01\x00\x00"], '20': [b"\x00\x00\x0F\x85\x84\x01\x00\x00", b"\x00\x00\x0F\x84\x84\x01\x00\x00"], '21': [b"\x00\x00\x0F\x85\x82\x01\x00\x00", b"\x00\x00\x0F\x84\x82\x01\x00\x00"] } def replace_file_bytes(file_path, old_bytes, new_bytes): with open(file_path, 'rb') as f: data = f.read() position = data.find(old_bytes) if(-1 == position): raise Exception("cannot find bytes, maybe the program is already patched?") with open(file_path, 'rb+') as file: file.seek(position) existing_bytes = file.read(len(old_bytes)) if existing_bytes == old_bytes: file.seek(position) file.write(new_bytes) def run_command(command): result = subprocess.run(command, shell=True, capture_output=True, text=True) return result.stdout.strip() def get_go_bin_version(filename): output = run_command(f"strings {filename} | grep '^go1' | head -n 1") if "" == output: output = run_command(f"strings {filename} | grep 'Go cmd/compile' | head -n 1 | cut -d' ' -f 3") if "" == output: raise Exception("cannot determine go binary version") return output.replace("go", "").replace(".", "") def patch_file(filename, version): if version not in supported_versions_to_bytes: raise Exception("unsupported version to patch. supported versions are: " + str(supported_versions_to_bytes.keys())) bytes_arr = supported_versions_to_bytes[version] replace_file_bytes(filename, bytes_arr[0], bytes_arr[1]) def main(): parser = argparse.ArgumentParser(description="Patching Go binaries to ignore SSL") parser.add_argument('binary', type=str, help='Go binary to patch') args = parser.parse_args() bin_version = get_go_bin_version(args.binary) print(f"Found version: {bin_version}") patch_file(args.binary, bin_version) if __name__ == "__main__": main() |
Этот скрипт позволяет быстро и эффективно изменять скомпилированные бинарные файлы Go, обходя проверку SSL-сертификатов. Теперь перехват HTTPS-запросов и анализ приложений на Golang становится значительно проще.
ПОЛЕЗНЫЕ ССЫЛКИ: