Poor PAC File Performance–A WireShark Example

A problem I have come across repeatedly in large enterprises is poor web browsing performance due to over engineered PAC file. One of the most common causes of issues I’ve found is poor DNS resolution performance in conjunction with extensive DNS queries through the PAC file.

In this example the PAC file had 100+ IsInNet calls that resulted in very slow performance browsing. The affected site also suffered very poor DNS resolution performance. However removing the PAC file resulted in vastly improved browsing performance.

Some good guidelines for PAC files are here http://www.websense.com/content/support/library/web/v76/pac_file_best_practices/PAC_best_pract.aspx

Also a tool I built here PacDbg for analysing /debugging logic in PAC file https://chentiangemalc.wordpress.com/2013/09/30/pacdbg-custom-proxy-browser-set-proxy-cmd-line-tool/

Using Fiddler and the statistics it collects also is useful in analysing PAC performance issues http://www.telerik.com/fiddler

In this case we took two WireShark traces browsing a set of websites:

  • Test #1 – PAC file
  • Test #2 – Proxy set by IP address

These were repeated several times and results were consistent. ipconfig /flushdns run before each test.

Graphs generated using WireShark’s StatisticsI/O Graph menu option.

Test #1 – PAC File – Slow

Display Filter: dns.time

  • Y AXIS: LOAD(Y field)
  • Y field: dns.time

DNS query response time ranged from 0.1 – 2.4 seconds

image

The frequency of DNS queries also analysed:

  • Display Filter: dns.flag.response eq 0
  • Y AXIS: Packet/s

image

Test #2 – Fast – Proxy set by IP address

DNS query response still slow, but much less frequent.

image

image

Posted in PacDbg, WireShark | Tagged | Leave a comment

Case of the Missing Printer Ports

A Citrix environment had both Windows Server 2008 R2 published desktops and Windows 7 SP1  VDIs

On the Windows 7 VDIs the ports tab on printers added via a print server was always empty.

image

We did a quick comparison between working/broken system using AutoRuns compare functionality, but there were so many differences it was going to be easier to use debugging tools to find the cause.

The UI for this print dialog is hosted in Windows Explorer

So I started with Rohitab API Monitor and enabled tracing of Documents and Printing – Printing – Prints Spooler APIs

image

Here we saw the call to winspool.drv!EnumPorts was succeeding. However pcReturned was 0

image

Based on the MSDN documentation for EnumPorts here we know pcReturned is the number of ports retrieved from the print server.

On the working Server 2008 R2 machines this same call to EnumPorts showed pcReturned with a value of 628.

Using Windows in-built packet capture netsh trace we took a packet capture while opening the Ports tab. On the working machine we saw a DNS query to find the print server, then some additional network traffic to/from the print server as the ports were retrieved.

On the broken Windows 7 machines there was no network traffic at all, not even a DNS query.

To investigate the network call this time we attached API monitor to the Print Spooler service, spoolsv.exe and monitored Network APIs

image

In this case GetAddrInfo failed with error No Such Host Known

#    Time of Day    Thread    Module    API    Return Value    Error    Duration
18    5:06:27.421 PM    3    spoolsv.exe    GetAddrInfoW ( “PRINTSERVER01”, 0x0000000000000000, 0x0000000003c9e458, 0x0000000003c9e450 )    11001    11001 = No such host is known.     0.0000168

However if I called GetAddrInfo with the same parameters from my own program,, on the same computer it worked fine, and obtained the correct IP address for the print server.

So I attached WinDbg to the spoolsv.exe process and set breakpoint on GetAddrInfo and Set Last Error then traced some instructions

bp ws2_32!GetAddrInfoW

However it wasn’t immediately obvious what was causing the failure to resolve the host name.

To get a better understanding of call flow I set up a break point on spoolsv!EnumPortsW and created a trace log of 100,000 instructions

bp spoolsv!EnumPortsW
.logopen C:\logs\trace.txt
t 100000
.logclose

Then I used some quick & dirty PowerShell to parse this in an easy to follow manner:

$data = Get-Content C:\log\trace.txt

$last =""

ForEach ($line in $data)
{
  if ($line.Contains("!") -and $line.Contains("+"))
  {
    $b=$line.Split('+')
    if ($b[0].Contains("!") -and !$b[0].Contains(" ") -and !$b[0].StartsWith("ntdll"))
    {
    if ($last -ne $b[0])
    {
        Write-Output $b[0]
        $last=$b[0]
        }
    }
  }
}

From this it was easy to identify 3rd party code was getting executed during EnumPorts call:

spoolsv!TNameResolutionCache::GetNetBiosInfo
msvcrt!free
ntdll!RtlFreeHeap
msvcrt!free
spoolsv!TNameResolutionCache::GetNetBiosInfo
netutils!NetApiBufferFree
netutils!MIDL_user_free
KERNELBASE!LocalFree
ntdll!RtlFreeHeap
KERNELBASE!LocalFree
netutils!MIDL_user_free
netutils!NetApiBufferFree
spoolsv!TNameResolutionCache::GetNetBiosInfo
spoolsv!TNameResolutionCache::AddName
spoolsv!NCoreLibrary::TString::~TString
spoolsv!TNameResolutionCache::AddName
spoolsv!NCoreLibrary::TList<TConnection>::~TList<TConnection>
spoolsv!NCoreLibrary::TLink::Next
spoolsv!NCoreLibrary::TList<TConnection>::~TList<TConnection>
spoolsv!NCoreLibrary::TLink::~TLink
spoolsv!TNameResolutionCache::AddName
spoolsv!NCoreLibrary::TList<TConnection>::~TList<TConnection>
spoolsv!NCoreLibrary::TLink::Next
spoolsv!NCoreLibrary::TList<TConnection>::~TList<TConnection>
spoolsv!NCoreLibrary::TLink::~TLink
spoolsv!TNameResolutionCache::AddName
spoolsv!ImpersonatePrinterClient
spoolsv!ImpersonationToken
KERNELBASE!GetLastError
spoolsv!ImpersonationToken
ADVAPI32!GetTokenInformationStub
KERNELBASE!GetTokenInformation
ntdll!ZwQueryInformationToken
KERNELBASE!GetTokenInformation
KERNELBASE!BaseSetLastNTError
ntdll!RtlNtStatusToDosError
ntdll!RtlNtStatusToDosErrorNoTeb
ntdll!RtlNtStatusToDosError
KERNELBASE!BaseSetLastNTError
ntdll!RtlSetLastWin32Error
KERNELBASE!BaseSetLastNTError
KERNELBASE!GetTokenInformation
ADVAPI32!GetTokenInformationStub
spoolsv!ImpersonationToken
ntdll!RtlSetLastWin32Error
spoolsv!ImpersonationToken
spoolsv!ImpersonatePrinterClient
ntdll!NtSetInformationThread
spoolsv!ImpersonatePrinterClient
ntdll!ZwClose
spoolsv!ImpersonatePrinterClient
spoolsv!TNameResolutionCache::AddName
spoolsv!_security_check_cookie
spoolsv!TNameResolutionCache::AddName
spoolsv!EnumPortsW
spoolsv!YEnumPorts
spoolsv!EnumPortsW
UpProv!InitializePrintProvidor

From the complete trace log we could see spoolsv!EnumPortsW had a line of the code that initially entered localspl!LocalEnumPorts

spoolsv!EnumPortsW+0xa4
call    qword ptr [rbp+150h] ss:00000000`02783390={localspl!LocalEnumPorts (000007fe`ebaa8e20)}

However the 2nd time around this call went into Citrix Universal Print Client code:

spoolsv!EnumPortsW+0xa4:
00000000`ff1fd508 ff9550010000    call    qword ptr [rbp+150h] ss:00000000`0279bf70=000007fedc7d5170
0:001> u 000007fedc7d5170
UpProv!InitializePrintProvidor+0x1240:
000007fe`dc7d5170 44894c2420      mov     dword ptr [rsp+20h],r9d
000007fe`dc7d5175 4c89442418      mov     qword ptr [rsp+18h],r8
000007fe`dc7d517a 89542410        mov     dword ptr [rsp+10h],edx
000007fe`dc7d517e 48894c2408      mov     qword ptr [rsp+8],rcx
000007fe`dc7d5183 4883ec78        sub     rsp,78h
000007fe`dc7d5187 4883bc248000000000 cmp   qword ptr [rsp+80h],0
000007fe`dc7d5190 7512            jne     UpProv!InitializePrintProvidor+0x1274 (000007fe`dc7d51a4)
000007fe`dc7d5192 b932000000      mov     ecx,32h

In WinDbg checked where this DLL originated…

0:002> lmvm upprov
Browse full module list
start             end                 module name
000007fe`dc7d0000 000007fe`dc8e6000   UpProv     (export symbols)       C:\Program Files\Citrix\Universal Print Client\UpProv.dll
    Loaded symbol image file: C:\Program Files\Citrix\Universal Print Client\UpProv.dll
    Image path: C:\Program Files\Citrix\Universal Print Client\UpProv.dll
    Image name: UpProv.dll
    Browse all global symbols  functions  data
    Timestamp:        Sun Sep 13 14:27:23 2015 (55F4FB2B)
    CheckSum:         0011AE23
    ImageSize:        00116000
    File version:     7.6.300.7024
    Product version:  7.6.300.7024
    File flags:       8 (Mask 3F) Private
    File OS:          40004 NT Win32
    File type:        2.0 Dll
    File date:        00000000.00000000
    Translations:     0409.04b0
    CompanyName:      Citrix Systems, Inc.
    ProductName:      Citrix ICA Host
    InternalName:     UPPROV
    OriginalFilename: UPPROV.DLL
    ProductVersion:   7.6
    FileVersion:      7.6.300.7024
    FileDescription:  Citrix Universal Printing Provider
    LegalCopyright:   Copyright 2012-2015 Citrix Systems, Inc.

Renaming the upprov.dll and restarting Print Spooler service fixed the issue.

Further investigating the Universal Printing Client configuration we identified it had a Citrix Group Policy set to disable it :

Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Citrix\PrintingPolicies]
"UpsHttpPort"=dword:00001f90
"UpsEnable"=dword:00000000

Restoring upprov.dll then setting UpsEnable to 1 (Enabled with fallback to Windows native remote printing) or 2 (Enabled with no fallback to Windows native remote printing) and restarting the Print Spooler service also fixed the issue.

Posted in API Monitor, Citrix, PowerShell, WinDbg | Tagged | Leave a comment

Decompile Compiled VBS EXE with WinDbg

In this case we are looking  at a 32-bit EXE with WinDbg (x86) from the Windows SDK.

This exact process may or may not work depending on how the script was compiled, but the technique will be similar for many different types of interpreted scripts that are “compiled” to EXE

First we open the executable in WinDbg with File –> Open Executable

We then run the following commands

bp vbscript!COleScript::AddNamedItem
g
bp oleaut32!SysAllocStringLen
g

Now EAX is pointing to the beginning of our decompiled script, which we can check with du @EAX

If this doesn’t point to any script, you can instead try creating a dmp file with .dump /ma <filename.dmp> then use http://live.sysinternals.com/strings.exe to parse the dmp, i.e. strings filename.dmp > out.txt and examine the output for decompiled script.

image

To write the script to a VBS file type the following additional commands:

pt
.writemem c:\support\decompiled.vbs @edi @edi+@edx

image

Open in Notepad or other editor, ensuring to select “UNICODE” format

image

Posted in Reverse Engineering, WinDbg | Tagged | Leave a comment

Case of the Slow Downloads Folder

In Windows 10 on Surface Pro 4, performance is mostly pretty quick. But the first time opening Downloads folder after a restart it was very slow, taking 11 seconds just to display 114 files and 35 directories.

To confirm issue was repeatable I tried rebooting system 3 times and each time the issue occurred when browsing Downloads folder.

Using Process Monitor I took a trace with filter

  • Path Includes Downloads

From this I noticed a very high number of ReadFile events coming from Explorer.exe

Most files in the directory had no ReadFile events related to them. They looked like this:

image

However some files had hundreds of ReadFile events:

image

Right clicking a single ReadFile event and viewing Properties we can view stack whicfh may give us some clues us to what is causing this.

image

Interestingly this ReadFile operation, although taking some time, was only occurring on a few files, some DOCX, PPTX, and EXE file.

However this didn’t happen for all files of those type in the Downloads folder.

With a ProcMon filter set to

  • Process Name is Explorer.exe
  • Operation is ReadFile

I found the six files this operation occurred on

I removed those files from the folder

Restarted the machine. Downloads folder opened instantly.

Restarted the machine again, Downloads folder opened instantly again.

So I copied back the “bad files” into the Downloads folder…

The issue could no longer be reproduced…

I took my original ProcMon file from when Issue was occurring, and selected a top ReadFile event from affected machine

I then reset the filter to show all events, and could see that Windows Antimalware (MsMpEng.exe) had scanned the file just before these ReadFile events

image

After I had moved the files, Explorer behaved differently.

When I filtered on

  • Process Name is MsMpEng.exe
  • Path Contains Downloads

Now when I clicked Downloads folder in Explorer, these files were no longer being scanned

image

However as I started to scroll through the directory listing they did get scanned sometimes, seemingly randomly, but without any noticeable performance impact..image

Posted in Performance, ProcMon, Window, Windows 10 | Tagged , , | Leave a comment

Retrieve Time Machine Password Last Reset via PowerShell

In diagnosing a machine lockout issue I wanted to find the time that:

  • Machine Password was last set in Active Directory
  • Machine Password was last set locally

By default when a machine has a healthy connection to Active Directory you should see the following:

  • Local and Active Directory Times should match (May be out by a few seconds)
  • The time should not be older than 30 days (Unless default max. password age has been modified from 30 days)

The script can be downloaded here, instructions for use contained in the script.

https://onedrive.live.com/redir?resid=93A8E9D387076121!23829&authkey=!AJXBw6m_bmSSRko&ithint=file%2czip

# Get-MachinePasswordLastSetDate by Malcolm McCaffery (@chentiangemalc)
# https://chentiangemalc.wordpress.com
#
#
# Retrieves the date/time Windows machine account was last set
# 
# For Location LocalMachine = Must run as local Administrator
# For Location ActiveDirectory = Must run as, or specify credential, with appropriate Active Directory access
#
# Usage examples:
#
# Get-MachinePasswordLastSetDate -Location ActiveDirectory
# Get-MachinePasswordLastSetDate -Location LocalMachine
# Get-MachinePasswordLastSetData -Location ActiveDirectory -ComputerName server01
# Get-MachinePasswordLastSetData -Location ActiveDirectory -ComputerName server01 -Server DC02
# Get-MachinePasswordLastSetData -Location ActiveDirectory -ComputerName server01 -Server DC02 -Domain other.domain.local -Credential (Get-Credential)

Set-StrictMode -Version Latest

Add-Type -TypeDefinition @'
public enum AccountLocation {
    ActiveDirectory,
    LocalMachine
}
'@

Function Get-MachinePasswordLastSetDate
{
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = $true)]
        [AccountLocation]$Location
    )

    DynamicParam 
    {
        $paramDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
        
        if ($Location -eq 'ActiveDirectory')
        {
            # COMPUTERNAME PARAMETER
            $computerAttributeCollection = new-object System.Collections.ObjectModel.Collection[System.Attribute]
            $computernameAttribute = New-Object System.Management.Automation.ParameterAttribute
            $computernameAttribute.Mandatory = $false
            $computernameAttribute.HelpMessage = "The name of computer object to search for in Active Directory"
            $computerAttributeCollection.Add($computernameAttribute)

            #add our paramater specifying the attribute collection
            $computernameParam = New-Object System.Management.Automation.RuntimeDefinedParameter('ComputerName', [string], $computerAttributeCollection)
 
            #expose the name of our parameter
            $paramDictionary.Add('ComputerName', $computernameParam)

            # CREDENTIAL Parameter
            $credentialAttributeCollection = new-object System.Collections.ObjectModel.Collection[System.Attribute]
            $credentialAttribute = New-Object System.Management.Automation.ParameterAttribute
            $credentialAttribute.Mandatory = $false
            $credentialAttribute.HelpMessage = "Specifies a user account that has permission to perform this action. The default is the current user."
            $credentialAttributeCollection.Add($credentialAttribute)

            #add our paramater specifying the attribute collection
            $credentialParam = New-Object System.Management.Automation.RuntimeDefinedParameter('Credential', [PSCredential], $credentialAttributeCollection)
 
            #expose the name of our parameter
            $paramDictionary.Add('Credential', $credentialParam)

            # SERVER Parameter
            $serverAttributeCollection = new-object System.Collections.ObjectModel.Collection[System.Attribute]
            $serverAttribute = New-Object System.Management.Automation.ParameterAttribute
            $serverAttribute.Mandatory = $false
            $serverAttribute.HelpMessage = "Specifies the domain controller to use."
            $serverAttributeCollection.Add($serverAttribute)

            #add our paramater specifying the attribute collection
            $serverParam = New-Object System.Management.Automation.RuntimeDefinedParameter('Server', [String], $serverAttributeCollection)
 
            #expose the name of our parameter
            $paramDictionary.Add('Server', $serverParam)

            # DOMAIN parameter
            $domainAttributeCollection = new-object System.Collections.ObjectModel.Collection[System.Attribute]
            $domainAttribute = New-Object System.Management.Automation.ParameterAttribute
            $domainAttribute.Mandatory = $false
            $domainAttribute.HelpMessage = "Specifies the FQDN of domain to use."
            $domainAttributeCollection.Add($domainAttribute)

            #add our paramater specifying the attribute collection
            $domainParam = New-Object System.Management.Automation.RuntimeDefinedParameter('Domain', [String], $domainAttributeCollection)
 
            #expose the name of our parameter
            $paramDictionary.Add('Domain', $domainParam)


        }

        return $paramDictionary
    }

    Process 
    {
        $result = $null
        switch ($Location)
        {
            "ActiveDirectory"
            {
                $computerName = $paramDictionary["ComputerName"].Value
                $domain = $paramDictionary["Domain"].Value
                $credential = $paramDictionary["Credential"].Value
                $server = $paramDictionary["Server"].Value

                if ($computerName -eq $null) { $computerName = $env:COMPUTERNAME }

                # EASY WAY
                #$computer = Get-ADComputer -Identity $ComputerName -Credential $credential -Server $server -Properties "PasswordLastSet"
                #$result = $computer.PasswordLastSet

                # Using ADSI to use on machines without ActiveDirectory PowerShell Module
                $Searcher = New-Object -TypeName System.DirectoryServices.DirectorySearcher -ErrorAction Stop
                $Searcher.Filter = "(&(objectCategory=Computer)(name=$ComputerName))"
                $Searcher.SizeLimit = 0
                $Searcher.SearchRoot = ([ADSISearcher]"").SearchRoot.Path
                if ($server -ne $null)
                {
                    $Searcher.SearchRoot = [String]::Format("LDAP://{0}/{1}",$server,([ADSISearcher]"").SearchRoot.Path.Substring(7))
                }
                if ($domain -ne $null)
                {
                    [String]$DomainDN = "LDAP://"
                    if ($server -ne $null) { $DomainDN += $server + "/" }

                    $DNSArray = $Domain.Split('.')
                    for ($x = 0; $x -lt $DNSArray.Length - 1 ; $x++) 
                    { 
                        $DomainDN += "DC=$($DNSArray[$x]),"
                    } 
                    $DomainDN += "DC=$($DNSArray[$DNSArray.Length-1])"
    
                    $Searcher.SearchRoot = $DomainDN
                }

                if ($credential -ne $null) 
                {
                    $Domain = New-Object -TypeName System.DirectoryServices.DirectoryEntry 
                        -ArgumentList $DomainDN,$($Credential.UserName),$($Credential.GetNetworkCredential().password) 
                    $Searcher.SearchRoot = $Domain
                }
                    
                $computer = $searcher.FindOne()
                $pwdLastSetTime = $computer.Properties["pwdLastSet"][0]
                $result = [DateTime]::FromFileTime($pwdLastSetTime)

            }

            "LocalMachine" 
            {
                # Scheduled Task Used To Access Registry key as SYSTEM account
                $taskName ="ExtractMachineSecrets"
                $taskDescription = "Extract Machine Policy Secrets"
                $regKey = "HKLM\SECURITY\Policy\Secrets\`$MACHINE.ACC\CupdTime"
                
                # the number of attempts to wait for scheduled task output
                $maxTries = 10
                
                $fileName = [System.IO.Path]::GetTempFileName()
                if (Test-Path $filename) { Remove-Item $filename -Force }

                # must run as SYSTEMfs to access these registry keys
                # even local Administrator does not have access
                $principal = New-ScheduledTaskPrincipal -LogonType ServiceAccount -RunLevel Highest -UserId "SYSTEM"
    
                # use reg command line tool to export required registry data
                $action = New-ScheduledTaskAction -Execute "reg.exe" -Argument ([String]::Format('export "{0}" "{1}"',$regKey,$filename))
               
                # must apply these settings or task won't run on laptop running on battery
                $settings = New-ScheduledTaskSettingsSet -DontStopIfGoingOnBatteries -AllowStartIfOnBatteries 
    
                # create scheduled task
                $task = Register-ScheduledTask -TaskName $taskName -Description $taskDescription -Action $action -Principal $principal -Settings $settings -Force
                $start = Start-ScheduledTask -TaskName $taskName

                $counter = 0
                while ($counter -lt $maxTries)
                {
                    if (Test-Path $filename) { break }
                    Start-Sleep -Milliseconds 500
                    $counter++
                }

                if (!(Test-Path $fileName))
                {
                    Throw "$fileName not found!"
                }
                else
                {
                    $content = Get-Content $fileName
    
                    # extract comma separated hex values from reg file on line 3
                    $hexData = $content[3].Substring(9)

                    # byte array to store the hex values
                    [byte[]]$bytes = @()
                    for ($i = 0; $i -lt 8; $i++)
                    {
                        $bytes += [Convert]::ToInt32($hexData.Split(",")[$i],16)
                    }

                    # convert bytes into Int64
                    $time = [System.BitConverter]::ToInt64($bytes,0)
        
                    # convert Int64 to DateTime
                    $result = [DateTime]::FromFileTime($time)
                }

                Unregister-ScheduledTask -TaskName $taskName -Confirm:$false
                Remove-Item  $fileName -Force
            }
       
        }
        return $result
    }
}

Posted in .NET, Active Directory, PowerShell | Tagged | Leave a comment

Using PowerShell + Word Interop to Extract Tables from PDFs

I needed to extract some tables from PDFs. If you’ve ever done this before you realise PDF format doesn’t really contain table information, it just contains lines and positions of text. Testing out Tabula http://tabula.technology/ which worked reasonably well, but didn’t handle tables split across pages.

So I used PowerShell and Word Interop with Microsoft Office 2016 to extract the tables into CSV files.

This script will extract every table into an individual CSV file named after the PDF. i.e. Document.pdf will produce Document1.csv, Document2.csv etc

Currently header names are not taken from the tables, and are instead labelled Column1, Column2, etc. This is due to the fact I was working with a lot of tables that didn’t have header names.

The script can be downloaded here:

https://onedrive.live.com/redir?resid=93A8E9D387076121!23690&authkey=!ADJ_3Kn6-tMBO4Q&ithint=file%2czip

# USAGE
# Extract-PdfTables -FileName <PDF FILE>
# Extracts a single CSV in same directory as PDF file
# For every table found in the PDF file
# PDFFile1.CSV, PDFFile2.CSV etc

Set-StrictMode -Version Latest

# function from http://stackoverflow.com/questions/5544844/how-to-call-a-complex-com-method-from-powershell
Function Invoke-NamedParameter {
    [CmdletBinding(DefaultParameterSetName = "Named")]
    param(
        [Parameter(ParameterSetName = "Named", Position = 0, Mandatory = $true)]
        [Parameter(ParameterSetName = "Positional", Position = 0, Mandatory = $true)]
        [ValidateNotNull()]
        [System.Object]$Object
        ,
        [Parameter(ParameterSetName = "Named", Position = 1, Mandatory = $true)]
        [Parameter(ParameterSetName = "Positional", Position = 1, Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]$Method
        ,
        [Parameter(ParameterSetName = "Named", Position = 2, Mandatory = $true)]
        [ValidateNotNull()]
        [Hashtable]$Parameter
        ,
        [Parameter(ParameterSetName = "Positional")]
        [Object[]]$Argument
    )

    end {  ## Just being explicit that this does not support pipelines
        if ($PSCmdlet.ParameterSetName -eq "Named") {
            ## Invoke method with parameter names
            ## Note: It is ok to use a hashtable here because the keys (parameter names) and values (args)
            ## will be output in the same order.  We don't need to worry about the order so long as
            ## all parameters have names
            $Object.GetType().InvokeMember($Method, [System.Reflection.BindingFlags]::InvokeMethod,
                $null,  ## Binder
                $Object,  ## Target
                ([Object[]]($Parameter.Values)),  ## Args
                $null,  ## Modifiers
                $null,  ## Culture
                ([String[]]($Parameter.Keys))  ## NamedParameters
            )
        } else {
            ## Invoke method without parameter names
            $Object.GetType().InvokeMember($Method, [System.Reflection.BindingFlags]::InvokeMethod,
                $null,  ## Binder
                $Object,  ## Target
                $Argument,  ## Args
                $null,  ## Modifiers
                $null,  ## Culture
                $null  ## NamedParameters
            )
        }
    }
}


Function Extract-PdfTables
{
    param([string]$FileName)

    Add-Type -Assembly "Microsoft.Office.Interop.Word"

    $item = Get-ChildItem $filename
    $baseFile = [System.IO.Path]::Combine($item.DirectoryName,$item.BaseName)

    $word = New-Object -ComObject Word.Application 
    $word.Visible = $true 

    # check Word Version, required for PDF conversion capability
    if ($word -eq $null -or $word.Version -lt 16)
    {
        Throw "Requires Microsoft Word 2016 or later"
    }

    # disable convert PDF warning prompt
    Set-ItemProperty -Path HKCU:\SOFTWARE\Microsoft\Office\16.0\Word\Options -Name DisableConvertPdfWarning -Value 0x1
    
    # open PDF with Microsoft Word
    $doc = Invoke-NamedParameter $word.Documents "Open" @{ "FileName" = $filename ; "AddToRecentFiles" = $false ; "ConfirmConversions" = $false ; "ReadOnly" = $true ; "Visible"=$false } 

    $tableCount = 0
    $lastTable = $null
    ForEach ($table in $doc.Tables)
    {
        # tables split across pages in PDFs will show up as separate tables
        # we will try to auto-detect if table is continuing across page
        # in this case $newTable will be set to false
        $newTable = $true
        
        # check for continuing table
        if ($lastTable -ne $null)
        {
            # Get the range between previous table and current table
            $RangeBetweenTables = $lastTable.Range
            $RangeBetweenTables.Collapse([Microsoft.Office.Interop.Word.WdCollapseDirection]::wdCollapseEnd)
            $RangeStartOfNextTable = $table.Range
            $RangeStartOfNextTable.Collapse([Microsoft.Office.Interop.Word.WdCollapseDirection]::wdCollapseStart)
            $RangeBetweenTables.End = $RangeStartOfNextTable.Start -1

            # Count lines separating tables
            $lineCount = $RangeBetweenTables.ComputeStatistics([Microsoft.Office.Interop.Word.WdStatistic]::wdStatisticLines)
            if ($lastTable.Columns.Count -eq $table.Columns.Count -and $lineCount -eq 0)
            {
                $newTable = $false
            }
        }

        $lastTable = $table
    
        if ($newTable)
        {
            $rowOffset = 0
            $tableCoun
            "Table # $tableCount"
            $dataTable = New-Object System.Data.DataTable "Table$tableCount"

            # build the columns for our table - these will get default names Column1,Column2
            For ($i = 0; $i -le $table.Columns.Count; $i++ )
            {
                $dataTable.Columns.Add()
            }
        }
        else
        {
            $rowOffset = $dataTable.Rows.Count - 1
        }

        For ($i = 0; $i -le $table.Rows.Count; $i++)
        {
            [void]$dataTable.Rows.Add() 
        }


        ForEach ($row in $table.Rows)
        {
            ForEach ($cell in $row.Cells)
            {
                # remove some unprintable characters that may be at end of cell value
                $text = $cell.Range.Text.Trim()
                $text = $text.Replace([string][char]13,"")
                $text = $text.Replace([string][char]7,"")

                $dataTable.Rows[$rowOffset + $cell.RowIndex -1][$cell.ColumnIndex-1] = $text
            }
        }

        $dataTable | Export-Csv -NoTypeInformation -Path "$baseFile$tableCount.csv"
    }
    $doc.Close
    $word.Quit
}
Posted in Office, PowerShell | Tagged | Leave a comment

Case of the Unwanted Chrome Search Modification

A friend’s machine had the Google Chrome home page set to piesearch.com

image

But there is no homepage set in settings.

image

Checking the Chrome Shortcut in Start Menu we can see it’s been modified, so we remove the piesearch parameter. This setting needs Administrative privilege to change because Chrome was installed to “All Users”

image

That fixed the homepage, but now searching in the omnibox resulted in searches from coldsearch.com NOT google which had been set before:

image

When settings were opened it was advised this was set by the administrator, and the default could not be changed.

I did a ProcMon trace and used filter Details Contains coldsearch to identify registry keys with the value coldsearch but got no hits.

So I changed ProcMon filter to

  • Process Name is Chrome.exe
  • Result is Success

Then using ProcMon –> Tools | Summary could quickly identify the folders Chrome accessed files from:

image

We could see app settings loading from C:\Users\<Username>\AppData\Local\Google\Chrome

However renaming this folder didn’t remove the search setting.

To find where the setting might be stored I used SearchMyFiles http://www.nirsoft.net/utils/search_my_files.html )

And limited the search to those folders found with ProcMon to speed it up:

image

This machine was not supposed to have any Group Policy settings, so I removed the Registry.pol file, which represents Local Machine Group Policy.

Using the tool here https://sdmsoftware.com/gpoguy/free-tools/library/registry-pol-viewer-utility/ we could view the .pol file (Requires .NET 3.5, Free registration required to download)

image

With the Registry.Pol file removed, Chrome was back to Normal.

Finally we cleaned up the Mozilla Firefox directory settings by backing up, then deleting the files under C:\Users\<username>\AppData\Roaming\Mozilla\Firefox\Profiles

Microsoft Edge had not been affected, and Internet Explorer settings modification had been blocked by Windows Defender.

Posted in Group Policy, Internet Explorer, ProcMon, Sys, SysInternals | Leave a comment