#!/usr/bin/perl -w # # $Id: //pentools/main/pcainfo/pcainfo.p#7 $ # # written by : Stephen J. Friedl # Software Consultant # Tustin, California USA # steve@unixwiz.net / www.unixwiz.net # 2002/02/26 # # This program reads a bunch of pcAnywhere .CHF and .CIF files # and picks out as much as we can find from them. We've figured # out most of the "intersting" stuff, and we then output those # parts. # # There are two filetypes we decode: # # .CHF (Remote Control) files describe remote machines that # we will call and control via the local workstation. # It normally contains the remote's IP address and # (hopefully) autologin account information. # # .CIF (Caller ID) files are used by pcAnywhere Host to hold # login information that remotes use to get into the # system. Mainly it has the account ID and password # that it expects to find. # # Both files have a level of obscuring via a kind of XOR, and # from there we pick out the fields of interest. The actual file # layouts are described in the pcadecode.pm module: we just do # the I/O and the reporting here. # # These files are typically found in: # # C:\WINNT\Profiles\All Users\Application Data\Symantec\pcAnywhere # # and probably in another place on native Windows 2000 installs. # # COMMAND LINE # ------------ # # --help show this help listing # # --rewrite save decoded versions of the file into FILE.CHFBIN (used # for debugging) # # --dir=DIR scan directory DIR for all .CHF and .CIF files (avoids # globbing issues on NT. # # --dir same as --dir=. # # --version just show version info and exit # # VERSION HISTORY # --------------- # # 1.0 (2002/02/27) - Initial release # # 1.1 (2002/02/28) - Added .CIF (caller ID) support # use strict; # use pcadecode; my $version = "1.1 (2002/02/28)"; my @FILES = (); # list of files to scan my $do_rewrite = 0; # --rewrite parameter foreach ( @ARGV ) { if ( m/^--help/ ) { ( my $pn = $0 ) =~ s| .* [/\\]||x; # dump leading path print STDERR < .CIFBIN # .CHF -> .CHFBIN # sub save_rewrite { my $fname = shift; # original filename my $str = shift; # decoded string $fname =~ s|.*[/\\]||g; # extract only tail part of path # we're anal about making the "BIN" extension have the same # case as the original extension - this is silly. if ( $fname =~ m/[A-Z]$/ ) { $fname .= "BIN"; } else { $fname .= "bin"; } if ( open(F, ">$fname") ) { binmode F; print F $str; close F; } else { print "ERROR: cannot create $fname\n"; $do_rewrite = 0; # disable more writes } } # # process_CIF # # Given a filename and the raw data from a .CIF (Caller ID) File, # XOR decode it, parse the fields, and report to the user. The # fields here are pretty straightforward... # # When finished, rewrite the decoded data if requested. # sub process_CIF { my $fname = shift; my $rawdata = shift; print "Processing $fname (Caller ID File)\n"; my $decstr = decode_pca_CIF( $rawdata ); my $ref = parse_pca_CIF( $decstr ); my $x; print " Display Name = $x\n" if $x = $ref->{DisplayName}; print " File Password = $x\n" if $x = $ref->{FilePassword}; print " Caller Name = $x\n" if $x = $ref->{CallerName}; print " Caller Password = $x\n" if $x = $ref->{CallerPassword}; save_rewrite($fname, $decstr) if $do_rewrite; } # # process_CHF # # Given a filename and raw data from a .CHF file that describes # a pcAnywhere remote, XOR decode it, parse the fields, and report # them to the user. # sub process_CHF { my $fname = shift; my $rawdata = shift; print "Processing $fname (Remote Control file)\n"; my $decstr = decode_pca_CHF($rawdata); my $ref = parse_pca_CHF( $decstr ); my $x; print " FilePassword = $x\n" if $x = $ref->{FilePassword}; print " ConxType = $x\n" if $x = $ref->{ConxType}; print " Hostname = $x\n" if $x = $ref->{Hostname}; print " IPAddr = $x\n" if $x = $ref->{IPAddress}; print " Logname = $x\n" if $x = $ref->{Domain_Logname}; print " Password = $x\n" if $x = $ref->{Password}; print " Area Code = $x\n" if $x = $ref->{AreaCode}; print " Phone Number = $x\n" if $x = $ref->{PhoneNumber}; $x = $ref->{EncrLevel}; $x = "None" if $x eq "255"; $x = "pcAnywhere" if $x eq "0"; $x = "Symmetric" if $x eq "1"; $x = "Public Key" if $x eq "2"; # always displayed print " Encrypt = $x\n"; # if no encryption used, there *is* no "lower" if ( $x ne "None" and $ref->{DenyLowerEncr} ) { print " Deny Lower Encr = ", $ref->{DenyLowerEncr}, "\n"; } print " Private Key Cont = $x\n" if $x = $ref->{PrivKeyContainer}; print " CertCommonName = $x\n" if $x = $ref->{CertCommonName}; print " SaveSessionFile = $x\n" if $x = $ref->{SaveSessionFile}; print " Autorun File = $x\n" if $x = $ref->{AutoRun}; print " Location = $x\n" if $x = $ref->{Location}; print "\n"; save_rewrite($fname, $decstr) if $do_rewrite; } # # readfile # # Given a filename, read it into memory and return it to the # caller as one big string. We have to insure that we are # always in binary mode - no CR/LF translation! - and we return # -undef- if we can't read it. # sub readfile { my $fname = shift; open(F, $fname) or return undef; binmode F; # binary mode: no CR/LF translate local $/ = undef; # no line delimiters my $encstr = ; # slurp entire file close F; return $encstr; } # # scan_dir # # Given a directory name, scan it for .CHF and .CIF files. This # is mainly only useful for NT where filename globbing doesn't # seem to work right. # sub scan_dir { my $dname = shift; opendir(D, $dname) or die "ERROR: cannot open directory {$dname}\n"; $dname =~ s|\\|/|g; # convert \ to / $dname =~ s|/*$|/|g; # must have one trailing slash $dname = '' if $dname eq './'; # AH: current directory my @F = map { $dname . $_ } grep( m/\.(chf|cif)$/i, readdir D ); closedir D; return @F; } # # $Id: //pentools/main/libperl/pcadecode.pm#7 $ # # This module is responsible for decoding of a pcAnywhere .CHF file # that describes a remote system the client wishes to connect to. # We use the term "decoding" in two senses. First, the file is # obscured with a simple XOR algorithm, which we can reverse, plus # the picking apart of the various fields within the file. This # two-step process # # FILE OBSCURING ALGORITHM # ------------------------ # # The general idea of the file obscuring algorith mis that each # byte is XOR'd with the previous byte plus an incrementing # eight-bit counter. For reasons unknown to us, there seems to # be some kind of shift in the algorithm starting at byte 448, # so we split up our decoding into a "first part" and a "second # part). We don't know why this is. # # for each byte # do # char = thisbyte (XOR) prevbyte (XOR) counter++ # done # # FIELD BREAKDOWN # --------------- # # The file seems to always be the same size -- 3308 bytes -- and # the interesting fields appear to be in fixed positions: this was # very helpful. # # String fields seem to be terminated with a NUL byte, and we have # observed that changing a long value to a short one leaves the # tail end of the long field inside the file. In some cases we do # not ever care about the "old" value, but since passwords and # login names are disabled by NULing out the first byte, the bytes # that remain might be interesting. We keep those around if they # give us a clue as to most of an old password. # # We believe that some fields are slightly overloaded - we have # seen overlap - and they mainly revolve around the GATEWAY fields. # We don't know how pcAnywhere gateways work well enough to really # know what to make of it. # # USING THIS MODULE # ----------------- # # When decoding the file contents, we simply pass down the whole # string representing the file, and we crank it through the XOR # decoder: # # local $/ = undef; # # open(F, $fname) || die; # binmode F; # my $rawdata = ; # close(F); # # my $clrdata = decode_pca_CNF($rawdata); # # Now $clrdata is the "cleartext" representation of the file. # It's then parsed into our interesting fields: # # my $ref = parse_pca_CHF($clrdata); # # This returns an anon hash that has all the fields that seem # interesting. See the code for the details. # # .CHF FILE VERSIONS? # ------------------- # # This was developed usingfiles produced by pcAnywhere 9.2.1 on # Windows 2000 (build 258), and we have not found any kind of # version indicator in the file. We will make notes when we find # updates to this. # # STUFF TO DO # ----------- # # Our understanding of the decoding process just looks incomplete: # it's complicated enough for no good reason that we really just # suspect that we have done it wrong. There are a couple of glitches # even in the current decoding that it requires a bit more thought. # # Very early in the file is a "description" field that we are not # quite decoding properly. # # The "hostname" and "IP Address" fields seem to be redundant, # and this requires more research. # # There are still plenty of big unused fields in the .CHF file, # and we ought to find out what they are used for. Try looking # at the other protocols (ISDN, SPX, NETBIOS, etc.) # use strict; # # rawdecode # # This is the low-level engine that handles the XOR decoding of the # byte stream. It knows nothing of pcAnywhere data, and it can be # called on multiple sections of the file independently. # # $roll - starting value of the rolling counter # $prev - the "previous byte" value upon entry to the loop # $str - the string we're to decode # sub rawdecode { my $roll = shift; my $prev = shift; my $str = shift; my $decstr = ""; # decoded string foreach ( split( m//, $str) ) { my $c = ord($_); $decstr .= chr( $c ^ $prev ^ ($roll++ & 0xFF) ); $prev = $c; } return $decstr; } # # decode_pca_CHF # # Provided with a pcAnywhere .CNF file, un-obscure the whole # thing into the "clear" format. We don't really do any kind # of actual parsing - that's later. The return value is the same # length as the input string, but after XOR decoding. # sub decode_pca_CHF { my $rawdata = shift; my $part1 = substr($rawdata, 0, 444); my $part2 = substr($rawdata, 444); return rawdecode(255, 0, $part1) . rawdecode(255, 0x54, $part2); } # # decode_pca_CIF # # The CIF is a Caller ID File, and it defines a "caller" that's # allowed to login to the system. The XOR format is the same as # the CHF file, so we (quietly) just call that routine to do the # work. This is helpfuil. # sub decode_pca_CIF { my $rawdata = shift; return decode_pca_CHF( $rawdata ); } # ------------------------------------------------------------------------ # CHF FIELD CHDECODING # # These routines are used to take an XOR-decoded string that represents an # XOR-decoded pcAnywhere .CHF file and picks it apart into the constituent # pieces. # # We define all the fields of interest in a %HASH to allow us to do a # bit more than just decode: perhaps a bit of reporting or double-checking # for overlaps and the like. # # Each entry has a name, which is used as the key to the user-returned # hash, plus the zero-based offset into the setring, the length, and a # "type". The type is one of: # # 0 = string, strip trailing NUL bytes # 1 = string, strip everything after first NUL byte # 2 = binary # # The reason we allow for type #1 is to *not* strip NUL bytes from a # few fields, such as "passwords". If you have entered a login name # or password, but then disable the "auto-login", pcAnywhere simply # NULs out the first byte: this is still useful information. # # The BEGIN {} stuff is required so that this module works as a regular # perl module *and* appended to a main driver program (insures that the # %FIELDS var is initialized properly). # my %FIELDS_CHF; BEGIN { %FIELDS_CHF = ( # off len type # ---- --- ---- FilePassword => [ 280, 128, 0 ], SaveSessionFile => [ 744, 128, 0 ], ScriptFileName => [ 889, 128, 0 ], PhoneNumber => [ 1038, 31, 0 ], Location => [ 1069, 128, 0 ], AreaCode => [ 1210, 40, 0 ], IPAddress => [ 1324, 128, 0 ], ConxType => [ 1701, 64, 0 ], ConnCount => [ 1913, 1, 2 ], RetrySecs => [ 1914, 1, 2 ], Gateway => [ 1928, 24, 0 ], # don't truncate after NUL Hostname => [ 1940, 128, 1 ], Domain_Logname => [ 2093, 128, 1 ], Password => [ 2222, 128, 1 ], DenyLowerEncr => [ 3050, 1, 2 ], EncrLevel => [ 3052, 1, 2 ], PrivKeyContainer => [ 3053, 48, 0 ], CertCommonName => [ 3103, 48, 0 ], ); }; # ------------------------------------------------------------------------ # CIF FIELD DECODING # # The .CIF file is quite a bit smaller, and we have less interesting # stuff found within. The description of the field mechanism is as # above. # my %FIELDS_CIF; BEGIN { %FIELDS_CIF = ( # off len type # ---- --- ---- DisplayName => [ 16, 186, 0 ], FilePassword => [ 280, 128, 0 ], CallerName => [ 460, 128, 0 ], CallerPassword => [ 589, 127, 0 ], ); }; sub parse_pca_file { my $str = shift; my $fref = shift; my $ref = { }; # value to be returned foreach my $key ( keys %{ $fref } ) { my $f = $fref->{$key}; my $off = $f->[0]; my $len = $f->[1]; my $type = $f->[2]; my $val = substr($str, $off, $len); if ( $type == 0 ) # strip ALL after NUL { $val =~ s/\0.*$//; } elsif ( $type == 1 ) # strip only trailing NUL { $val =~ s/\0+$//; $val =~ s/\0+/{NUL}/g; } elsif ( $type == 2 ) # binary byte { $val = ord( $val); } else { # do nothing - just leave it be } $ref->{ $key } = $val; } return $ref; } sub parse_pca_CIF { my $str = shift; return parse_pca_file( $str, \%FIELDS_CIF); } sub parse_pca_CHF { my $str = shift; return parse_pca_file( $str, \%FIELDS_CHF); } 1;