#!/usr/bin/perl -w
##################

##
#    Name: download_symbols.pl
#  Author: hdm@metasploit.com
# Purpose: Download DBG and PDB symbol files from the Microsoft Public Symbol Server
#   Usage: ./download_symbols /path/to/exe_or_dll
#   Notes:
#          This script has been tested with the DLL files that ship with Windows NT 4.0.
#          Windows 2000, and Windows XP. It does not support the RSDS .NET type yet,
#          nor does it actually extract any of the symbols from the files it downloads. 
#          Seperate extraction tools will be made available as they are completed. At 
#          this point I plan on releasing a symbol extractor for the unstripped images,
#          NB09 debug files, and NB10 program database files. For the download to work
#          you must already have wget and cabextract in your path, they available from:
#
#          wget:        http://wget.sunsite.dk/
#          cabextract:  http://www.kyz.uklinux.net/cabextract.php
#
##


package main;
use strict;

my $VERSION = 1.0;

my $target = shift() || Usage();
my $pe = PEInfo->new($target);
my $dbg_idx = 0;
my $dbg_skp = 0;
my $dbg_hsh;
my $dbg_raw;
my $pdb_hsh;
my ($exe_nb09, $exe_nb10, $exe_nb11);
my $valid_nb10 = 0;
 
if (! $pe)
{
    print "[x] Error loading the executable image.\n";
    exit(0);
}

my $filename = lc($target);
$filename =~ s/.*\/(.*)/$1/g;

my $raw = $pe->Raw();

$dbg_hsh = sprintf("%.8x%x", $pe->ImageHeader("TimeDateStamp"), $pe->OptImageHeader("SizeOfImage"));

if ($pe->ImageHeader("NumberOfSymbols") > 0 && $pe->ImageHeader("PointerToSymbolTable") > 0)
{
    printf("[x] This file contains " . $pe->ImageHeader("NumberOfSymbols") .
          " symbols at offset 0x%.8x.\n", $pe->ImageHeader("PointerToSymbolTable"));
}

my $debug_rva = $pe->Rva("debug");
if (! $debug_rva->[0])
{
    print " [  RVA ] " . join("\n", $pe->Rvas()) . "\n";
    print "[x] No debug directory found, something is seriously wrong...\n";
    exit(0);
}

my $debug_off = $pe->VirtualToOffset($debug_rva->[0]);
my $debug_dat = substr($raw, $debug_off, $debug_rva->[1]);
my $debug_tds = substr($debug_dat, 4);

my $debug_typ = unpack("L", substr($debug_dat, 12));
my $debug_siz = unpack("L", substr($debug_dat, 16));
my $debug_dir = unpack("L", substr($debug_dat, 24));


# printf("DEBUG: TYPE=0x%.8x SIZE=0x%.8x OFFSET=0x%.8x\n", $debug_typ, $debug_siz, $debug_dir);

if ($debug_typ == 2 && substr($raw, $debug_dir, 4) eq "NB10")
{
    print "[x] Executable contains a NB10 signature and does not require a DBG file.\n";
    $dbg_skp++;
    $dbg_idx = $exe_nb10;
}

if (! $dbg_skp)
{

    my $dbg_fn = $filename;
    $dbg_fn =~ s/(.*)\..../$1\.dbg/;

    my $dbg_fn_com = $dbg_fn;
    $dbg_fn_com =~ s/\.dbg/\.db\_/;

    if (! -r $filename . "_" . $dbg_hsh . "._")
    {
        my $url = "http://msdl.microsoft.com/download/symbols/$dbg_fn/$dbg_hsh/$dbg_fn_com";
        print "[x] Downloading DBG file: $url\n";
        unlink($dbg_fn_com);
        open(X, "wget -q $url|") || die "wget: $!";
        while(<X>)
        {
            chomp;
            next if !length($_);
            print "[x] WGET> $_\n";
        }
        close(X);
        
        rename($dbg_fn_com, $filename . "_" . $dbg_hsh . "._");
        unlink($dbg_fn_com);
    }

    unlink($dbg_fn);
    
    open(X, "cabextract " . $filename . "_" . $dbg_hsh . "._|") || die "cabextract: $!";
    while(<X>)
    {
        chomp;
        next if !length($_);
        print "[x] CAB> $_\n";
    }
    close(X);
    
    if (! -f $dbg_fn)
    {
        print STDERR "[x] Could not extract debug file.\n";
        exit(0);
    }
    open(X, "<$dbg_fn");
    while (<X>) { $dbg_raw .= $_ }
    close (X);
    
    $dbg_idx = index($dbg_raw, "NB09");
    if ($dbg_idx)
    {
        print "[x] This DBG file contains a NB09 symbol table, no PDB needed.\n";
        exit(0);
    }
    
    $debug_dir = index($dbg_raw, "NB10");
    if (! $debug_dir)
    {
        print "[x] No NB10 CodeView segment found in the debug file, giving up.\n";
        exit(0);
    }
    
} else {

    $dbg_raw = $raw;
}

$pdb_hsh = sprintf("%.8x%x", unpack("L", substr($dbg_raw, $debug_dir + 8)), unpack("L", substr($dbg_raw, $debug_dir + 12)));

if ($pdb_hsh)
{
    my $dbg_fn = $filename;
    $dbg_fn =~ s/(.*)\..../$1\.pdb/;

    my $dbg_fn_com = $dbg_fn;
    $dbg_fn_com =~ s/\.pdb/\.pd\_/;

    if (! -r $filename . "_" . $pdb_hsh . "._")
    {
        my $url = "http://msdl.microsoft.com/download/symbols/$dbg_fn/$pdb_hsh/$dbg_fn_com";
        print "[x] Downloading PDB file: $url\n";
        unlink($dbg_fn_com);
        
        open(X, "wget -q $url|") || die "wget: $!";
        while(<X>)
        {
            chomp;
            next if !length($_);
            print "[x] WGET> $_\n";
        }
        close(X);
        
        rename($dbg_fn_com, $filename . "_" . $pdb_hsh . "._");
        unlink($dbg_fn_com);
    }

    unlink($dbg_fn);
    
    open(X, "cabextract " . $filename . "_" . $pdb_hsh . "._|") || die "cabextract: $!";
    while(<X>)
    {
        chomp;
        next if !length($_);
        print "[x] CAB> $_\n";
    }
    close(X);

    if (! -f $dbg_fn)
    {
        print STDERR "[x] Could not extract debug file.\n";
        exit(0);
    }
}  


sub Usage {
    print STDERR "Usage: $0 <file>\n";
    exit(0);
}


##
# Extract information from a PE image and make it accessible
# through an object-oriented interface.
##

package PEInfo;
use strict;

my $RAW;
my $LastErrorVal;
my %IMAGE_HDR;
my %OPT_IMAGE_HDR;
my %RVA;
my %SECTIONS;



sub new {
    my ($class, $args) = @_;
    my $object = bless {}, $class;
    return($object->LoadImage($args));
}

sub LastError {
    my $object = shift;
    if (@_) { $LastErrorVal = shift }
    return ($LastErrorVal);
}

sub Raw {
    my $object = shift;
    return $RAW;
}

sub ImageHeader {
    my $object = shift;
    my $name = shift;
    if (exists($IMAGE_HDR{$name}))
    {
        return($IMAGE_HDR{$name});
    }
    return undef;
}

sub ImageHeaders {
    my $object = shift;
    return keys(%IMAGE_HDR);
}

sub OptImageHeader {
    my $object = shift;
    my $name = shift;
    if (exists($OPT_IMAGE_HDR{$name}))
    {
        return($OPT_IMAGE_HDR{$name});
    }
    return undef;
}

sub OptImageHeaders {
    my $object = shift;
    return keys(%OPT_IMAGE_HDR);
}

sub Rva {
    my $object = shift;
    my $name = shift;
    if (exists($RVA{$name}))
    {
        return($RVA{$name});
    }
    return undef;
}

sub Rvas {
    my $object = shift;
    return keys(%RVA);
}

sub Section {
    my $object = shift;
    my $name = shift;
    if (exists($SECTIONS{$name}))
    {
        return($SECTIONS{$name});
    }
    return undef;
}

sub Sections {
    my $object = shift;
    return keys(%SECTIONS);
}

sub LoadImage {
    my ($object, $fn) = @_;
    my $data;   
    local *X;
    
    if (! open(X, "<$fn"))
    {
        $object->LastError("Could not open file: $!");
        return(undef);
    }
    
    while(<X>) { $data .= $_ }
    close(X);
    
    $RAW = $data;
    
    my $peo = $object->FindPEOffset(\$data);
    if (! $peo)
    {
        $object->LastError("Could not find PE header");
        return(undef);
    }
    
    $IMAGE_HDR{"MachineID"}               = unpack("S", substr($data, $peo + 4));
    $IMAGE_HDR{"NumberOfSections"}        = unpack("S", substr($data, $peo + 6));
    $IMAGE_HDR{"TimeDateStamp"}           = unpack("L", substr($data, $peo + 8));
    $IMAGE_HDR{"PointerToSymbolTable"}    = unpack("L", substr($data, $peo + 12));
    $IMAGE_HDR{"NumberOfSymbols"}         = unpack("L", substr($data, $peo + 16));
    $IMAGE_HDR{"SizeOfOptionalHeader"}    = unpack("S", substr($data, $peo + 20));
    $IMAGE_HDR{"Characteristics"}         = unpack("S", substr($data, $peo + 22));

    if ($IMAGE_HDR{"SizeOfOptionalHeader"} > 0)
    {
        my $opthdr = substr($data, $peo + 24, $IMAGE_HDR{"SizeOfOptionalHeader"});

        $OPT_IMAGE_HDR{"Magic"}               = unpack("S", substr($opthdr, 0));
        $OPT_IMAGE_HDR{"MajorLinker"}         = unpack("C", substr($opthdr, 2));
        $OPT_IMAGE_HDR{"MinorLinker"}         = unpack("C", substr($opthdr, 3));
        $OPT_IMAGE_HDR{"SizeOfCode"}          = unpack("L", substr($opthdr, 4));
        $OPT_IMAGE_HDR{"SizeOfInitialized"}   = unpack("L", substr($opthdr, 8));
        $OPT_IMAGE_HDR{"SizeOfUninitialized"} = unpack("L", substr($opthdr, 12));

        $OPT_IMAGE_HDR{"EntryPoint"}          = unpack("L", substr($opthdr, 16));
        $OPT_IMAGE_HDR{"BaseOfCode"}          = unpack("L", substr($opthdr, 20));
        $OPT_IMAGE_HDR{"BaseOfData"}          = unpack("L", substr($opthdr, 24));

        $OPT_IMAGE_HDR{"ImageBase"}           = unpack("L", substr($opthdr, 28));
        $OPT_IMAGE_HDR{"SectionAlign"}        = unpack("L", substr($opthdr, 32));
        $OPT_IMAGE_HDR{"FileAlign"}           = unpack("L", substr($opthdr, 36));

        $OPT_IMAGE_HDR{"MajorOS"}             = unpack("S", substr($opthdr, 38));
        $OPT_IMAGE_HDR{"MinorOS"}             = unpack("S", substr($opthdr, 40));
        $OPT_IMAGE_HDR{"MajorImage"}          = unpack("S", substr($opthdr, 42));
        $OPT_IMAGE_HDR{"MinorImage"}          = unpack("S", substr($opthdr, 44));
        $OPT_IMAGE_HDR{"MajorSub"}            = unpack("S", substr($opthdr, 46));
        $OPT_IMAGE_HDR{"MinorSub"}            = unpack("S", substr($opthdr, 48));

        $OPT_IMAGE_HDR{"Reserved"}            = unpack("L", substr($opthdr, 52));
        $OPT_IMAGE_HDR{"SizeOfImage"}         = unpack("L", substr($opthdr, 56));
        $OPT_IMAGE_HDR{"SizeOfHeaders"}       = unpack("L", substr($opthdr, 60));
        $OPT_IMAGE_HDR{"Checksum"}            = unpack("L", substr($opthdr, 64));
        $OPT_IMAGE_HDR{"Subsystem"}           = unpack("S", substr($opthdr, 68));
        $OPT_IMAGE_HDR{"DllCharacteristics"}  = unpack("S", substr($opthdr, 70));
        $OPT_IMAGE_HDR{"SizeOfStackReserve"}  = unpack("L", substr($opthdr, 72));
        $OPT_IMAGE_HDR{"SizeOfStackCommit"}   = unpack("L", substr($opthdr, 76));
        $OPT_IMAGE_HDR{"SizeOfHeapReserve"}   = unpack("L", substr($opthdr, 80));
        $OPT_IMAGE_HDR{"SizeOfHeapCommit"}    = unpack("L", substr($opthdr, 84));
        $OPT_IMAGE_HDR{"LoaderFlags"}         = unpack("L", substr($opthdr, 88));
        $OPT_IMAGE_HDR{"NumberOfRvaAndSizes"} = unpack("L", substr($opthdr, 92));

        my @RVAMAP =
        (
            "export",
            "import",
            "resource",
            "exception",
            "certificate",
            "basereloc",
            "debug",
            "archspec",
            "globalptr",
            "tls",
            "load_config",
            "boundimport",
            "importaddress",
            "delayimport",
            "comruntime",
            "none"
        );

        # parse the rva data
        my $rva_data = substr($opthdr, 96, $OPT_IMAGE_HDR{"NumberOfRvaAndSizes"} * 8 );
        for (my $x = 0; $x < $OPT_IMAGE_HDR{"NumberOfRvaAndSizes"}; $x++)
        {
            if (! $RVAMAP[$x]) { $RVAMAP[$x] = "unknown_$x" }
            $RVA{ $RVAMAP[$x] } =
                        [
                            unpack("L", substr($rva_data, ($x * 8))),
                            unpack("L", substr($rva_data, ($x * 8) + 4)),
                        ];
        }
    }
    
    # parse the section headers
    my $sec_begn = $peo + 24 + $IMAGE_HDR{"SizeOfOptionalHeader"};
    my $sec_data = substr($data, $sec_begn);
    
    for (my $x = 0; $x < $IMAGE_HDR{"NumberOfSections"}; $x++)
    {
        my $sec_head = $sec_begn + ($x * 40);
        my $sec_name = substr($data, $sec_head, 8);
        $sec_name =~ s/\x00//g;

        $SECTIONS{$sec_name} =
                    [
                       unpack("L", substr($data, $sec_head +  8)),
                       unpack("L", substr($data, $sec_head +  12)),
                       unpack("L", substr($data, $sec_head +  16)),
                       unpack("L", substr($data, $sec_head +  20)), 
                    ];
                    
        # delta to virtual from file offset inside this section
        $SECTIONS{$sec_name}->[4] = $SECTIONS{$sec_name}->[1] - $SECTIONS{$sec_name}->[3];
    }   
    
    #foreach (keys(%IMAGE_HDR)) { printf("%s\t0x%.8x\n", $_ , $IMAGE_HDR{$_}); }
    #foreach (keys(%OPT_IMAGE_HDR)) { printf("%s\t0x%.8x\n", $_ , $OPT_IMAGE_HDR{$_}); }
    #foreach (keys(%RVA)) { printf("%s\t0x%.8x [0x%.8x]\n", $_ , $RVA{$_}->[0], $RVA{$_}->[1] ); }
    #foreach (keys(%SECTIONS)) 
    #{ 
    #    printf("%s\t0x%.8x\t0x%.8x\t0x%.8x\t0x%.8x\t0x%.8x\n",
    #           $_ , $SECTIONS{$_}->[0], $SECTIONS{$_}->[1], $SECTIONS{$_}->[2], $SECTIONS{$_}->[3], $SECTIONS{$_}->[4]);
    #}
    
    return($object);    
}

sub OffsetToVirtual {
    my ($object, $offset) = @_;

    # if this image has no optional header and defined image base,
    # just return zero since we can't calculate the virtual
    if (! $OPT_IMAGE_HDR{"ImageBase"})
    {
        return(0);
    }

    foreach (keys(%SECTIONS)) 
    {
        if ($offset >= $SECTIONS{$_}->[3] && $offset < ($SECTIONS{$_}->[3] + $SECTIONS{$_}->[2]))
        {
            return($OPT_IMAGE_HDR{"ImageBase"} + $offset + $SECTIONS{$_}->[4]);
        }
    }   
        
    # not in any given section, return the offset + ImageBase    
    return($OPT_IMAGE_HDR{"ImageBase"} + $offset);
}

sub VirtualToOffset {
    my ($object, $virtual) = @_;
    if (! $virtual) { return(0) }
    
    foreach (keys(%SECTIONS)) 
    {
       if ($virtual > $SECTIONS{$_}->[1] && $virtual < ($SECTIONS{$_}->[0] + $SECTIONS{$_}->[1]))
       {
            return $virtual - $SECTIONS{$_}->[4];
       }
    }   
    return(0);
}

sub FindPEOffset {
    my ($object, $data_ref) = @_;
    my $peo = unpack("L", substr(${$data_ref}, 0x3c, 4));
    if (substr(${$data_ref}, 0, 2) ne "MZ"  || substr(${$data_ref}, $peo, 2) ne "PE") { return undef } 
    return($peo);
}
