以前玩游戏遇到一些实在过不去的管卡,经常会找一些游戏修改软件来修改游戏,让自己变得无比强大,将boss一路砍瓜切菜过足游戏瘾。其实游戏修改软件的功能大多都比较简单,我们可以通过windbg的一些简单命令同样也可以实现。
在这片文章中我们会通过一个简单的winform登录程序来演示将一系列简单调试命令联合运用,破解登录程序。
测试程序
登录程序界面
登录验证代码
private void btnLogin_Click(object sender, EventArgs e) { if (txtUser.Text == "test" && txtPassword.Text == "abc123") { MessageBox.Show("login successfully"); } else { MessageBox.Show("login failed"); } }
调试步骤
运行程序,将windbg attach到登录程序。输入用户名和密码(这里我们让用户名比较特殊,这样方便我们在内存中查找,密码随便输入,我们装作不知道)。
在windbg中通过内存查找命令搜索我们输入的用户名,
这里解释下这个查找命令,s是search memory命令符,-u代表搜索的目标类型为unicode,另外还支持-a ascii,-d dword等。0x00000000 L?0x7fffffff为搜索范围参数,这里我们的目标空间为2G所有的user mode内存空间。
0:005> s-u 0x00000000 L?0x7fffffff mockuser
005f0178 006d 006f 0063 006b 0075 0073 0065 0072 m.o.c.k.u.s.e.r.
运气很好,只有一个匹配,那就是这个地址了。接下来我们要找到验证用户名和密码的方法,这个如何找到呢?
靠猜!我猜这个方法调用过程中会访问我们找到的用户名地址从而读取用户名。我们来试一下,我在这个地址上设置一个访问断点,
ba是访问断点设置命令(break on access),r4代表读取前4位的时候触发断点。
0:005> ba r4 005f0178 0:005> bl 0 e 005f0178 r 4 0001 (0001) 0:****
点login按钮,果然断点被触发了,接下来我们来看一下调用栈,这里看到与程序自定义代码相关的方法就是这个btnLogin_Click事件了。如果你调试的程序为非托管的话,那这个部分需要很下翻功夫,因为很可能看不到程序方法名。
Breakpoint 0 hit eax=006f006d ebx=00611050 ecx=00000004 edx=00000000 esi=005f0178 edi=0044ed1c eip=75c7980a esp=0044e90c ebp=0044e914 iopl=0 nv up ei ng nz ac pe cy cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000297 msvcrt!UnwindUpVec+0x3c: 75c7980a 89448ff0 mov dword ptr [edi+ecx*4-10h],eax ds:002b:0044ed1c=00004c00 0:000> kL ChildEBP RetAddr 0044e914 725f9f09 msvcrt!UnwindUpVec+0x3c 0044e934 725f9af7 comctl32_725f0000!Edit_GetTextHandler+0x3a ... 0044edec 63e422a5 System_Windows_Forms_ni!System.Windows.Forms.Control.get_WindowText()+0xac 0044edfc 63e3316b System_Windows_Forms_ni!System.Windows.Forms.TextBoxBase.get_WindowText()+0x5 0044edfc 63e422b5 System_Windows_Forms_ni!System.Windows.Forms.Control.get_Text()+0x1b 0044ee38 00210845 System_Windows_Forms_ni!System.Windows.Forms.TextBox.get_Text()+0x5 0044ee38 63ddd522 MemoryEdit!MemoryEdit.SampleLoginForm.btnLogin_Click(System.Object, System.EventArgs)+0x45 0044ee50 63ddf920 System_Windows_Forms_ni!System.Windows.Forms.Control.OnClick(System.EventArgs)+0x62 0044ee60 643a3f58 System_Windows_Forms_ni!System.Windows.Forms.Button.OnClick(System.EventArgs)+0x80 0044ee7c 6437b7ec System_Windows_Forms_ni!System.Windows.Forms.Button.OnMouseUp(System.Windows.Forms.MouseEventArgs)+0xac 0044eed4 646e9a72 System_Windows_Forms_ni!System.Windows.Forms.Control.WmMouseUp(System.Windows.Forms.Message ByRef, System.Windows.Forms.MouseButtons, Int32)+0x274 0044ef28 646f0821 System_Windows_Forms_ni!System.Windows.Forms.Control.WndProc(System.Windows.Forms.Message ByRef)+0x8ba812 ...
0044f27c 00210093 System_Windows_Forms_ni!System.Windows.Forms.Application.Run(System.Windows.Forms.Form)+0x31 0044f288 68842952 MemoryEdit!MemoryEdit.Program.Main()+0x43 ...
这里我们需要反编译btnLogin_Click的代码,这个方法接下来调用了TextBox.get_Text方法,那get_Text的返回地址00210845就在btnLogin_Click中,所以我们可以方编译这个地址来查看login_click方法。
我们可以看到这个方法逻辑比较简单,其中调用了两个String.op_Equality方法来比较字符串,那我们要做的就是让它的比较的结果为True。
在比较之前有两个汇编操作,分别将两个地址mov到了edx和ecx,猜测应该是op_Equality的两个字符串参数。
0:000> uf 00210845 MemoryEdit!MemoryEdit.SampleLoginForm.btnLogin_Click(System.Object, System.EventArgs)+0x45 : 22 00210845 8945d4 mov dword ptr [ebp-2Ch],eax 22 00210848 8b1560217f03 mov edx,dword ptr ds:[37F2160h] 22 0021084e 8b4dd4 mov ecx,dword ptr [ebp-2Ch] 22 00210851 e89a228566 call mscorlib_ni!System.String.op_Equality(System.String, System.String) (66a62af0) 22 00210856 8945ec mov dword ptr [ebp-14h],eax 22 00210859 837dec00 cmp dword ptr [ebp-14h],0 22 0021085d 7435 je MemoryEdit!MemoryEdit.SampleLoginForm.btnLogin_Click(System.Object, System.EventArgs)+0x94 (00210894) MemoryEdit!MemoryEdit.SampleLoginForm.btnLogin_Click(System.Object, System.EventArgs)+0x5f : 22 0021085f 8b45d8 mov eax,dword ptr [ebp-28h] 22 00210862 8b8848010000 mov ecx,dword ptr [eax+148h] 22 00210868 8b01 mov eax,dword ptr [ecx] 22 0021086a 8b404c mov eax,dword ptr [eax+4Ch] 22 0021086d ff501c call dword ptr [eax+1Ch] 22 00210870 8945d0 mov dword ptr [ebp-30h],eax 22 00210873 8b1564217f03 mov edx,dword ptr ds:[37F2164h] 22 00210879 8b4dd0 mov ecx,dword ptr [ebp-30h] 22 0021087c e86f228566 call mscorlib_ni!System.String.op_Equality(System.String, System.String) (66a62af0) 22 00210881 8945dc mov dword ptr [ebp-24h],eax 22 00210884 90 nop 22 00210885 837ddc00 cmp dword ptr [ebp-24h],0 22 00210889 0f94c0 sete al 22 0021088c 0fb6c0 movzx eax,al 22 0021088f 8945e8 mov dword ptr [ebp-18h],eax 22 00210892 eb07 jmp MemoryEdit!MemoryEdit.SampleLoginForm.btnLogin_Click(System.Object, System.EventArgs)+0x9b (0021089b) MemoryEdit!MemoryEdit.SampleLoginForm.btnLogin_Click(System.Object, System.EventArgs)+0x94 : 22 00210894 c745e801000000 mov dword ptr [ebp-18h],1 ... MemoryEdit!MemoryEdit.SampleLoginForm.btnLogin_Click(System.Object, System.EventArgs)+0xce : 30 002108ce 90 nop 30 002108cf 8d65f8 lea esp,[ebp-8] 30 002108d2 5e pop esi 30 002108d3 5f pop edi 30 002108d4 5d pop ebp 30 002108d5 c20400 ret 4
我们在op_Equality方法上设置断点。然后继续运行程序,断点触发。
为了验证我们的猜想,我们来通过sos managed debug extension来验证下我们的猜想,果然ecx,edx中存的是我们输入的用户名和校验方法中比对的用户名。到这里其实我们已经知道登录代码要比对的用户名是test。不过这次为了演示,要更改内存让本次登录成功。
0:000> bp 00210851 0:000> bp 0021087c 0:000> bl 0 e 005f0178 r 4 0001 (0001) 0:**** 1 e 00210851 0001 (0001) 0:**** MemoryEdit!MemoryEdit.SampleLoginForm.btnLogin_Click(System.Object, System.EventArgs)+0x51 2 e 0021087c 0001 (0001) 0:**** MemoryEdit!MemoryEdit.SampleLoginForm.btnLogin_Click(System.Object, System.EventArgs)+0x7c 0:000> g Breakpoint 1 hit eax=0283140c ebx=02818468 ecx=0283140c edx=028312f4 esi=027f36ec edi=0044ee28 eip=00210851 esp=0044ee08 ebp=0044ee38 iopl=0 nv up ei pl zr na pe nc cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246 MemoryEdit!MemoryEdit.SampleLoginForm.btnLogin_Click(System.Object, System.EventArgs)+0x51: 00210851 e89a228566 call mscorlib_ni!System.String.op_Equality(System.String, System.String) (66a62af0) 0:000> .loadby sos clr 0:000> !do 0283140c Name: System.String MethodTable: 66b8acc0 EEClass: 6679486c Size: 30(0x1e) bytes File: C:WindowsMicrosoft.NetassemblyGAC_32mscorlibv4.0_4.0.0.0__b77a5c561934e089mscorlib.dll String: mockuser Fields: MT Field Offset Type VT Attr Value Name 66b8c480 40000aa 4 System.Int32 1 instance 8 m_stringLength 66b8b6b8 40000ab 8 System.Char 1 instance 6d m_firstChar 66b8acc0 40000ac c System.String 0 shared static Empty >> Domain:Value 0057ae08:NotInit << 0:000> !do 028312f4 Name: System.String MethodTable: 66b8acc0 EEClass: 6679486c Size: 22(0x16) bytes File: C:WindowsMicrosoft.NetassemblyGAC_32mscorlibv4.0_4.0.0.0__b77a5c561934e089mscorlib.dll String: test Fields: MT Field Offset Type VT Attr Value Name 66b8c480 40000aa 4 System.Int32 1 instance 4 m_stringLength 66b8b6b8 40000ab 8 System.Char 1 instance 74 m_firstChar 66b8acc0 40000ac c System.String 0 shared static Empty >> Domain:Value 0057ae08:NotInit <<
这里我们通过内存查看命令来看一下这两个字符串的数据结构是如何在呈现的。
可以看到黄色的为string类型的方法表(method table),绿色的为该字符串的长度,再接下来的内容为字符串内容。
我们要做的是要将字符串大小从8改为4,然后将字符串内容替换为test。
0:000> dc 0283140c 0283140c 66b8acc0 00000008 006f006d 006b0063 ...f....m.o.c.k. 0283141c 00730075 00720065 00000000 00000000 u.s.e.r......... 0:000> dc 028312f4 028312f4 66b8acc0 00000004 00650074 00740073 ...f....t.e.s.t. 02831304 00000000 80000000 66b8acc0 00000006 ...........f....
通过内存更改命令eb和eu来完成。这里我们看到更改之后字符串貌似变成了testuser,没关系,因为我们更改了字符串长度,所以程序只会比较前四个字符。
0:000> eb 0283140c+4 4 0:000> eu 0283140c+8 "test" 0:000> dc 0283140c 0283140c 66b8acc0 00000004 00650074 00740073 ...f....t.e.s.t. 0283141c 00730075 00720065 00000000 00000000 u.s.e.r.........
接下来我们要运行了,如果运气好我们可以运行到下一个密码比较的断点。
当然我们运气一项不错,第二个断点如约触发了。还是做同样的事情,将需要比较的密码改掉。
0:000> g Breakpoint 2 hit eax=028314a4 ebx=02818468 ecx=028314a4 edx=0283130c esi=027f36ec edi=0044ee28 eip=0021087c esp=0044ee08 ebp=0044ee38 iopl=0 nv up ei pl zr na pe nc cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246 MemoryEdit!MemoryEdit.SampleLoginForm.btnLogin_Click(System.Object, System.EventArgs)+0x7c: 0021087c e86f228566 call mscorlib_ni!System.String.op_Equality(System.String, System.String) (66a62af0) 0:000> !do 028314a4 Name: System.String MethodTable: 66b8acc0 EEClass: 6679486c Size: 26(0x1a) bytes File: C:WindowsMicrosoft.NetassemblyGAC_32mscorlibv4.0_4.0.0.0__b77a5c561934e089mscorlib.dll String: 123456 Fields: MT Field Offset Type VT Attr Value Name 66b8c480 40000aa 4 System.Int32 1 instance 6 m_stringLength 66b8b6b8 40000ab 8 System.Char 1 instance 31 m_firstChar 66b8acc0 40000ac c System.String 0 shared static Empty >> Domain:Value 0057ae08:NotInit << 0:000> !do 0283130c Name: System.String MethodTable: 66b8acc0 EEClass: 6679486c Size: 26(0x1a) bytes File: C:WindowsMicrosoft.NetassemblyGAC_32mscorlibv4.0_4.0.0.0__b77a5c561934e089mscorlib.dll String: abc123 Fields: MT Field Offset Type VT Attr Value Name 66b8c480 40000aa 4 System.Int32 1 instance 6 m_stringLength 66b8b6b8 40000ab 8 System.Char 1 instance 61 m_firstChar 66b8acc0 40000ac c System.String 0 shared static Empty >> Domain:Value 0057ae08:NotInit <<
0:000> eu 028314a4+8 "abc123"
0:000> dc 028314a4
028314a4 66b8acc0 00000006 00620061 00310063 ...f....a.b.c.1.
028314b4 00330032 00000000 00000000 00000000 2.3.............
看看最终的运行结果吧,虽然用户名和密码都不正确,但还是越狱成功了。
总结
本文使用的都是最基础的调试命令,但也是最重要的命令。
- 内存搜索: s -a/-u/-d
- 访问断点: ba r/w
- 查看调用栈: kv/kL/kp
- 查看内存: dc/du/da/dd
- 修改内存: ed/eu/ea
- 反编译代码: uf/ub
在真实的调试中情况不会这么简单,需要耐心分析,不断尝试。
可以粗略了解些汇编语言,明白常用命令的作用,寄存器的用途,和方法调用规范(calling convention x86 x64)。
发挥想象力,想象力比知识更重要。
I am enough of an artist to draw freely upon my imagination. Imagination is more important than knowledge. Knowledge is limited. Imagination encircles the world.
Albert Einstein
Aaron Zhang