Colocamos os parâmetros em operações inseguras em código seguro

Olá pessoal. Desta vez, continuamos a rir da chamada de método normal. Sugiro que você se familiarize com a chamada de método com parâmetros sem passar parâmetros. Também tentamos converter o tipo de referência em um número - seu endereço, sem usar ponteiros e código não seguro.

Isenção de responsabilidade


Antes de começar a história, recomendo fortemente que você leia o post anterior sobre o StructLayout , porque as coisas acordadas não serão repetidas aqui.

Gostaria também de advertir que este artigo não contém material que deve ser usado em projetos reais.

Algumas informações iniciais


Antes de começarmos, vamos lembrar como o código C # é convertido.
Vamos dar uma olhada em um exemplo simples. Deixe-me lembrá-lo que, para me divertir com o StructLayout, eu uso apenas métodos virtuais.

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); } } 

Este código não contém nada complicado, mas as instruções geradas pelo JiT contêm vários pontos-chave. Proponho analisar apenas um pequeno fragmento do código gerado.

  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] 

Neste pequeno exemplo, você pode observar a chamada rápida - um acordo sobre a passagem de parâmetros através de registradores (os dois primeiros parâmetros da esquerda para a direita nos registradores ecx e edx), e os demais parâmetros são passados ​​da direita para a esquerda na pilha. O primeiro parâmetro (implícito) é o endereço da instância da classe na qual o método é chamado. É passado como o primeiro parâmetro implícito para cada método de instância. O segundo parâmetro é uma variável local do tipo int (no nosso caso).

Portanto, na primeira linha, vemos a variável local 5, não há nada interessante aqui.
Na segunda linha, copiamos o endereço da instância Helper no registro ecx. Este é o endereço da própria tabela de métodos.
A terceira linha contém a cópia da variável local 5 no registro edx
A quarta linha copia o endereço da tabela de métodos para o registro eax
A quinta linha contém um deslocamento do registro eax em 40 bytes, carregando um valor da memória em um endereço 40 bytes maior que o endereço da tabela de métodos: o endereço do início dos métodos na tabela de métodos. (A tabela de métodos contém várias informações armazenadas anteriormente. Essas informações, por exemplo, incluem o endereço da tabela de métodos da classe base, o endereço da classe EEC, vários sinalizadores, incluindo o sinalizador do coletor de lixo e assim por diante). Portanto, o endereço do primeiro método da tabela de métodos agora está armazenado no registro eax.
Na sexta linha, o método é chamado no deslocamento 16 desde o início, ou seja, o quinto na tabela de métodos. Por que nosso único método é o quinto? Lembro que o objeto possui 4 métodos virtuais (ToString, Equals, GetHashCode e Finalize), que, portanto, estarão em todas as classes.

Vamos seguir praticando


É hora de começar uma pequena demonstração. Proponho aqui um espaço em branco (muito semelhante ao espaço em branco do artigo anterior).

 [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) { } } 

E o seguinte recheio do método Main:

  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(); } } 

Como você pode imaginar, a partir da experiência do artigo anterior, o método Useless (int j) do tipo Test1 será chamado.

Mas o que será deduzido? Um leitor atento, acredito, já respondeu a essa pergunta. 55555 é exibido no console.

Mas vamos dar uma olhada nos fragmentos do código gerado.

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

Eu acho que você reconhece o padrão de chamada do método virtual, ele inicia após L00cc: nop. Como podemos ver, em ecx o endereço da instância na qual o método é chamado deve ser gravado. Mas desde Se chamarmos um método como Test2, que não possui parâmetros, nada será gravado no edx. Entretanto, antes disso, o método era chamado, que passava o parâmetro pelo registrador edx, respectivamente, e o valor permanecia nele. e podemos observá-lo na janela de saída.

Há outra nuance interessante. Eu usei especificamente um tipo significativo. Sugiro tentar substituir o tipo de parâmetro do método Foo do tipo Stub por qualquer tipo de referência, por exemplo, uma string. Mas o tipo de parâmetro do método Useless não é alterado. Abaixo você pode ver o resultado na minha máquina com alguns elementos de esclarecimento: WinDBG e Calculator :)


Imagem clicável

A janela de saída exibe o endereço do tipo de referência no sistema de números decimais

Sumário


Eles atualizaram o conhecimento de métodos de chamada usando a convenção de chamada rápida e imediatamente usaram o maravilhoso registro edx para passar os métodos do parâmetro 2 por vez. Eles também não deram a mínima para todos os tipos e lembrando que todos os bytes existentes recebem apenas o endereço do objeto com facilidade, sem usar ponteiros e códigos inseguros. Além disso, pretendo usar o endereço recebido para propósitos ainda mais inaplicáveis!

Obrigado pela atenção!

O código PS C # pode ser encontrado aqui

Source: https://habr.com/ru/post/pt424011/


All Articles