Возиться со стеком в ассемблере и с ++ - PullRequest
6 голосов
/ 12 апреля 2010

Я хочу сделать следующее:

У меня есть функция, которая не моя (здесь на самом деле это не имеет значения, я просто хочу сказать, что я не контролирую ее), и я хочу установить патч, чтобы она вызывала мою функцию, сохраняя список аргументов (прыжки не вариант).

То, что я пытаюсь сделать, это поместить указатель стека, как это было до вызова этой функции, а затем вызвать мой (например, вернуться назад и сделать то же самое, но с другой функцией). Это не работает прямо, потому что стек становится испорченным. Я считаю, что когда я делаю вызов, он заменяет обратный адрес. Итак, я сделал шаг, чтобы сохранить адрес возврата, сохранив его в глобальной переменной, и это работает, но это не нормально, потому что я хочу, чтобы он сопротивлялся рекурсивности, и вы понимаете, о чем я. Во всяком случае, я новичок в сборке, поэтому я здесь.

Пожалуйста, не говорите мне о уже сделанном программном обеспечении для этого, потому что я хочу сделать все по-своему.

Конечно, этот код должен быть независимым от компилятора и оптимизации.

Мой код (если он больше допустимого, скажите, пожалуйста, как его опубликовать):

// A function that is not mine but to which I have access and want to patch so that it calls a function of mine with its original arguments
void real(int a,int b,int c,int d)
{

}

// A function that I want to be called, receiving the original arguments
void receiver(int a,int b,int c,int d)
{
 printf("Arguments %d %d %d %d\n",a,b,c,d);
}

long helper;

// A patch to apply in the "real" function and on which I will call "receiver" with the same arguments that "real" received.
__declspec( naked ) void patch()
{
 _asm
 {
  // This first two instructions save the return address in a global variable
  // If I don't save and restore, the program won't work correctly.
  // I want to do this without having to use a global variable
  mov eax, [ebp+4]
  mov helper,eax

  push ebp
  mov ebp, esp

  // Make that the stack becomes as it were before the real function was called
  add esp, 8

  // Calls our receiver 
  call receiver

  mov esp, ebp
  pop ebp

  // Restores the return address previously saved
  mov eax, helper
  mov [ebp+4],eax

  ret
 }
}

int _tmain(int argc, _TCHAR* argv[])
{
 FlushInstructionCache(GetCurrentProcess(),&real,5);

 DWORD oldProtection;
 VirtualProtect(&real,5,PAGE_EXECUTE_READWRITE,&oldProtection);

 // Patching the real function to go to my patch
 ((unsigned char*)real)[0] = 0xE9;
 *((long*)((long)(real) + sizeof(unsigned char))) = (char*)patch - (char*)real - 5;

 // calling real function (I'm just calling it with inline assembly because otherwise it seems to works as if it were un patched
 // that is strange but irrelevant for this
 _asm
 {
  push 666
  push 1337
  push 69
  push 100
  call real
  add esp, 16
 }

 return 0;
}

Печать (и должна):

Аргументы 100 69 1337 666

Edit:

Код, который я тестирую по предложению Влада (все еще не работает)

// A patch to apply in the real function and on which I will call receiver with the same arguments that "real" received.
__declspec( naked ) void patch()
{
    _asm
    {
        jmp start

        mem:

        nop
        nop
        nop
        nop

        start :

        // This first two instructions save the return address in a global variable
        // If I don't save and restore the program won't work correctly.
        // I want to do this without having to use a global variable
        mov eax, [ebp+4]
        mov mem, eax

        push ebp
        mov ebp, esp

        // Make that the stack becomes as it were before the real function was called
        add esp, 8

        // Calls our receiver 
        call receiver

        mov esp, ebp
        pop ebp

        // Restores the return address previously saved
        mov eax, mem
        mov [ebp+4],eax

        ret
    }
} 

Ответы [ 4 ]

2 голосов
/ 10 мая 2010

Следующие фрагменты кода были проверены с помощью mingw-g ++, но должны работать в VC ++ с небольшими изменениями. Полные источники доступны на Launchpad: 1

Единственный способ безопасно сохранить данные, относящиеся к вызову, - это сохранить их в стеке. Одним из способов является вращение части стека.

отрывок patch.s (patchfun-rollstack):

sub esp, 4          # allocate scratch space

mov eax, DWORD PTR [esp+4]  # first we move down
mov DWORD PTR [esp], eax    # our return pointer

mov eax, DWORD PTR [esp+8]  # then our parameters
mov DWORD PTR [esp+4], eax
mov eax, DWORD PTR [esp+12]
mov DWORD PTR [esp+8], eax
mov eax, DWORD PTR [esp+16]
mov DWORD PTR [esp+12], eax
mov eax, DWORD PTR [esp+20]
mov DWORD PTR [esp+16], eax

mov eax, DWORD PTR [esp]    # save return pointer
mov DWORD PTR [esp+20], eax # behind arguments

add esp, 4          # free scratch space
call    __Z8receiveriiii

mov eax, DWORD PTR [esp+16] # restore return pointer
mov DWORD PTR [esp], eax

ret

Мы опустили ebp здесь. Если мы добавим это, нам нужно будет использовать 8 байтов пустого пространства и сохранить и восстановить ebp, а также eip. Обратите внимание, что когда мы восстанавливаем указатель возврата, мы перезаписываем параметр a. Чтобы избежать этого, нам нужно снова повернуть стек обратно.


Другой способ заключается в том, чтобы вызываемый пользователь знал о дополнительных данных в стеке и игнорировал их.

patch.s (patchfun-ignorepointers):

push    ebp
mov ebp, esp
call    receiver
leave
ret

receiver.cc:

void receiver(const void *epb, const void *eip, int a,int b,int c,int d)
{
 printf("Arguments %d %d %d %d\n",a,b,c,d);
}

Здесь я включил epb, если вы удалите его из asm, все, что останется, это call и ret, а получателю нужно будет только принять и игнорировать eip.


Конечно, все это в основном для развлечения и любопытства. На самом деле нет простого преимущества перед простым решением:

void patch(int a,int b,int c,int d)
{
 receiver(a,b,c,d);
}

Сгенерированная сборка будет короче, чем наш стек, но потребуется стек на 16 байт, потому что значения копируются в новую область ниже patch() стека.

(На самом деле сгенерированный gcc ассемблер выделяет 28 байтов в стеке, хотя он использует только 16. Я не уверен, почему. Может быть, дополнительные 12 байтов являются частью какой-либо схемы защиты от разрушения стека.)

0 голосов
/ 15 апреля 2010

Я никогда не использовал C ++ для подобных вещей низкого уровня, поэтому я не буду вдаваться в специфику вашего примера, но в целом, если вы хотите перехватить вызов и иметь рекурсию поддержки логики, у вас есть два варианта: Либо скопируйте весь кадр стека (параметры) и вызовите «перехваченный» оригинал с новой копией, либо, если это невозможно, сохраните свой собственный небольшой стек для хранения исходного возвращаемого значения (например, в качестве корневого связанного списка) в TLS- на основе структуры данных.

0 голосов
/ 15 апреля 2010

Во время нормального выполнения операнды функции помещаются в стек в обратном порядке. При выполнении кода операции call процессор сначала помещает регистр EIP (или CS / IP) в стек. Это обратный адрес. Когда исполнение достигает функции, которую вы хотите заменить, вот так выглядит склад:

Return address 1
Operand 1
Operand 2
Operand 2

В этот момент вы будете вызывать свою собственную функцию, которая будет иметь такой стек:

Return address 2
Return address 1
Operand 1
Operand 2
Operand 3

Ваша функция должна знать, что в стеке есть дополнительный DWORD, поскольку она делает то, что вы хотите. Это легко сделать, если вы также написали сборку функции замены, просто добавьте 4 всякий раз, когда вы ссылаетесь на ESP. Когда вы вызываете RET в своей функции, первый обратный адрес будет вытолкнут, и выполнение вернется к функции, которую вы заменяете. Стек снова будет:

Return address 1
Operand 1
Operand 2
Operand 3

Вызов RET в этой функции еще раз вытолкнет адрес возврата из стека и вернет управление вызывающей функции. Это оставляет ваши операнды в стеке, что приводит к повреждению. Я предлагаю вызывать RET с таким количеством операндов функций, как это:

RET 3

Это вытолкнет 3 (в моем примере) или сколько угодно операндов из стека. Вот пара ссылок, которые могут оказаться полезными:

http://pdos.csail.mit.edu/6.828/2009/readings/i386/CALL.htm http://pdos.csail.mit.edu/6.828/2009/readings/i386/RET.htm

0 голосов
/ 12 апреля 2010

У вас есть один раз add esp, 8 и один раз add esp, 16. Один из них должен быть не прав.

Edit:
О, я вижу, после add esp, 8 вы, должно быть, удалили из стека ebp выдвинули 2 инструкции до и адрес возврата.

На [ebp + 4] должен быть обратный адрес звонка _tmain.

Edit2:
Вы можете выделить «внутреннюю» переменную примерно так:

  call next
  dd 0
next:
  pop eax
  mov [eax], yourinfo

Но все еще неясно, зачем нам вообще сохранять это значение.

Edit3: (удалено, было не так)

Edit4:
Другая идея:

__declspec( naked ) void patch()
{
 _asm
 {
  call next
  // here we temporarily save the arguments
  dd   0
  dd   0
  dd   0
  dd   0
next:
  pop eax
  // eax points to the first dd

  // now store the args
  pop edx
  mov [eax], edx
  pop edx
  mov [eax+4], edx
  pop edx
  mov [eax+8], edx
  pop edx
  mov [eax+12], edx

  // now we can push the value
  mov edx, [ebp+4]
  push edx

  // now, push the args again
  mov edx, [eax+12]
  push edx
  mov edx, [eax+8]
  push edx
  mov edx, [eax+4]
  push edx
  mov edx, [eax]
  push edx

  // now continue with the old code
  // --------------------------------
  // restore the arguments    
  push ebp
  mov ebp, esp

  // Make that the stack becomes as it were before the real function was called
  add esp, 8

  // Calls our receiver 
  call receiver

  mov esp, ebp
  pop ebp

  // ----------------------------
  pop edx
  mov [ebp+4], edx

  ret
 }
}

Это решение переживает рекурсию, но не одновременное выполнение из 2 разных потоков.

...