Skip to content

Instantly share code, notes, and snippets.

@jes
Created July 4, 2023 15:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jes/9628554e9e676dbaf63e24755c9cf4cb to your computer and use it in GitHub Desktop.
Save jes/9628554e9e676dbaf63e24755c9cf4cb to your computer and use it in GitHub Desktop.
opensips topology_hiding Contact param decoder
#!/usr/bin/perl
#
# Decoder for OpenSIPS topology_hiding Contact header data params
# James Stanley 2023
#
# The OpenSIPS topology_hiding module can encode information in
# a Contact header URI parameter. The encoding is just XOR against the
# key, with the key repeating when it is too short. The plaintext
# contains the contents of the Record-Route headers, Contact header,
# and receiving socket address of the message that came into OpenSIPS.
#
# A large part of that Contact header can be derived from looking
# at the From/To header (or the request URI), so we have a decent
# amount of known plaintext. If the key is shorter than the part
# of the plaintext that we know, then we can discover the key.
#
# Set "$ciphertext" to the contents of the encoded param in the Contact
# header that you see coming out of OpenSIPS.
# Set "$known_plaintext_fragment" to anything that you know will
# occur in the plaintext (the best bet is the part of the Contact header
# that you can guess)
#
# If this script can not find the solution (e.g. the key is longer than
# the plaintext fragment that you know), then your best bet is to
# collect multiple ciphertexts and XOR pairs of them together. The result of
# XOR'ing 2 ciphertexts together is equivalent to XOR'ing the corresponding
# plaintexts together, at which point you have completely removed the
# key material and you just have XOR'd plaintexts to decode.
#
# This script checks whether a possible solution is "likely" by looking
# at whether the plaintext ends in a plausible socket address. Another
# good way would be to check whether the derived key consists entirely of
# printable ASCII.
use strict;
use warnings;
use MIME::Base64 qw(decode_base64);
##############################################################
#
# Configure your ciphertext and known plaintext fragment here:
#
my $ciphertext = "bTZUOUMHHUwNU0dqEg0eQjtYUw0CAwlCdARWRl0YVBcJVFtGVwYPQRQJA0UUVkIaUAIERiY1EwwdDFULB0BdWFcYBQ9TWF0G";
my $known_plaintext_fragment = "sip:jes_test_account@";
#
#
##############################################################
my $have_likely_answer = 0;
my @unlikely_answers;
# try every key length up to the length of the known fragment
for my $keylen (1..length($known_plaintext_fragment)) {
try_decode([str2arr(decode_word64($ciphertext))], [str2arr($known_plaintext_fragment)], $keylen);
}
if (!$have_likely_answer) {
# we didn't find any likely answers, so just dump every possible solution
for my $u (@unlikely_answers) {
print "key=" . printable($u->{key}) . " ";
print "msg=" . printable($u->{msg}) . "\n";
}
}
sub decode_word64 {
my ($w64) = @_;
$w64 =~ s/\./\//g;
$w64 =~ s/-/=/g;
return decode_base64($w64);
}
# we don't know where the known fragment is within the ciphertext, so
# just assume it is at every position and keep the derived keys that don't
# present a contradiction
sub try_decode {
my ($ctxt, $known, $keylen) = @_;
my @c = @$ctxt;
for my $pos (0 .. ($#c-@$known)) {
my $key = find_key([@c[$pos..$#c]], $known, $pos, $keylen);
if (defined $key) {
my $msg = arr2str(@{ decode($ctxt,$key) });
if (likely($msg)) {
# if this is likely to be the true plaintext, print it now
# (but keep looking for more answers anyway)
$have_likely_answer = 1;
print "key=" . printable(arr2str(@$key)) . " ";
print "msg=" . printable($msg) . "\n";
} else {
# if it's unlikely, save the solution, to be printed out at
# the end only in the case that we did not find any likely
# solutions
push @unlikely_answers, {
key => arr2str($key),
msg => $msg,
};
}
}
}
}
# return 1 if the given plaintext is likely, 0 otherwise
sub likely {
my ($msg) = @_;
# a plaintext is likely to be correct if it looks like it ends in a
# string like "udp:127.0.0.1:5000"
return 1 if $msg =~ /[a-z]{3}:\d+\.\d+\.\d+\.\d+:\d+$/;
return 0;
}
# if the given ciphertext has the given known fragment at the given position,
# and the key is the given length, what is the key?
# return undef if there is no possible key, otherwise return the key
sub find_key {
my ($ctxt, $known, $pos, $keylen) = @_;
my @key = (undef)x$keylen;
my @c = @$ctxt;
my @k = @$known;
die "known plaintext fragment too short (need at least $keylen bytes)" if @k < $keylen;
for my $i (0 .. $#k) {
my $keybyte = $c[$i] ^ $k[$i];
my $ki = ($i+$pos) % $keylen;
return if defined $key[$ki] && $key[$ki] != $keybyte;
$key[$ki] = $keybyte;
}
return \@key;
}
# decode the given ciphertext with the given key
sub decode {
my ($ctxt, $key) = @_;
my @c = @$ctxt;
my @k = @$key;
my @m;
for my $i (0 .. $#c) {
push @m, $c[$i] ^ $k[$i % @k];
}
return \@m;
}
# convert a string of characters to an array of numbers
sub str2arr {
my ($str) = @_;
return map { ord($_) } split //, $str;
}
# convert an array of numbers to a string of characters
sub arr2str {
my (@arr) = @_;
return join('', map { chr($_) } @arr);
}
# make the string printable
sub printable {
my ($str) = @_;
$str =~ s/\\/\\\\/g;
$str =~ s/([^[:print:]])/sprintf "\\x%02x", ord($1)/ge;
return "\"$str\"";
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment