用 PowerShell 移除 Evernote 的广告

破解过程

Evernote(印象笔记)免费用户的左下角有个正方形的广告,点击关闭按钮反而会出来一个对话框:

虽然破解 + 写脚本 + 写这篇博客花了一两个小时,但是如果能节约更多读者的时间,并且提高一点技术水平,也算有益了吧。

我们用 Visual Studio 中的 Spy++ 查看一下控件的窗口类名:

得到的结果是“ENAdBrowserCtrl”,从窗口类名来看,似乎是为了广告而设计的。出于保险起见,用 WinHex 搜索了一下,这个字符串只出现一次,并且是采用双字节编码的“45004E0041006400420072006F0077007300650072004300740072006C”。

我们尝试破坏这个字符串试试:用 WinHex 的“填充选块”功能,将这块区域替换成 00,然后保存运行,广告果然没有了。

但是手工修改毕竟比较麻烦,而且未来版本更新以后还要再次破解。所以简单写了个 PowerShell 脚本来自动完成破解。

PowerShell 自动化脚本

请将以下代码保存成 Remove-EvernoteAD.ps1 并以管理员身份执行 :)
脚本的思路是以二进制的方式搜索指定的模式(pattern),并替换成新的模式。涉及到一些字节操作和进制转换。

$pattern = '45004E0041006400420072006F0077007300650072004300740072006C'
$replacement = $pattern -replace '.', '0'

function Replace-Pattern ($buffer, $pattern, $replacement) {
    $isPatternMatched = $false
    for ($offset = 6220000; $offset -lt $buffer.Length - $pattern.Length; $offset++) {
        $isByteMatched = $true
        for ($patternOffset = 0; $patternOffset -lt $pattern.Length; $patternOffset++) {
            if ($buffer[$offset + $patternOffset] -ne $pattern[$patternOffset]) {
                $isByteMatched = $false
                break
            }
        }
        if ($isByteMatched) {
            $isPatternMatched = $true
            break
        }
    }

    if ($isPatternMatched) {
        for ($index = 0; $index -lt $pattern.Length; $index++) {
            $buffer[$offset + $index] = [byte]0
        }

        return $true
    } else {
        return $false
    }
}

function Convert-HexStringToByteArray
{
    ################################################################
    #.Synopsis
    # Convert a string of hex data into a System.Byte[] array. An
    # array is always returned, even if it contains only one byte.
    #.Parameter String
    # A string containing hex data in any of a variety of formats,
    # including strings like the following, with or without extra
    # tabs, spaces, quotes or other non-hex characters:
    # 0x41,0x42,0x43,0x44
    # x41x42x43x44
    # 41-42-43-44
    # 41424344
    # The string can be piped into the function too.
    ################################################################
    [CmdletBinding()]
    Param ( [Parameter(Mandatory = $True, ValueFromPipeline = $True)] [String] $String )

    #Clean out whitespaces and any other non-hex crud.
    $String = $String.ToLower() -replace '[^a-f0-9\\\,x\-\:]',''

    #Try to put into canonical colon-delimited format.
    $String = $String -replace '0x|\\x|\-|,',':'

    #Remove beginning and ending colons, and other detritus.
    $String = $String -replace '^:+|:+$|x|\\',''

    #Maybe there's nothing left over to convert...
    if ($String.Length -eq 0) { ,@() ; return }

    #Split string with or without colon delimiters.
    if ($String.Length -eq 1)
    { ,@([System.Convert]::ToByte($String,16)) }
    elseif (($String.Length % 2 -eq 0) -and ($String.IndexOf(":") -eq -1))
    { ,@($String -split '([a-f0-9]{2})' | foreach-object { if ($_) {[System.Convert]::ToByte($_,16)}}) }
    elseif ($String.IndexOf(":") -ne -1)
    { ,@($String -split ':+' | foreach-object {[System.Convert]::ToByte($_,16)}) }
    else
    { ,@() }
    #The strange ",@(...)" syntax is needed to force the output into an
    #array even if there is only one element in the output (or none).
}

echo '本程序用于去除 Evernote 非会员左下角的正方形广告。'
echo '请稍候……'

$patternArray = Convert-HexStringToByteArray $pattern
$replacementArray = Convert-HexStringToByteArray $replacement


$path = "${Env:ProgramFiles}\Evernote\Evernote\Evernote.exe"
$path86 = "${Env:ProgramFiles(x86)}\Evernote\Evernote\Evernote.exe"
if (Test-Path $path) {
    $execute = Get-Item $path
} elseif (Test-Path $path86) {
    $execute = Get-Item $path86
} else {
    Write-Warning '没有找到 Evernote.exe。'
    exit
}

$exe = gc $execute -ReadCount 0 -Encoding byte

if (Replace-Pattern $exe $patternArray $replacementArray) {
    $newFileName = $execute.Name + '.bak'
    $newPath = Join-Path $execute.DirectoryName $newFileName
    Stop-Process -Name Evernote -ErrorAction SilentlyContinue
    Move-Item $execute $newPath
    Set-Content $execute -Value $exe -Encoding Byte
    echo '广告去除成功!Evernote 未来升级后需重新运行本程序。'
    Start-Process $execute
} else {
    Write-Warning '无法去除广告,是否已经去除过了?'
    if (!(Get-Process -Name Evernote -ErrorAction SilentlyContinue)) {
        Start-Process $execute
    }
}

您也可以从这里 下载 写好的脚本。

PowerShell 技能连载 - 在 ISE 编辑器中打开文件

如果您想在 ISE 编辑器中打开一个脚本,一个快捷的方法是使用命令“ise”。例如要打开您的配置脚本(每次 ISE 启动时自动调用的脚本),请试试一下代码:

PS> ise $profile

您现在可以方便地增加或删除您希望 ISE 每次启动时自动执行的命令。

如果您的配置脚本不存在,以下单行的代码可以为您创建一个(它将覆盖已经存在的文件,所以只能在文件确实还不存在的时候运行它):

PS> New-Item -Path $profile -ItemType File -Force

您也可以从一个 PowerShell.exe 控制台中使用“ise”命令来启动一个 ISE 编辑器。

(顺便提一下:有一个叫做 psEdit 的函数使用效果十分相似,但是只在 PowerShell ISE 内部有效,在 PowerShell.exe 控制台中无效。)

技术分析:VIM,PowerShell 和签名代码

摘要:在 UNIX 和 Linux 世界中,vi 和 EMACS 长期占据了处理大量代码或其他文本的最佳编辑器的位置。后来,一个称为 VIM(Vi, IMproved 的简称)的 vi 改进克隆版出现了。VIM 具有语法高亮、一个类似 vi 的命令行界面,以及更多强大的编辑大型文本工程的功能。它很快成为 Windows 世界之外最好用的文本编辑器之一。本文关注 VIM 和 Windows PowerShell 的配合使用,并讨论如何进行代码签名。

下载 PDF 文档:《technical-analysis-vim-powershell-and-signed-code.pdf》。

PowerShell 技能连载 - 在 PowerShell ISE 中使用块注释

从 PowerShell 3.0 开始,您可以按住 ALT 键并选择一些内容,来获取一个矩形选区。

如果您尽可能地缩窄这个选区(您将只看到一条细细的蓝线),您可以方便地在选区之内增加或删除字符。只需要按下“#”键即可块注释它们,或者删除 # 号重新启用这段代码。

PowerShell 技能连载 - 查找远程计算机上已登录的用户

在上一个技巧当中我们使用 quser.exe 来查询本机当前登录的用户。以下是一个支持查询远程计算机上已登录用户的函数。有个额外的好处是,返回的信息附加了一个名为“ComputerName”的属性,所以当您查询多台计算机时,您将可以知道结果是属于那一台计算机的:

function Get-LoggedOnUser
{
  param([String[]]$ComputerName = $env:COMPUTERNAME)

    $ComputerName | ForEach-Object {
      (quser /SERVER:$_) -replace '\s{2,}', ',' |
        ConvertFrom-CSV |
        Add-Member -MemberType NoteProperty -Name ComputerName -Value $_ -PassThru
  }
}

以下是一个调用的例子,查询本地计算机以及一台远程计算机:

PowerShell 技能连载 - 查询已登录的用户

有一个十分有用的控制台程序叫做 quser.exe 可以告诉您哪些用户登录到了一台机器上。该可执行程序返回的是纯文本,但通过一点点正则表达式,该文本可以转换成 CSV 并导入 PowerShell。

以下代码以对象的形式返回所有当前登录到您机器上的用户信息:

(quser) -replace 's{2,}', ',' | ConvertFrom-Csv

PowerShell 技能连载 - 查询登录失败记录

只要有人使用错误的凭据登录,就会在安全日志中产生一条日志记录。以下是一个可以从安全日志中读取这些事件的函数(需要管理员特权)。它能够列出所有日志中非法的登录信息:

# requires Admin privileges!
function Get-LogonFailure
{
      param($ComputerName)
      try
      {
          Get-EventLog -LogName security -EntryType FailureAudit -InstanceId 4625 -ErrorAction Stop @PSBoundParameters |
                  ForEach-Object {
                    $domain, $user = $_.ReplacementStrings[5,6]
                    $time = $_.TimeGenerated
                    "Logon Failure: $domain\$user at $time"
                }
      }
      catch
      {
            if ($_.CategoryInfo.Category -eq 'ObjectNotFound')
            {
                  Write-Host "No logon failures found." -ForegroundColor Green
            }
            else
            {
                  Write-Warning "Error occured: $_"
            }

      }

}

请注意这个函数还可以在远程主机上运行。请使用 -ComputerName 参数来查询一台远程主机。远程主机需要运行 RemoteRegistry 服务,并且您需要在目标机器上的本地管理员权限。

用 PowerShell 屏蔽腾讯 QQ 的广告

非会员 QQ,在对话窗口的右上角会显示一个广告横幅,如图所示:

我们可以将 %appdata%\Tencent\Users\QQ号\QQ\Misc.db 文件删除并且替换成一个同名文件夹,就可以屏蔽该广告:

如果您有多个 QQ 号的话,我们可以用 PowerShell 来批量完成该任务:

echo '本脚本用于屏蔽 QQ 对话窗口右上方的广告条。'
Read-Host '请关闭所有 QQ,按回车键继续' | Out-Null

$usersDir = "$($env:AppData)\Tencent\Users\"
dir $usersDir -Directory | foreach {
    $qq = $_
    $qqDir = Join-Path $_.FullName 'QQ'
    $miscDb = Join-Path $qqDir Misc.db
    if (Test-Path -PathType Leaf $miscDb) {
        echo "正在禁用 $qq 的广告"
        del $miscDb
        md $miscDb | Out-Null
    }
}
exit
echo '处理完毕。'

您也可以从这里 下载 写好的脚本,祝您使用愉快。
本方法在 QQ2013(SP6) 上验证通过。

PowerShell 技能连载 - 更改桌面背景

PowerShell 可以通过调用 Windows API,实现更改当前桌面背景并且立即生效。以下函数实现立刻更换桌面背景:

function Set-Wallpaper
{
    param(
        [Parameter(Mandatory=$true)]
        $Path,

        [ValidateSet('Center', 'Stretch')]
        $Style = 'Stretch'
    )

    Add-Type @"
using System;
using System.Runtime.InteropServices;
using Microsoft.Win32;
namespace Wallpaper
{
public enum Style : int
{
Center, Stretch
}
public class Setter {
public const int SetDesktopWallpaper = 20;
public const int UpdateIniFile = 0x01;
public const int SendWinIniChange = 0x02;
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
private static extern int SystemParametersInfo (int uAction, int uParam, string lpvParam, int fuWinIni);
public static void SetWallpaper ( string path, Wallpaper.Style style ) {
SystemParametersInfo( SetDesktopWallpaper, 0, path, UpdateIniFile | SendWinIniChange );
RegistryKey key = Registry.CurrentUser.OpenSubKey("Control Panel\\Desktop", true);
switch( style )
{
case Style.Stretch :
key.SetValue(@"WallpaperStyle", "2") ;
key.SetValue(@"TileWallpaper", "0") ;
break;
case Style.Center :
key.SetValue(@"WallpaperStyle", "1") ;
key.SetValue(@"TileWallpaper", "0") ;
break;
}
key.Close();
}
}
}
"@

    [Wallpaper.Setter]::SetWallpaper( $Path, $Style )
}

Set-Wallpaper -Path 'C:\Windows\Web\Wallpaper\Characters\img24.jpg'