如何在安全代码中将参数推入没有参数的方法中

你好 这次我们继续嘲笑常规方法调用。 我建议不带参数地熟悉带参数的方法调用。 我们还将尝试将引用类型转换为数字-其地址,而不使用指针和不安全的代码


免责声明


在继续讲故事之前,我强烈建议您阅读有关StructLayout的上一篇文章 。 在这里,我将使用其中描述的一些功能。

我也要警告,本文不包含在实际项目中应使用的材料。

一些初步信息


在开始练习之前,让我们记住如何将C#代码转换为汇编代码。
让我们来看一个简单的例子。

public class Helper { public virtual void Foo(int param) { } } public class Program { public void Main() { Helper helper = new Helper(); var param = 5; helper.Foo(param); } } 

该代码没有任何困难,但是JiT生成的指令包含几个关键点。 我建议仅查看生成的代码的一小部分。 在我的示例中,我将对32位计算机使用汇编代码。

 1: mov dword [ebp-0x8], 0x5 2: mov ecx, [ebp-0xc] 3: mov edx, [ebp-0x8] 4: mov eax, [ecx] 5: mov eax, [eax+0x28] 6: call dword [eax+0x10] 

在这个小示例中,您可以观察到快速调用的调用约定,该约定使用寄存器传递参数(在ecx和edx寄存器中,前两个参数从左到右),其余参数从右到左通过堆栈。 第一个(隐式)参数是在其上调用该方法的类的实例的地址(对于非静态方法)。

在我们的例子中,第一个参数是实例的地址,第二个参数是我们的int值。

因此,在第一行中我们看到了局部变量5,在这里没有什么有趣的。
第二行中,我们将Helper实例的地址复制到ecx寄存器中。 这是指向方法表的指针的地址。
第三行将局部变量5复制到edx寄存器中
第四行中,我们可以看到将方法表地址复制到eax寄存器中
第五行包含从内存中加载比方法表地址大40个字节的地址中的值:方法表中方法地址的开始。 (方法表包含以前存储的各种信息。例如,基类方法表的地址,EEClass地址,各种标志,包括垃圾收集器标志,等等)。 因此,方法表中第一个方法的地址现在存储在eax寄存器中。
注意:在.NET Core中,方法表的布局已更改。 现在有一个字段(分别为32和64位系统的32/64位偏移),它包含方法列表开始的地址。
第六行中,该方法从起始位置偏移16,即方法表中的第五个位置处调用。 为什么我们唯一的方法是第五种? 我提醒您, 对象具有4个虚拟方法( ToString(),Equals(),GetHashCode()和Finalize() ),所有类都将分别具有。

转到练习;


主动:
现在该开始一个小型演示了。 我建议使用这么小的空白(非常类似于上一篇文章中的空白)。

  [StructLayout(LayoutKind.Explicit)] public class CustomStructWithLayout { [FieldOffset(0)] public Test1 Test1; [FieldOffset(0)] public Test2 Test2; } public class Test1 { public virtual int Useless(int param) { Console.WriteLine(param); return param; } } public class Test2 { public virtual int Useless() { return 888; } } public class Stub { public void Foo(int stub) { } } 

让我们以这种方式使用这些东西:

  class Program { static void Main(string[] args) { Test2 fake = new CustomStructWithLayout { Test2 = new Test2(), Test1 = new Test1() }.Test2; Stub bar = new Stub(); int param = 55555; bar.Foo(param); fake.Useless(); Console.Read(); } } 

您可能会猜到,根据上一篇文章的经验,将调用类型为Test1Useless(int j)方法。

但是会显示什么? 我相信,细心的读者已经回答了这个问题。 控制台上显示“ 55555”。

但是,让我们仍然看看生成的代码片段。

  mov ecx, [ebp-0x20] mov edx, [ebp-0x10] cmp [ecx], ecx call Stub.Foo(Int32) mov ecx, [ebp-0x1c] mov eax, [ecx] mov eax, [eax+0x28] call dword [eax+0x10] 

我认为您认识到虚拟方法调用模式,它在Stub.Foo(Int32)调用之后开始。 如我们所见,正如预期的那样,ecx填充了在其上调用该方法的实例的地址。 但是由于编译器认为我们调用了没有参数的Test2类型的方法,因此不会将任何内容写入edx。 但是,我们之前还有另一个方法调用。 在那里,我们使用edx传递参数。 当然,我们没有说明,这是清晰的edx。 因此,如您在控制台输出中所见,使用了先前的edx值。

还有另一个有趣的细微差别。 我专门使用了有意义的类型。 我建议尝试用任何引用类型(例如字符串)替换Stub类型的Foo方法的参数类型。 但是方法Useless()的参数类型不会更改。 您可以在下面的机器上查看结果,并提供一些澄清的信息:WinDBG和Calculator :)


可点击的图片

输出窗口以十进制表示法显示引用类型的地址。

合计


我们使用fastcall约定刷新了调​​用方法的知识,并立即使用奇妙的edx寄存器一次在两种方法中传递参数。 我们还吐槽了所有类型,并且知道所有内容都是字节,因此很容易获得对象地址,而无需使用指针和不安全的代码。 此外,我计划将收到的地址用于更多不适用的目的!

感谢您的关注!

PS C#代码可以在这里找到

Source: https://habr.com/ru/post/zh-CN447254/


All Articles