基于NPAPI接口的windows平台安全控件开发(一)

    由于最近公司在做支付,所以需要开发安全控件。可能就因为有“安全”两个字,所以这个控件的开发任务就到我这个做信息安全的人身上了,,,,对于没写过安全控件的我来说,自然是各种查资料,找博客,期望能够找到现成的代码(懒。。。),不过,可能是因为这个比较小众,连有点干货的文章都没找到几篇,更不用说找到源代码了。所以,只能自食其力,自己折腾。

    这次安全控件主要分为两个版本:IE内核的和支持NPAPI借口的。IE一直表现的比较个性啊,就玩自己的一套,导致前端人员比较苦逼。结合各种博客,书籍等资料,经过一段时间的开发,算是把两个版本的空间都写好了,这系列的博客就当一个开发记录吧。由于也是第一次开发这种空间,水平有限,大神可以直接PASS。

    首先是基于NPAPI接口的安全控件。当然了,这个接口很快就要被抛弃了,chrome和firefox都宣布要逐步放弃该接口。不管以后是HTML5,PPAPI还是什么高达上的新技术出来,至少目前还是要继续使用这个接口。

    安全控件目前主流的功能大体是:1.加密密码。2.通过hook技术防止键盘被窃听。3.其他搜集信息功能。(个人见解,大神有不同意见,请指教),在开发的角度来说,控件主要包含一个图形化的输入框以及对外提供的各种借口。NPAPI体系我就不介绍了,网上也有一些介绍的文章(我也介绍不来,,,),以下是基于VS2003平台开发的插件介绍(用2003主要是因为不需要额外安装运行库,用08,10什么的也行的)

    首先,需要下载一个SDK文件,里面包含一些必要的头文件,库文件等。目录如下:

    基于NPAPI接口的windows平台安全控件开发(一)

随后,就可以建立一个Win32项目,应用程序类型选择DLL,附加选项选择“空项目”,项目应以np开头。

基于NPAPI接口的windows平台安全控件开发(一)

建立项目后,首先建立一个模块定义文件(def)。def文件为链接器提供dll的导出,属性等信息。不过此处的def可以使固定写法,如下:

LIBRARY npxxxEdit
EXPORTS
NP_GetEntryPoints @1
NP_Initialize @2
NP_Shutdown @3

NP_GetEntryPoints(),NP_Initialize(),NP_Shutdown()属于入口函数。浏览器启动时,会先调用NP_GetEntryPoints()获得NPPluginFuncs结构体对象,随后构造NPNetscapesFuncs结构体,并调用NP_Initialize()函数进行初始化,随后浏览器通过NPP_New创建插件实例,浏览器再通过NP_GetValue获取插件相关信息。NP_Shutdown是关闭浏览器时进行调用。写到这里,就再介绍一下NPAPI接口的一些函数命名规则,以NP开头的函数式NPAPI插件库提供给浏览器的上层接口。以NPP开头的函数是插件提供给浏览器调用的接口,主要用来填充NPPluginsFuncs结构体。以NPN开头的函数是浏览器提供给插件条用的接口,主要保存在NPNetscapesFuncs结构体中。

    建立完def文件之后,就需要添加资源文件,选择添加-->资源,新建version。建立完成后,在工程目录中就会存在一个.rc文件,以代码的方式打开该文件(VS2003中选择打开方式,源代码(文本)编辑器),随后找到以下代码:

BEGIN
    BLOCK "StringFileInfo"
    BEGIN
        BLOCK "040904e4"
        BEGIN
            VALUE "CompanyName", "XXX Inc."
            VALUE "FileDescription", "xxx security control"
            VALUE "FileVersion", "1, 0, 0, 3"
            VALUE "InternalName", "npxxxEdit"
            VALUE "LegalCopyright", "版权所有 (C) 2015"
            VALUE "MIMEType", "application/xxxEdit-plugin"
            VALUE "OriginalFilename", "npxxxEdit.dll"
            VALUE "ProductName", "xxx Security Control 1.0"
            VALUE "ProductVersion", "1, 0, 0, 3"
        END
    END
    BLOCK "VarFileInfo"
    BEGIN
        VALUE "Translation", 0x409, 1252
    END
END

新建的rc文件内容不是上面显示的这样,需要将其修改如上述代码所示。主要修改

BLOCK "040904e4"
 BEGIN
        VALUE "Translation", 0x409, 1252
    END

409表示为英文,默认804表示问中文,据其他博客中介绍,chrome是两种都可以支持,但是firefox只支持英文,故需要将中文改成英文,这样就做到两种浏览器均支持(我没试验过)。

还有就是需要添加一个字段:

VALUE "MIMEType", "application/xxxEdit-plugin"

此字段主要是指定MIMEType,后续在浏览器中调用插件的时候需要该值。其他各字段看命名就知道代表何种含义了,可以自行修改。随后我们在rc文件中添加一个Dialog,并且设置一个Edit Control。设置Dialog属性,Border设置为None,Style设置为Child,Visible设置为true。设置Edit控件属性,将Password设置为Ture。设置完成后,就是如下的一个输入框:

基于NPAPI接口的windows平台安全控件开发(一)

    建立完资源文件后,需要先设置一下项目属性,在C/C++-->常规-->附加包含目录中将SDK中的base\public和sdk\samples\include目录添加到项目中。并在预处理器中添加_X86_

基于NPAPI接口的windows平台安全控件开发(一)

基于NPAPI接口的windows平台安全控件开发(一)

    随后,将sdk\samples\common中的三个cpp文件添加到项目中,分别为np_entry.cpp,npn_gate.cpp,npp_gate.cpp。这三个文件预定义了一些必要的函数,实际项目中,可以根据需要,对着文件中的函数进行修改。

    以上这些辅助动作做完之后,我们就可以正式开始建立插件类了,我们定义类名为CPlugin,CPlugin需要继承nsPluginInstanceBase类,并且实现三个纯虚函数init(),shut(),isInitialized()。其中的CYKedit随后我们再介绍。

CPlugin类的定义如下:

class CPlugin :
public nsPluginInstanceBase
{
private:
    NPP m_pNPInstance;
    NPBool m_bInitialized;
    NPWindow *m_Window;
    HWND m_hWnd;
    NPStream *m_pNPStream;
    NPObject *m_pScriptableObject;
    NPObject *m_jsobj;
public:
    CYKedit *myedit;
    int m_Width,m_Height;
public:
    CPlugin(NPP pNPInstance);
    ~CPlugin(void);
    //重载父类虚函数
    NPBool init(NPWindow* pNPWindow);
    void shut();
    NPBool isInitialized();
    int16_t handleEvent(void* event);
    NPObject *GetScriptableObject();
};

我也把pluginbase.h中的nsPluginInstanceBase类的定义贴出来

class nsPluginInstanceBase
{
public:
  // these three methods must be implemented in the derived
  // class platform specific way
  virtual NPBool init(NPWindow* aWindow) = 0;
  virtual void shut() = 0;
  virtual NPBool isInitialized() = 0;
  // implement all or part of those methods in the derived 
  // class as needed
  virtual NPError SetWindow(NPWindow* pNPWindow)                    { return NPERR_NO_ERROR; }
  virtual NPError NewStream(NPMIMEType type, NPStream* stream, 
                            NPBool seekable, uint16_t* stype)       { return NPERR_NO_ERROR; }
  virtual NPError DestroyStream(NPStream *stream, NPError reason)   { return NPERR_NO_ERROR; }
  virtual void    StreamAsFile(NPStream* stream, const char* fname) { return; }
  virtual int32_t WriteReady(NPStream *stream)                      { return 0x0fffffff; }
  virtual int32_t Write(NPStream *stream, int32_t offset, 
                        int32_t len, void *buffer)                  { return len; }
  virtual void    Print(NPPrint* printInfo)                         { return; }
  virtual uint16_t HandleEvent(void* event)                         { return 0; }
  virtual void    URLNotify(const char* url, NPReason reason, 
                            void* notifyData)                       { return; }
  virtual NPError GetValue(NPPVariable variable, void *value)       { return NPERR_NO_ERROR; }
  virtual NPError SetValue(NPNVariable variable, void *value)       { return NPERR_NO_ERROR; }
};

当然也可以覆盖父类中的其他函数,例如GetValue函数。看到这里有人可能要问why?为什么需要新建一个类CPlugin,继承nsPluginInstanceBase,并且覆盖其中的一些函数?第一次看到这里的时候我也是云里雾里,不知所云。其实理由很简单,打开前面添加到项目中的npp_gate.cpp文件(有点长,就不贴了),可以看到该文件时包含pluginbase.h这个头文件的。可以随便选一个函数NPP_GetValue函数,定义如下:

NPErrorNPP_GetValue(NPP instance, NPPVariable variable, void *value)
{
  if (!instance)
    return NPERR_INVALID_INSTANCE_ERROR;
  nsPluginInstanceBase * plugin = (nsPluginInstanceBase *)instance->pdata;
  if (!plugin) 
    return NPERR_GENERIC_ERROR;
  return plugin->GetValue(variable, value);
}

可以看到函数中通过plugin指针指向nsPluginInstanceBase类实例,并且随后会调用nsPluginInstanceBase类的GetValue函数。至此可以大致理清函数间的调用关系,首先NPP开头的函数是浏览器调用插件的接口,当浏览器调用NPP_GetValue时,实际就会调用到我们建立的CPlugin类中的GetValue函数(其他类推),这也是创建CPlugin类的原因,通过CPlugin类可以实现符合我们要求的功能。当然,也可以通过修改npp_gate.cpp中的函数,从而修改上述流程。例如修改NPP_GetValue,让其调用CPlugin中的GetScriptableObject函数而不是GetValue函数,实际实现的时候我也是这么做的。

    通过上面的分析,可以知道CPlugin存在的目的和价值。定义好了CPlugin后,我们就需要实现类中的函数,但在实现前需要先实现一下四个函数,分别为NS_PluginInitialize、NP_PluginShutdown、NP_NewPluginInstance、NS_DestroyPluginInstance。四个函数的用途分别是:在NP_Initialize中调用函数fillNetscapeFunctionTable,根据参数给NPNetscapeFuncs对象进行复制,并调用NS_PluginInitialize。在NPP_New中调用NS_NewPluginInstance生成CPlugin对象实例。在NPP_Destroy中调用CPlugin中的shut函数并调用NS_DestroyPluginInstance来释放资源。在NP_Shutdown中调用NS_PluginShutdown(有点绕,,)。四个函数的实现如下:

NPError NS_PluginInitialize()  
{  
    return NPERR_NO_ERROR;  
}  
void NS_PluginShutdown()  
{  
}  
nsPluginInstanceBase * NS_NewPluginInstance(nsPluginCreateData * aCreateDataStruct)  
{  
    if(!aCreateDataStruct)  
        return NULL;  
    CPlugin * plugin = new CPlugin(aCreateDataStruct->instance);  
    /*
    //创建winless插件
    BOOL bWindowed = FALSE;
    NPN_SetValue(aCreateDataStruct->instance,NPPVpluginWindowBool,(void *)bWindowed);//winless插件在    此指出,默认为Windowed
    */
    return plugin;  
}  
void NS_DestroyPluginInstance(nsPluginInstanceBase * aPlugin)  
{  
if(aPlugin){  
    delete (CPlugin *)aPlugin; 
} 
}

以上只是默认调用流程,实际实现时,我们也可以通过修改相应函数去改变函数调用流程。

    随后我们可以实现CPlugin类,init,shut,isInitialized三个函数实现如下:

void CPlugin::shut()
{
    SubclassWindow(m_hWnd, lpOldProc);
    m_hWnd = NULL;
    myedit = NULL;
    m_bInitialized = FALSE;
}
NPBool CPlugin::isInitialized()
{
    return m_bInitialized;
}
NPBool CPlugin::init(NPWindow* pNPWindow)
{
    if(pNPWindow == NULL){
    return false;
    }
    m_hWnd = (HWND)pNPWindow->window;
    if(m_hWnd == NULL){
    return false;
    }
    lpOldProc = SubclassWindow(m_hWnd,(WNDPROC)PluginWinProc);
    SetWindowLongPtr(m_hWnd, GWLP_USERDATA, (LONG_PTR)this);
    m_Window = pNPWindow;
    myedit = new CYKedit();
    myedit->Create(GetModuleHandle("npYoukuEdit.dll"),MAKEINTRESOURCE(IDD_DIALOG1),m_hWnd);
    myedit->ShowDlg();
    SetWindowPos(myedit->hWnd,HWND_TOPMOST,0,0,m_Width,m_Height,SWP_NOZORDER|SWP_NOMOVE);
    m_bInitialized = TRUE;
    return TRUE;
}

init函数主要是实例化CYKedit类,并且将前面资源文件的图形界面创建出来。至此的话,应该是可以将密码输入框给显示出来。CYKedit类中主要是实现了前面提到的加密,hook保护等功能。另外还有实现与JS交互的功能,插件如何安装等功能,鉴于每篇博客有字数限制,都在后续博客再写了。写的有点乱。。。。后面有时间再好好整理了

  

你可能感兴趣的:(NPAPI,安全控件)