Created
September 13, 2022 20:58
-
-
Save wolveix/af2f09c54d024e8e99736eef4c28aac5 to your computer and use it in GitHub Desktop.
PCAP Decryption in Go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package pcap | |
import ( | |
"bytes" | |
"errors" | |
"fmt" | |
"io/ioutil" | |
"os" | |
"os/exec" | |
"path/filepath" | |
"strings" | |
"github.com/google/gopacket" | |
"github.com/google/gopacket/layers" | |
"github.com/spf13/cast" | |
) | |
const ( | |
generateDebugLog = `tshark -n -o "tls.desegment_ssl_records: TRUE" -o "tls.desegment_ssl_application_data: TRUE" -o "tls.keys_list:SERVER_IP_HERE,SERVER_PORT_HERE,http,TLS_KEY_HERE" -o "tls.debug_file:DEBUG_PATH_HERE" -r PCAP_PATH_HERE -Y "(tcp.port eq SERVER_PORT_HERE)"` | |
injectSessionKey = `editcap --inject-secrets tls,SESSIONKEY_PATH_HERE PCAP_PATH_HERE OUTPUT_PATH_HERE` | |
parseDebugLog = `grep -A3 'ssl_save_master_key inserted (pre-)master secret for Session ID' DEBUG_PATH_HERE > TMPLOG_PATH_HERE && \ | |
grep -A3 'hash out\[48\]' DEBUG_PATH_HERE >> TMPLOG_PATH_HERE && \ | |
grep -A2 'client random\[32\]' DEBUG_PATH_HERE >> TMPLOG_PATH_HERE && \ | |
grep -A3 'hash out\[48\]' DEBUG_PATH_HERE | sed -e 's/hash/hash repeat/g' >> TMPLOG_PATH_HERE && \ | |
cat TMPLOG_PATH_HERE | sed -e 's/ //g' | awk -F'|' '$1 ~ "storedkey" {printf("RSA Session-ID:");next} | |
$1 ~ "hashout" {printf(" Master-Key:");next} | |
$1 ~ "clientrandom" {printf("\nCLIENT_RANDOM:");next} | |
$1 ~ "hashout" {printf(" Master-Key:");next} | |
$1 ~ "hashrepeatout" {printf(" ");next} | |
$1 == "--" {printf("\n");next} {printf("%s",$2)} END {printf("\n")}' > OUTPUT_PATH_HERE` | |
) | |
// Decrypt produces a session key file, and embeds this into the PCAP | |
func Decrypt(pcapPath string, tlsKeyPath string) error { | |
var err error | |
if pcapPath, err = filepath.Abs(pcapPath); err != nil { | |
return fmt.Errorf("failed to resolve absolute path to pcap: %v", err) | |
} | |
// check the pcap exists | |
if _, err = os.Stat(pcapPath); errors.Is(err, os.ErrNotExist) { | |
return errors.New("pcap could not be found") | |
} | |
// check the tls key exists | |
if _, err = os.Stat(tlsKeyPath); errors.Is(err, os.ErrNotExist) { | |
return errors.New("tls key could not be found") | |
} | |
// check that the PCAP hasn't already been decrypted | |
pcapContents, err := ioutil.ReadFile(pcapPath) | |
if err != nil { | |
return fmt.Errorf("failed to check whether the pcap has already been decrypted: %v", err) | |
} | |
if strings.Contains(string(pcapContents), "RSA Session-ID:") { | |
return errors.New("pcap has already been decrypted") | |
} | |
pcapFile, err := openPcap(pcapPath) | |
if err != nil { | |
return fmt.Errorf("failed to open pcap: %v", err) | |
} | |
decrypt := false | |
packetCount := 0 | |
serverIp := "" | |
var serverPort uint16 | |
// get external IP and port to decrypt, and verify that at least one TLS packet exists | |
for { | |
rawPacket, _, err := pcapFile.ReadPacketData() | |
if err != nil { | |
break | |
} | |
packet := gopacket.NewPacket(rawPacket, layers.LayerTypeEthernet, gopacket.Default) | |
packetCount++ | |
if packetCount == 1 { | |
serverIp = packet.NetworkLayer().NetworkFlow().Dst().String() | |
} | |
// loop over packet layers | |
for _, layer := range packet.Layers() { | |
switch layer.LayerType() { | |
case layers.LayerTypeTCP: | |
if serverPort == 0 { | |
tcp, _ := layer.(*layers.TCP) | |
serverPort = uint16(tcp.SrcPort) | |
} | |
default: | |
app := packet.ApplicationLayer() | |
if app != nil { | |
var decoded []gopacket.LayerType | |
var tls layers.TLS | |
parser := gopacket.NewDecodingLayerParser(layers.LayerTypeTLS, &tls) | |
if err = parser.DecodeLayers(packet.ApplicationLayer().LayerContents(), &decoded); err != nil { | |
// TODO: review whether these errors should be considered as fatal | |
// fmt.Printf("unexpected error while parsing tls packets: %v: %v\n", pcapPath, err) | |
continue | |
} | |
for _, layerType := range decoded { | |
switch layerType { | |
case layers.LayerTypeTLS: | |
decrypt = true | |
break | |
} | |
} | |
} | |
} | |
} | |
} | |
if !decrypt { | |
return errors.New("no tls packets could be found") | |
} | |
if serverIp == "" || serverPort == 0 { | |
return errors.New("could not calculate ip and port from pcap") | |
} | |
// get debug log to obtain session key | |
sessionKeyPath, err := generateSessionKey(serverIp, cast.ToString(serverPort), pcapPath, tlsKeyPath) | |
if err != nil { | |
return err | |
} | |
injSessionKey := strings.Replace(injectSessionKey, "SESSIONKEY_PATH_HERE", sessionKeyPath, -1) | |
injSessionKey = strings.Replace(injSessionKey, "PCAP_PATH_HERE", pcapPath, -1) | |
injSessionKey = strings.Replace(injSessionKey, "OUTPUT_PATH_HERE", pcapPath+"with-keys", -1) | |
if _, err = execCommand(injSessionKey); err != nil { | |
return fmt.Errorf("failed to inject tls session key: %v", err) | |
} | |
if err = os.Rename(pcapPath+"with-keys", pcapPath); err != nil { | |
return fmt.Errorf("failed to overwrite existing pcap: %v", err) | |
} | |
// delete session key file | |
go func(sessionKeyPath string) { | |
// ignore errors as it's not too important that this file gets removed | |
_ = os.Remove(sessionKeyPath) | |
}(sessionKeyPath) | |
return nil | |
} | |
// generateSessionKey generates the tls debug log using tshark, parses the log for the session key, and returns the path | |
// to the session key file | |
func generateSessionKey(serverIp string, serverPort string, pcapPath string, tlsKeyPath string) (string, error) { | |
debugLogPath := "/tmp/ppm-pcap-tls-log-" + filepath.Base(pcapPath) + ".log" | |
sessionKeyPath := "/tmp/ppm-pcap-session-keys-" + filepath.Base(pcapPath) + ".keys" | |
tmpKeyPath := "/tmp/ppm-pcap-tmp-session-keys-" + filepath.Base(pcapPath) + ".log" | |
genDebugLog := strings.Replace(generateDebugLog, "SERVER_IP_HERE", serverIp, -1) | |
genDebugLog = strings.Replace(genDebugLog, "SERVER_PORT_HERE", serverPort, -1) | |
genDebugLog = strings.Replace(genDebugLog, "TLS_KEY_HERE", tlsKeyPath, -1) | |
genDebugLog = strings.Replace(genDebugLog, "DEBUG_PATH_HERE", debugLogPath, -1) | |
genDebugLog = strings.Replace(genDebugLog, "PCAP_PATH_HERE", pcapPath, -1) | |
if _, err := execCommand(genDebugLog); err != nil { | |
return "", fmt.Errorf("failed to generate tls session key: %v", err) | |
} | |
prsDebugLog := strings.Replace(parseDebugLog, "DEBUG_PATH_HERE", debugLogPath, -1) | |
prsDebugLog = strings.Replace(prsDebugLog, "TMPLOG_PATH_HERE", tmpKeyPath, -1) | |
prsDebugLog = strings.Replace(prsDebugLog, "OUTPUT_PATH_HERE", sessionKeyPath, -1) | |
if _, err := execCommand(prsDebugLog); err != nil { | |
return "", fmt.Errorf("failed to parse tls session key: %v", err) | |
} | |
// delete interim log files | |
go func(debugLogPath string, tmpKeyPath string) { | |
// ignore errors as it's not too important that these logs get removed | |
_ = os.Remove(debugLogPath) | |
_ = os.Remove(tmpKeyPath) | |
}(debugLogPath, tmpKeyPath) | |
return sessionKeyPath, nil | |
} | |
func execCommand(command string) (string, error) { | |
var stdErr, stdOut bytes.Buffer | |
cmd := exec.Command("bash") | |
cmd.Stderr = &stdErr | |
cmd.Stdin = strings.NewReader(command) | |
cmd.Stdout = &stdOut | |
if err := cmd.Run(); err != nil { | |
var Error string | |
if stdErr.String() != "" { | |
Error = stdErr.String() | |
} else { | |
Error = stdOut.String() | |
} | |
return "", errors.New(Error) | |
} | |
return stdOut.String(), nil | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment