今天博客就是复习一下之前看的内容。
现在网络上有很多破解软件,一个本来要钱才能用得软件经过大佬破解之后就变得可以不用钱了。这个破解的过程肯定是非常的辛苦,一些脱壳逆向的过程就不用多说了。而这里我只是写了一个hello worid级别的程序,然后主要是介绍怎么用ReadProcessMemory函数和WriteProcessMemory函数来对一个进程的内存地址空间进行读和写的操作。
要对一个进程进行各种操作,那么最基本的肯定就是要先获得进程的句柄了。获取进程的句柄的方式有很多,可以通过CreateProcess函数,也可以通过目标进程所打开的窗口的类名和标题获得窗口的句柄,再通过窗口的句柄获得打开该窗口的进程的ID,再通过该ID获得进程的句柄。(这个好麻烦啊)还有一种办法是通过快照函数获取一个当前系统的进程的快照,然后再找出目标进程。
这上面三种方法,第一种是我们在自己的进程中用这个函数打开目标进程,然后函数会返回目标进程的句柄。也就是说那个目标进程现在变成了我们自己的进程的子进程。后面两种方法都是进程已经存在了,然后我们再去获得句柄。但是结合我们是想模拟一个破解软件的亚子。我们想的应该就是打开我们的补丁程序,然后就程序自动就帮我们打好了补丁,然后再打开目标程序。如果说进程已经存在了,那么我们再去打补丁可能就麻烦了,因为要打补丁的部分可能已经运行过去了,并且本来就一般都是先打好了再运行的嘛。而CreateProcess函数里面的参数可以指定创建进程后,进程的主线程处于挂起状态,然后这个时候我们就可以对它进行打补丁的操作了。所以我们选用CreateProcess函数来创建目标进程。
这里先给出这个Hello world级别的程序的源代码
1 .386 2 .model flat,stdcall 3 option casemap:none 4 ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 5 include windows.inc 6 include user32.inc 7 includelib user32.lib 8 include kernel32.inc 9 includelib kernel32.lib 10 ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 11 .const 12 szErr db '对不起,你使用的是盗版软件',0 13 szOK db '感谢您使用正版软件',0 14 szCaption db '谢谢',0 15 .code 16 start: 17 xor eax,eax 18 or eax,eax 19 je @F 20 invoke MessageBox,NULL,addr szOK,\ 21 addr szCaption,MB_OK 22 jmp loc_1 23 @@: 24 invoke MessageBox,NULL,addr szErr,NULL,\ 25 MB_OK or MB_ICONSTOP 26 loc_1: 27 invoke ExitProcess,NULL 28 end start
该程序将EAX清0后判断EAX是否是0,如果是0就显示“使用盗版”的窗口,不是就显示使用正版的窗口。所以这里是一定会显示盗版窗口的。然后我们要做的就是利用之前说的那两个读写进程地址空间的函数把这个进程的地址空间中的代码改一改,让它显示正版窗口。
现在要确定的就是改哪里的代码。因为那两个读写函数是要根据目标进程中要进行读写的指令的地址来使用的,所以我们还得知道代码在目标进程中的地址。
修改哪个地方的代码,这个好确定(对于这个程序来说)。我们可以直接把第19行那个跳转改为跳到下面执行正版窗口的地方,或者是直接把这一条指令去掉,也就是用nop替换。或者用其他方法也行,这里就选择用nop填充,因为这个比较简单。
那么,现在确定了修改哪一条代码,剩下的就是确定要修改的代码的地址了。所以我们将改程序反汇编一下:
这个就是上面那个程序反汇编后的样子。可以看到我们要修改的指令的地址是:00401004,并且这个指令是16位的,而nop是8位(nop的机器码就是90h),所以我们呢要填充两个nop。这里要注意一下,这个地址是线性地址,并不是程序真正所在的内存地址,每次程序执行它都是这个线性地址,所以不用担心地址会变的问题。但是如果是DLL就不一样了。因为一个程序,它可以装入很多个DLL,可能这个DLL的建议装入地址是这个,但是它装入的时候发现另一个DLL已经被装入到这了,那它就只能改变地址了。这个时候可以可以通过查看PE文件的导入表来查看DLL的真正装入位置,不过其实光查看还不行,这样只能获得DLL里面的函数的地址,还得在这个函数的附近扫描DLL的头,也是一个PE文件头,然后才能确定位置。
我们那两个函数用的就是线性地址,所以用反汇编显示的地址是没问题的。然后就介绍一下这两个函数的用法。
invoke ReadProcessMemory,hProcess,lpBaseAddress,lpBuffer,dwSize,lpNumberOfBytesRead
invoke WriteProcessMemory,hProcess,lpBaseAddress,lpBuffer,dwSize,lpNumberOfBytesRead
该函数读进程的内存。函数执行成功返回非0值,参数定义如下:
- hProcess:该参数指定目标进程的句柄。
- lpBaseAddress:在目标进程中要读写的起始线性地址(对于ReadProcessMemory)或者在目标进程中要写入的起始线程地址(对于WriteProcessMemory)。
- lpBuffer:该参数指向一个缓冲区,用来接收读取的目标进程中的数据(对于ReadProcessMemory)。指向一个缓冲区,缓冲区里面是要写入的数据(对于WriteProcessMemory)。
- dwSize:要读或写的字节数。
- lpNumberOfBytesRead:该参数指向一个双字变量,函数将返回实际读写的字节数在里面,可以指定为NULL如果不关心这个数据的话。
然后要注意的就是我们在用CreateProcess函数创建进程的时候,要指定PROCESS_VM_READ和PROCESS_VM_WRITE参数,这样我们才能对进程地址空间进行读写。
做完这些准备工作后,要写一个针对上面那个简单程序就容易多了。程序大概流程就是先用CreateProcess函数创建我们的目标进程,并且参数里面要指定dwFlags参数进程刚创建就要将主线程挂起,然后再用ReadProcessMemory函数读取数据,然后再检测一下数据是不是我们要改的那个,这个是为了防止出现程序版本不一样的情况,当然这里这个程序不会出现这种问题。最后如果没有错误,就用WriteProcessMemory函数将数据写入,然后恢复进程主线程。否则就关闭进程并显示错误信息。
下面是这个简单程序的简单补丁程序的代码:
1 .386 2 .model flat,stdcall 3 option casemap:none 4 ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 5 ;Include 6 ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 7 include windows.inc 8 include user32.inc 9 includelib user32.lib 10 include kernel32.inc 11 includelib kernel32.lib 12 ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 13 ;数据段 14 ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 15 PATCH_POSITION equ 00401004h 16 ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 17 .data? 18 dbOldBytes db 2 dup (?) ;读取的数据存放的缓冲区 19 stStartUp STARTUPINFO <?> 20 stProcInfo PROCESS_INFORMATION <?> 21 .const 22 dbPatch db 74h,15h 23 dbPatched db 90h,90h 24 szExecFilename db 'test.exe',0 25 szErrExec db '无法装载执行文件!',0 26 szErrVersion db '执行文件的版本不正确,无法修正!',0 27 ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 28 .code 29 start: 30 ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 31 ;创建进程 32 ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 33 invoke GetStartupInfo,addr stStartUp ;获取当前进程的窗口信息 34 invoke CreateProcess,offset szExecFilename,NULL,NULL,NULL,\ ;创建进程,进程的主线程一开始就为挂起状态,防止补丁程序在修改代码时被WINDOWS打断 35 NULL,NORMAL_PRIORITY_CLASS or CREATE_SUSPENDED,0,0,\ 36 offset stStartUp,offset stProcInfo 37 .if eax 38 ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 39 ;读进程内存并验证内容是否正确 40 ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 41 invoke ReadProcessMemory,stProcInfo.hProcess,\ ;读取从程序入口开始4个字节的位置,也就是跳转命令的地方 42 PATCH_POSITION,addr dbOldBytes,2,NULL 43 .if eax 44 mov ax,word ptr dbOldBytes 45 .if ax == word ptr dbPatch ;验证版本信息 46 invoke WriteProcessMemory,stProcInfo.hProcess,\ 47 PATCH_POSITION,addr dbPatched,2,NULL ;将要修改的代码写入 48 invoke ResumeThread,stProcInfo.hThread ;恢复进程中的主线程 49 .else 50 invoke TerminateProcess,stProcInfo.hProcess,-1 ;如果版本不正确则终止进程 51 invoke MessageBox,NULL,addr szErrVersion,\ 52 NULL,MB_OK or MB_ICONSTOP 53 .endif 54 .endif 55 ;********************************************************************************************************** 56 invoke CloseHandle,stProcInfo.hProcess 57 invoke CloseHandle,stProcInfo.hThread ;关闭打开的进程句柄和它的线程句柄 58 .else 59 invoke MessageBox,NULL,addr szErrExec,NULL,\ 60 MB_OK or MB_ICONSTOP 61 .endif 62 invoke ExitProcess,NULL 63 64 end start
把两个程序都编译链接然后先运行目标程序:
果不其然,显示盗版信息。然后我们再直接运行补丁程序:
OK,成功。但是要注意我们修改的是文件在内存中的数据,并没有修改文件本身的数据,所以下一次直接运行还是会出现盗版窗口。