Перейти к содержанию

Использование createthread (или война со стеком)


Рекомендуемые сообщения

@Garik66 спросил - как использовать createthread, чтобы он не крашился. 

Начал отвечать и это слегка вышло за рамки простого ответа в тему, поэтому решил выделить это в отдельный топик.

 

Я не очень хорошо знаю assembler, но попробую описать возможные причины.

 

createthread создает поток и просит его выполнить call с адресом переданной функции. 

Например в данном случае createthread(my_functionв отдельном потоке выполнит call my_function:

Spoiler

alloc(my_function, 64)

my_function:
ret

createthread(my_function)

 

 

И тут вступает в дело стек, да.

Вот эта штука (в Memory View вызывается через ПКМ на правом нижнем окне и выборе пункта Full stack):

Spoiler

p01.thumb.png.c660ef3dac09db453942530485

 

После выполнения call my_function мы попадаем в нашу функцию, а в стек записывается адрес, куда нужно вернуться после выполнения этой функции (my_function). Собственно за возврат куда нужно и отвечает ret. Он берет первую запись из стека и делает jmp в правильное место. Собственно сам адрес возврата виден в стеке на скриншоте - это KERNEL32.BaseThreadInitThunk+24

 

Без ret ассемблер вывалится за пределы функции и попытается выполнить команду - add [eax],al , хотя для нас это просто участок пустой памяти - 00 00. Ну а так как в EAX ничего приличного нет, то программа выдает ошибку - сорян, не могу записать в данный участок памяти и падает. Поэтому ret обязателен.

 

Из этого вытекает то, что при подходе к ret в последней записи в стеке (адрес в регистр ESP) должна быть с правильным адресом возврата, который был нам передан из call my_function. И основная причина крашей в том, что регистр ESP указывает не на тот адрес при выполнении команды ret. Программа выполняет jmp на неизвестный нам адрес и крешится из-за невозможности выполнить команды, на которые она попала (как с add [eax],al).

 

А что собственно двигает стек и адрес регистра ESP?

 

А двигают стек наши любимые команды push и pop. Собственно, когда мы пытаемся вызывать функцию и передаем параметры через push #100, на самом деле выполняются две команды - sub esp,4 и mov [esp],#100. Первая уменьшает адрес вершины стека на 4 байта, а вторая записывает на вершину стека значение 100. А pop eax сдвигает вершину в другую сторону - делает mov eax, [esp] и дальше сдвигает вершину стека назад add esp,4.

 

Поэтому после того, как мы сдвинули стек с помощью push при передаче параметров в функцию - после её выполнения нужно сдвинуть стек обратно! 

 

Собственно следующий пример почти 100% скрешится:

Spoiler

[ENABLE]
//code from here to '[DISABLE]' will be used to enable the cheat
globalalloc(my_function, 64)
alloc(second_function, 64)

my_function:
push #100
call second_function
ret

second_function:
mov eax,[esp+4]
ret

createthread(my_function)
 
 
[DISABLE]
//code from here till the end of the code will be used to disable the cheat
dealloc(my_function)
unregistersymbol(my_function)

dealloc(second_function)

 

 

Мы ввели новую функцию second_function, которая берет переданный аргумент и записывает его в EAX. Вроде бы всё просто, передали в функцию 100, получили в EAX ответ, а всё равно креш. Почему? А это видно на следующем скриншоте - на вершине стека теперь хранится значение 0x64 (=100):

Spoiler

p02.thumb.png.339b4b233aa5b808f383436c30

 

Теперь когда выполнится ret он попытается прыгнуть на адрес 0x00000064, а не на 0x760E8744 как должен был.

 

Как же этого избежать? Есть несколько путей:

  1. сохраняем значение регистра esp и возвращаем его перед ret
  2. внимательно следим за стеком по мере выполнения нашей функции и к концу программы он сам будет в правильном месте

 

В целом первый способ очень неплох. Мы можем сохранить адрес регистра esp в памяти и вернуть его прямо перед вызовом ret. Например:

Spoiler

[ENABLE]
//code from here to '[DISABLE]' will be used to enable the cheat
globalalloc(my_function, 64)
alloc(second_function, 64)
alloc(esp_addr, 4)

esp_addr:
dd 0

my_function:
// сохраняем текущий адрес вершины стека
mov [esp_addr],esp

push #100
call second_function

// загружаем обратно адрес вершины стека
mov esp,[esp_addr]
ret

second_function:
mov eax,[esp+4]
ret

createthread(my_function)
 
 
[DISABLE]
//code from here till the end of the code will be used to disable the cheat
dealloc(my_function)
unregistersymbol(my_function)

dealloc(second_function)

dealloc(esp_addr)

 

 

Но это не совсем assembler-way, поэтому я предпочитаю следить за стеком самостоятельно.

Для этого нужно обращать внимание на то, как со стеком работает вызываемая функция.

 

Например, наша функция second_function не следит за стеком. Поэтому если мы делаем push для передачи параметров, то мы так же должны сдвинуть его назад. Это можно сделать через pop указав неиспользуемый регистр или просто добавить к адресу ESP 4 байта (размер адреса в 32 битной системе, в 64-битной - это 8 байт):

Spoiler

// через pop - eax используется, выполним pop в неиспользуемый ebx
push #100
call second_function
pop ebx

// через сдвиг esp
push #100
call second_function
add esp,4

 

 

Собственно таким образом мы двигаем стек за функцией. И в зависимости от количества переданных push - на такое же количество нужно сдвинуть и регистр ESP. Например для 3-х аргументов нужно выполнить 3 pop или добавить к esp 3 * 4 (3 - количество сдвигов, 4 - размер инструкции):

Spoiler

// через pop
push #100
push #200
push #300
call third_function
pop ebx
pop ebx
pop ebx

// через сдвиг esp
push #100
push #200
push #300
call third_function
add esp,c // 0xC = 4 * 3 = 12

 

 

В результате мы получим следующий код:

Spoiler

[ENABLE]
//code from here to '[DISABLE]' will be used to enable the cheat
globalalloc(my_function, 64)
alloc(second_function, 64)

my_function:
push #100
call second_function
add esp,4
ret

second_function:
mov eax,[esp+4]
ret

createthread(my_function)
 
 
[DISABLE]
//code from here till the end of the code will be used to disable the cheat
dealloc(my_function)
unregistersymbol(my_function)

dealloc(second_function)

 

 

Единственное, есть небольшое исключение - функции, которые сами заботятся о стеке, после их вызова не нужно сдвигать регистр esp, просто делаем push и call. Их довольно просто отличить, посмотрим на примере следующего кода:

Spoiler

[ENABLE]
//code from here to '[DISABLE]' will be used to enable the cheat
globalalloc(my_function, 64)
globalalloc(second_function, 64)
globalalloc(third_function, 64)

my_function:
// функция, не сдвигающая стек за собой
push #100
call second_function
add esp,4

// функция, сдвигающая стек за собой
push #100
push #200
call third_function

ret

second_function:
mov eax,[esp+4]
ret

third_function:
mov eax,[esp+4]
add eax,[esp+8]
ret 8

createthread(my_function)
 
[DISABLE]
//code from here till the end of the code will be used to disable the cheat
dealloc(my_function)
unregistersymbol(my_function)

dealloc(second_function)
unregistersymbol(second_function)

dealloc(third_function)
unregistersymbol(third_function)

 

 

Основное отличие - после call функции которая сама двигает стек не идет ни каких add esp,8 или не выполняются pop:

Spoiler

p03.thumb.png.0b1f6cd53dd660f0b8cb51e1e4

 

Так же можно обратить внимание на код самой функции, а точнее на ret 8. Параметр 8 указывает программе, что нужно не просто взять адрес из вершины стека и прыгнуть на него, а так же после этого сдвинуть стек на 8 байт, т.е. выполнить ret и add esp,8. Собственно и получается, что add делаем не мы, а сама функция.

 

Вот и получается, что следя за стеком и тем, как вызываемые функции заботятся о стеке - мы не получим крашей в вызове createthread.

Изменено пользователем srg91
  • Плюс 12
Ссылка на комментарий
Поделиться на другие сайты

Респект за статью!

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

 

Ссылка на комментарий
Поделиться на другие сайты

11 минуту назад, srg91 сказал:

 

@Garik66 спросил

 

@srg91 ответил :D

Ещё раз ОГРОМНОЕ СПАСИБО!!!

Жаль что не могу сразу на + 100 лайкнуть.

@MasterGH может ты поставишь?

  • Плюс 2
Ссылка на комментарий
Поделиться на другие сайты

  • Garik66 закрепил тема
9 minutes ago, MasterGH said:

Просьба хостить скриншоты на форум

 

Готово. Да, так даже удобнее, я что-то машинально.

Спасибо.

 

Спасибо за отзывы. Если есть ошибки - пишите, я исправлю.

  • Плюс 2
Ссылка на комментарий
Поделиться на другие сайты

28 минуты назад, srg91 сказал:

Параметр 8 указывает программе, что нужно не просто взять адрес из вершины стека и прыгнуть на него, а перед этим сдвинуть стека на 8 байт, т.е. выполнить add esp,8 и ret.

 

Для ret 8.  Сначала  возврат по [ESP] (как обычно) и после этого (либо одновременно) esp сдвигается на 8.

Ссылка на комментарий
Поделиться на другие сайты

1 час назад, srg91 сказал:

В целом первый способ очень неплох, мы сохранить адрес регистра esp и вернуть его перед ret.


немного бросается в глаза и еще, мне кажется все это описание работы со стеком уже переводит статью в раздел для новичков, просто потому, что все подробно описано, с таким подробным описанием даже, тот кто только установил CE разберется что и как делать.

Изменено пользователем partoftheworlD
  • Плюс 1
Ссылка на комментарий
Поделиться на другие сайты

17 minutes ago, partoftheworlD said:

немного бросается в глаза

 

Исправил, спасибо.

 

17 minutes ago, partoftheworlD said:

и еще

 

Я тоже так думаю, но предыдущие статьи показали, что это продвинутый уровень :)

Изменено пользователем srg91
Ссылка на комментарий
Поделиться на другие сайты

23 часа назад, srg91 сказал:

 

@Garik66 спросил - как использовать createthread, чтобы он не крашился. 

 

Крах как оказалось у меня происходит не от ESP и правильности написания скрипта. а как я понял в непонимании какие параметры нужно передавать, вот это у меня пока не укладывается в голове. У меня крах всегда с ошибкой "access  violation".

Нужно будет всё-таки потратить время, чтобы понять..

Ссылка на комментарий
Поделиться на другие сайты

4 минуты назад, srg91 сказал:

 

ты про вызов любых функций из createthread? или про саму createthread?

Это проще показать, чем описывать. А пока мне нужно самому покопаться. Просто пока не уложилось в голове.

Ссылка на комментарий
Поделиться на другие сайты

В 30.04.2017 в 14:43, partoftheworlD сказал:

немного бросается в глаза и еще, мне кажется все это описание работы со стеком уже переводит статью в раздел для новичков, просто потому, что все подробно описано, с таким подробным описанием даже, тот кто только установил CE разберется что и как делать.

Согласен))

А вообще гуглите calling convention, если до сих пор что-то не понятно. Это обычное согласование о вызове, я даже немного удивлен что люди, которые нехило так продвинуты в RE не знают об этом)

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

 

add esp (cdecl) обычно используется после вызова функций, у которых неопределенное количество аргументов. Ну, например тот же printf в Си. А в остальных случаях используется обычно stdcall и thiscall, при которых функция сама через ret чистит стек, так как знает сколько аргументов она принимает.

Изменено пользователем uhx
Ссылка на комментарий
Поделиться на другие сайты

19 минут назад, uhx сказал:

что люди, которые нехило так продвинуты в RE не знают об этом)

Если ты про меня, то всякое бывает, Я до 2014 г. с программированием и близко знаком не был. ни одного ЯП до сих пор не знаю, всё что знаю - узнал здесь и конечно теперь есть пробелы  в знаниях и в понимании некоторых вещей.

Ссылка на комментарий
Поделиться на другие сайты

1 час назад, Garik66 сказал:

Если ты про меня, то всякое бывает, Я до 2014 г. с программированием и близко знаком не был. ни одного ЯП до сих пор не знаю, всё что знаю - узнал здесь и конечно теперь есть пробелы  в знаниях и в понимании некоторых вещей.

Да не парься)) Если раньше не встречалось - значит не особо и нужно было. Мне например просто в свое время стало интересно, как вызовы происходят и все такое. Тот же printf в OllyDbg разглядывал сидел, а потом только прочитал про CC. Разберешься, куда деваться)

Ссылка на комментарий
Поделиться на другие сайты

3 часа назад, uhx сказал:

Это обычное согласование о вызове, я даже немного удивлен что люди, которые нехило так продвинуты в RE не знают об этом)

 

Пользователи, которые пишут статьи, ведут активность на форуме у меня на вес золота. @srg91 молодец, что написал статью. Огромный респект.

Если кто-то будет их провоцировать, обижать, сразу забаню.

Ссылка на комментарий
Поделиться на другие сайты

5 часов назад, uhx сказал:

я даже немного удивлен что люди, которые нехило так продвинуты в RE не знают об этом)


Видимо им это не нужно, раз не знают.

Ссылка на комментарий
Поделиться на другие сайты

6 hours ago, uhx said:

Согласен

 

А вот за этим это всё и пишется, чтобы продвигаться дальше. Понятно, что это всё известно, но это же еще нужно знать куда копать, тут ваши комментарии и помогают :) Заодно тоже могли бы топик с основными ссылками создать, их бы и закрепили )

 

1 hour ago, MasterGH said:

Пользователи

 

Спасибо, но в целом критика то к месту, да и выхлоп есть - правильные слова звучат, теперь еще и знаем куда смотреть ) Осталось только еще где-то время на это взять :D Но никто и не обещал, что будет быстро ))

 

1 hour ago, partoftheworlD said:

Видимо

 

Угум-с :)

Ссылка на комментарий
Поделиться на другие сайты

2 часа назад, MasterGH сказал:

 

Пользователи, которые пишут статьи, ведут активность на форуме у меня на вес золота. @srg91 молодец, что написал статью. Огромный респект.

Если кто-то будет их провоцировать, обижать, сразу забаню.

Никто никого и не обижает) Статья классная, безусловно, я просто немного дополнил ТСа, пояснив что эта вещь в общем и целом называется calling convention.

Ну еще речь шла о том что возможно из-за столь подробного описания стоит перенести тему в раздел для новичков.

Ссылка на комментарий
Поделиться на другие сайты

×
×
  • Создать...

Важная информация

Находясь на нашем сайте, Вы автоматически соглашаетесь соблюдать наши Условия использования.