美文网首页程序员
让代码飞出一段钢琴曲(freepiano小助手)

让代码飞出一段钢琴曲(freepiano小助手)

作者: anhkgg | 来源:发表于2017-05-22 22:29 被阅读0次

    概述

    突然想玩一下键盘弹曲子,就找到了freepiano,专业的东西不懂,就找了写简谱来玩玩,感觉挺不错的,哈哈~~

    玩疯了之后,突然想到,我平时写代码,是不是可以弹出一段曲子呢,是不是心情会变得非常好,代码也写的更有节奏呢~~

    说不定还搞出来一个什么《代码之歌》的钢琴曲~~ 嘎嘎

    突然被自己这个想法吸引住了,不管咋样,每敲下代码的一个字符,后面想起了背景音乐,真是不错的,程序员也可以是“钢琴师”啊~~

    有了想法,就开整!!!

    有下面几点问题:

    1. freepiano必须是激活窗口下,才能接受键盘输入
    2. 我要在写代码时,让freepiano响应按键,就需要全局劫持键盘输入了
    3. 怎么给freepiano通知,我按下了什么呢?

    忘了说,freepiano长这样:

    freepiano_1.png

    开搞

    先简单整理下思路:

    1. 首先肯定是弄个键盘钩子难道全局的所有键盘输入,暂定WH_KEYBOARD
    2. 怎么让钩子执行?弄个exe,把freepiano再启动起来,感觉麻烦,然后就想让freepiano加载我的模块吧,简单确认了一下,可行(后面具体描述)
    3. 劫持到键盘输入之后,通过PostMessage给freepiano发送键盘消息,模拟WM_KEYDOWN/WM_KEYUP

    1. 加载我的模块

    首先想到的就是DLL劫持和修改freepiano的导入表,后者不够优雅,果断要选择dll劫持。

    然后就用depends看了下freepiano的导入信息,发现几个可以劫持的(dsound.dll,d3d9.dll等),简单代码确认了一下,freepiano可以劫持这两个模块,选择了d3d9.dll(函数少)。

    freepiano_2.png

    然后偷懒用了aheadlib导出了d3d9.dll的导出函数信息,简单方便,飞快得就搞定了劫持。

    代码很简单,就贴一点(都不需要手写):

    // 导出函数
    #pragma comment(linker, "/EXPORT:Direct3DShaderValidatorCreate9=_AheadLib_Direct3DShaderValidatorCreate9,@1")
    #pragma comment(linker, "/EXPORT:PSGPError=_AheadLib_PSGPError,@2")
    #pragma comment(linker, "/EXPORT:PSGPSampleTexture=_AheadLib_PSGPSampleTexture,@3")
    ...
    // 导出函数
    ALCDECL AheadLib_Direct3DShaderValidatorCreate9(void)
    {
        // 保存返回地址
        __asm POP m_dwReturn[0 * TYPE long];
    
        // 调用原始函数
        GetAddress("Direct3DShaderValidatorCreate9")();
    
        // 转跳到返回地址
        __asm JMP m_dwReturn[0 * TYPE long];
    }
    

    一试,OK,模块起来了,freepiano正常工作。

    freepiano_3.png

    2. 安装钩子

    选择了安装全局WH_KEYBOARD钩子,这个代码网上也太多了,就不细说了,看看就行

    //安装钩子
    BOOL Hook(HMODULE hMod)
    {
        g_Hook = SetWindowsHookEx(WH_KEYBOARD, KeyboardProc, hMod, 0);
        return g_Hook?TRUE:FALSE;
    }
    //卸载钩子
    VOID Unhook()
    {
        if(g_Hook)
        {
            UnhookWindowsHookEx(g_Hook);
        }
    }
    //钩子函数,劫持键盘消息
    LRESULT CALLBACK KeyboardProc(  int code,       // hook code
                                  WPARAM wParam,  // virtual-key code
                                  LPARAM lParam   // keystroke-message information
                                  )
    {   
        if(code == HC_ACTION)
        {
            SendKeyMsg(wParam, lParam);
        }
        return CallNextHookEx(g_Hook, code, wParam, lParam);
    }
    

    3. 发送按键信息给freepiano

    首先想到的就是在钩子函数里给freepiano发送WM_KEYDOWN/WM_KEYUP消息就行了。

    先找到freepiano的窗口,spy++上,找到窗口标题和类型信息,然后代码:

    HWND hwnd = FindWindow("FreePianoMainWindow", "Wispow Freepiano 2");
    if(hwnd == NULL)
    {
        hwnd =  FindWindow("FreePianoMainWindow", NULL);
        if(hwnd == NULL)
        {
            hwnd =  FindWindow(NULL, "Wispow Freepiano 2");
        }
    }
    

    然后就是发消息:

    if(hwnd)
    {
        SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE|SWP_NOSIZE);
        if(keydown)
        {
            keydown = false;
            PostMessage(hwnd, WM_KEYDOWN, wParam, lParam);
        }else
        {
            keydown = true;
            PostMessage(hwnd, WM_KEYUP, wParam, lParam);
        }
    }
    

    测试,失败了,没有预想的效果。

    分析原因:

    1. Post发送消息失败。但是通过spy++抓消息看到freepiano是收到了消息的。那就不是这个原因。
    2. freepiano校验了窗口是否激活?然后就用上面每次置顶试了一下,依然不行。
    3. freepiano使用了GetKeyState之类的函数检查按键状态,通过ida简单看了一下导入表,没有相关函数(没有深究是否显示导入了)。
    4. 用ida看了下freepiano的窗口消息处理,看是否有什么过滤
    v7.lpfnWndProc = (WNDPROC)xxx_main_wndproc_41D070; //窗口响应函数
    v7.cbClsExtra = 0;
    v7.cbWndExtra = 0;
    v7.hInstance = GetModuleHandleA(0);
    v7.hIcon = LoadIconA(v1, (LPCSTR)0xA);
    v7.hCursor = 0;
    v7.hbrBackground = 0;
    v7.lpszMenuName = 0;
    v7.lpszClassName = "FreePianoMainWindow";
    

    然后发现居然没有对WM_KEYDOWN/WM_KEYUP/WM_CHAR之类的消息进行处理,那是怎么接受的按键信息

    继续用ida看是否有钩子之类的处理,果然,导入表中明晃晃的SetWindowsHookEx,进入一看,一个WM_KEYBOARD_LL局部钩子

    freepiano_4.png

    进钩子函数一下,各种按键状态记录的处理,不深究了。基本确认他使用这种方式来接受按键信息。

     v6 = (unsigned __int8)byte_4F6DC8[scanCode];
      if ( (unsigned int)(v6 - 1) > 0x6A )
        goto LABEL_23;
      if ( (unsigned __int8)byte_4F6ED0[v6] != pressed_0 )
      {
        byte_4F6ED0[v6] = pressed_0;
        sub_449B20(v6, pressed_0 != 0);
      }
      if ( (_BYTE)dword_4F6DC0
        || BYTE1(dword_4F6DC0) && (v6 == 'D' || v6 == 'H')
        || BYTE2(dword_4F6DC0) && (byte_4F6F15 || byte_4F6F17) && v6 == 28 )
        result = 1;
      else
    

    4. 改变策略

    那就不能直接PostMessage发送消息了。

    修改我的钩子为WM_KEYBOARD_LL全局键盘钩子,消息和freepiano完全一样了

    g_Hook = SetWindowsHookEx(WH_KEYBOARD_LL, LowLevelKeyboardProc, hMod, 0);
    
    

    钩子函数通过WM_COPYDATA打包数据,发送给freepiano

    LRESULT CALLBACK LowLevelKeyboardProc(  int nCode,     // hook code
                                          WPARAM wParam, // message identifier  
                                          LPARAM lParam  // message data
                                          )
    {
        COPYDATASTRUCT CopyData = {0};
        KeyboardLL_Msg Msg = {0};
    
        Msg.nCode = nCode;
        Msg.wParam = wParam;
        memcpy(&Msg.lParam, (char*)lParam, sizeof(KBDLLHOOKSTRUCT));
    
        CopyData.cbData = sizeof(KeyboardLL_Msg);
        CopyData.dwData = 0;
        CopyData.lpData = &Msg;
    
        HWND hwnd = FindFreepiano();
        if(hwnd)
        {
            BOOL ret = SendMessage(hwnd, WM_COPYDATA, (WPARAM)hwnd, (LPARAM)&CopyData);
        }
        return CallNextHookEx(g_Hook, nCode, wParam, lParam);
    }
    typedef struct _KeyboardLL_Msg
    {
        int nCode;
        WPARAM wParam;
        KBDLLHOOKSTRUCT lParam;
    }KeyboardLL_Msg, *PKeyboardLL_Msg;
    

    通过SetWindowLong挂钩freepiano的窗口响应函数,增加处理WM_COPYDATA,来接受全局键盘信息,找到freepiano的钩子函数地址A,然后接受到WM_COPYDATA之后,直接调用A,把键盘信息给freepiano

    通过一个线程,循环查找freepianp窗口(可能还没起来),然后hook窗口响应函数

    void HookWinProc()
    {
        while(1)
        {
            HWND hwnd = FindFreepiano();
            if(hwnd)
            {
                g_WndProc = (pfn_WindProc)GetWindowLong(hwnd, GWL_WNDPROC);
                if(g_WndProc)
                {
                    SetWindowLong(hwnd, GWL_WNDPROC, (LONG)fakeWindowProc);
                    break;
                }
            }
            Sleep(10);
        }
    }
    

    自己的函数中加入对WM_COPYDATA的消息处理,调用freepiano的钩子函数g_LowLevelKeyboardProc发键盘消息过去。

    
    LRESULT WINAPI fakeWindowProc(
                           HWND hWnd,              // handle to window
                           UINT Msg,               // message
                           WPARAM wParam,          // first message parameter
                           LPARAM lParam           // second message parameter
                           )
    {
        if(Msg == WM_COPYDATA)
        {
            COPYDATASTRUCT* CopyData = (COPYDATASTRUCT*)lParam; 
            //if(CopyData->cbData == sizeof(KeyboardLL_Msg))
            {
                KeyboardLL_Msg* Msg = (KeyboardLL_Msg*)CopyData->lpData;
                g_LowLevelKeyboardProc(Msg->nCode, Msg->wParam, (LPARAM)&Msg->lParam);
            }
        }
        
        return g_WndProc(hWnd, Msg, wParam, lParam);
    }
    

    g_LowLevelKeyboardProc地址这里使用硬编码,图方便

    HMODULE hExe = GetModuleHandle(NULL);
    g_LowLevelKeyboardProc = (pfn_LowLevelKeyboardProc)((DWORD)hExe + (DWORD)g_LowLevelKeyboardProc);
    

    功能到这里基本搞定。

    总结

    测试通过。

    手指立马不受控制的在编辑器里、浏览器、文件浏览器里各种按键,然后耳边响起了悠扬(忽略乱打的节奏的话)的钢琴声~~

    可能的优化:

    1. 加入进程名单控制,不想在某些进程中听到琴声
    2. 代码优化~~

    有兴趣的同学可以去折腾,我这里就不继续了~~

    转载请注明出处:http://anhkgg.github.io/coding-piano-hook

    相关文章

      网友评论

        本文标题:让代码飞出一段钢琴曲(freepiano小助手)

        本文链接:https://www.haomeiwen.com/subject/jcgixxtx.html