![]()
Но мы можем считать, что программа возвращает значение, которое является случайным и увеличивается, и это значение делится на константу 0x7D0.
![]()
И результат сохраняется в EAX, остаток - в EDX.
![]()
Поэтому COUNTER_GUITA на этом этапе будет меньше, чем CONTADOR_GUITA₂.
![]()
И когда COUNTER_GUITA будет меньше 30000, а COUNTER_GUITA_2 будет больше, программа изменит вывод на желтый, то же самое произойдет, когда COUNTER_GUITA будет меньше 10000, а COUNTER_GUITA_2 больше 10000.
До тех пор пока COUNTER_GUITA не станет равным нулю, и программа перейдет к красному блоку, который завершает программу.
![]()
Мы видим, что все это заставляет программу просто запускаться. Это является ее потоком. Теперь мы должны увидеть главную функцию, то есть ту, которая создает этот поток, чтобы увидеть, есть ли у нас возможность повлиять на что-то здесь, чтобы остановить счетчик, потому что здесь «легально» мы видели, что не можем ни на что повлиять.
Хорошо. Давайте начнем анализировать главную функцию, где создан поток, который мы видели ранее.
![]()
Здесь мы видим, что с помощью SUB RSP, RAX, резервируются 0x1088 байтов пространства в стеке для локальных переменных, буферов и т.д. Мы также переименовываем переменную, в которой хранится COOKIE.
Мы знаем, что можем перейти к RECV и работать напрямую с данными, которые мы отправили, потому что мы уже видели, что сервер прослушивает порт, но мы сделаем это медленно и максимально полно.
![]()
Первое, что делаете программа в первом вызове, это вызывает функцию WSASTARTUP.
![]()
IDA показывает нам аргументы. Первый из которых передается в ECX, и это версия. Здесь он устанавливается в 0x202 и сохраняется в указанной переменной WVERSIONREQUESTED, затем ECX читается как первый аргумент.
Второй аргумент, который передается через EDX, является структурой типа WSADATA.
![]()
Если мы перейдем на вкладку структуры, мы увидим это
![]()
Также в LOCAL TYPES.
![]()
Мы видим, что в начале переменная устанавливает в ноль. Мы будем называть ее FLAG_CORRECT, потому что если она равна 1, то это потому, что все в порядке.
![]()
Переменная равна 1, если поле WVERSION, которое было заполнено при вызове WSASTARTUP, равна переменной WVERSIONREQUESTED, которую мы передали ему в качестве аргумента.
Если они равны, программа устанавливает переменную FLAG_CORRECT в единицу, отмечая, что ошибки нет, и возвращает ее в EAX при выходе из этой функции в качестве возвращаемого значения.
Я переименовал функцию в нечто более близкое к тому, что она делает
![]()
Если переменная равно 1, все в порядке, и программа продолжается здесь
![]()
Первый аргумент в RCX - это адрес строки "0.0.0.0"
![]()
Это произойдет на моей машине по адресу 0X13F7ED238. Не путайте с именем A0000.
IDA всегда помещает строки в имя, которое начинается с A, а затем имеет некоторое описание строки, например, означающее, что оно указывает “A 0 0 0 0”
![]()
В опциях есть префикс А, и он создаст имя. Даже если эта опция не может быть изменена и имеет немедленный эффект, это будет сделано при следующем анализе.
Мы видим, что я могу использовать имя строки, которое IDA дает мне для удобства, и если я напишу это в комментариях, двойной щелчок по тому же комментарию приведет меня к строке. Это будет очень удобно.
![]()
![]()
Я делаю двойной щелчок здесь.
![]()
Отлично, продолжаем.
EDX будет иметь значение 41414, которое будет портом, в котором программа будет прослушиваться.
![]()
![]()
NETSTAT -ano в консоли с правами администратора покажет нам процессы и порты. В этом случае вы видите 41414.
И третий аргумент в R8 это указатель на QWORD с именем S
![]()
Мы видим другую переменную, которая сохранит флаг, если программа может выполнить успешное прослушивание. Поэтому мы переменовываем переменную в FLAG_LISTEN
![]()
Затем вызовите HTONS, чтобы перевести значение порта в BIG_ENDIAN (darlo vuelta en criollo)
![]()
Существует также переменная имени типа SOCKADDR.
![]()
В структурах.
![]()
И в LOCAL TYPES.
![]()
Затем вызывается сокет.
![]()
![]()
Мы видим, что в поле SA_FAMILY сохраняется 2, и затем три аргумента для сокета передаются непосредственно как константы, а в AF, что является семейством адресов, программа передает 2 напрямую. (IPV4)
![]()
Тип это 1 (SOCK_STREAM) что соответствует TCP.
![]()
И протокол будет нулевым (НЕ ОПРЕДЕЛЕН)
![]()
И возвращаемое значение будет дескриптором сокета. (То, что мы обычно называем дескриптором сокета)
![]()
Затем вызывается BIND с тремя аргументами: длина имени структуры SOCKADDR, затем указатель на то же имя структуры и сокет S.
![]()
![]()
Если вы поместите BIND в этот порт, вы получите ноль. Если вы не получите ошибку, которая видна в таблице с информацией.
Если программа получит ноль, она вызовет LISTEN.
![]()
Здесь мы видим информацию, что один из аргументов - это сокет, а другой - число, называемое BACKLOG, которое в нашем случае будет равно нулю.
![]()
Здесь есть нулевое значение, что делает
![]()
Также, если все правильно, программа возвратит нуль.
![]()
Сокет, который был в локальной переменной S, сохранит его в содержимом P_S для использования в родительских функциях
![]()
Я дал имя функции.
![]()
Если возвращается 1, программа продолжает выполнение здесь.
![]()
Первый вызов - эта функция без аргументов.
![]()
Посмотрим что делает эта функция.
![]()
Мы видим, что она не имеет аргументов, непосредственно резервирует пространство для переменных с помощью Sub RSP, 0x168.
![]()
Мы видим вызов MEMSET с аргументом 0x20. Другими словами должен заполниться буфер, длина которого в аргументе SIZE равна 0x100. Также в IDA, если я перехожу к статическому представлению стека, я могу сделать это, дважды щелкнув по CHARACTER или любой переменной.
![]()
Я вижу пустое пространство в буфере. Если щелкнуть правой кнопкой мыши и выбрать ARRAY мне говорится, что длина равна 256.
![]()
Здесь существует буфер с именем CHARACTER размером 256 байт или 0x100H.
![]()
Мы видим, что цикл будет повторяться, пока счетчик не достигнет 0x50.
![]()
![]()
Ещё раз получается хэндл вывода и вызывается WRITECONSOLEOUTPUTCHARACTERA,
Если я запускаю программа, в этом я не вижу записи буфера со значением 0x2020 в каждом слове, так как буфер инициализируется пробелами 0x20. Это просто нужно для рисования в консоли, Здесь нет ничего интересного нет. Продолжаем.
![]()
Затем устанавливается позиция курсора, обнуляя поля X и Y, передавая их как DWORD, поскольку они являются последовательными, и каждое из них является словом.
![]()
Затем создаётся поток, который мы уже проанализировали с помощью CREATETHREAD, передав его в качестве начального адреса STARTADDRESS. Эта функция уже проанализирована.
![]()
После запуска потока, программа напечатает TCP SERVER ACTIVATED.
![]()
Y ya empezaría la parte interesante, normalmente uno en un server buscaría el recv y empezaría a reversear desde allí, pero acá la idea es aprender y ser detallado para practicar, así que seguimos.
![]()
Перемещается курсор, так как Y теперь равен 1.
![]()
Происходит вывод “WAITING FOR CLIENT CONNECTIONS” и вызывается функция ACCEPT.
![]()
В этом случае ACCEPT разрешает входящее соединение и еще не назначает дескриптор сокета, который прослушивает, как мы уже видели, и здесь будет первый аргумент, но в регистре RAX вернется другой дескриптор, который будет дескриптором соединения с этим конкретным клиентом.
![]()
Здесь находятся 3 аргумента: указатель на ADDRLEN, в данном случае 0x10, указатель на ADDR, который получает адрес того, что подключается, и последний - сокет S. В регистре RAX возвращается дескриптор этого соединения, который был установлен.
![]()
Мы видим, что если я запущу сервер и установлю BP при возврате ACCEPT, то IDA не остановится, если я не отправлю правильный пакет на правильный порт, как в этом случае. Здесь отправил пакет и отладчик остановился.
![]()
Затем вызывается другая похожая функция, которая рисует что-то в консоли.
![]()
Мы это уже проходили, я не собираюсь анализировать это снова.
![]()
Здесь печатается NEW CONECTION ACCEPTED и идет переход в функцию в RECV, где начинается то что нужно, поскольку, когда она получает данные, которые я могу отправить ей, если в программе есть какая-либо уязвимость, она может на нее повлиять.
Мы видим, что длина того, что вы можете получить, будет максимум 0x1000, флаги будут равны нулю, то, что я отправлю в буфере BUF, будет сохранено, и первым аргументом в RCX будет HANDLE_CONNECTION.
![]()
Если все прошло хорошо, возвращаемым значением будет количество полученных байтов.
![]()
Давайте посмотрим длину буфера, куда программа будет получать данные.
![]()
![]()
Так что все в порядке, буфер не переполнится.
![]()
Мы видим, что переменная ADDRLEN, которая в ACCEPT использовала ее для длины ADDR, теперь повторно использует ее для получения количества полученных байтов.
![]()
![]()
Обычно, когда переменная используется повторно, я ставлю новое имя после нескольких подчеркиваний, чтобы оно выглядело как два разных применения.
После печати количества полученных байтов идет функция. Посмотрим, что она делает.
![]()
Она имеет два аргумента. Первый в ECX - количество полученных байтов, а второй в RDX - указатель на буфер, в котором я храню данные, которые отправляю.
![]()
Мы видим, что локально я ставлю имя текущего значения повторно используемой переменной, чтобы не перепутать.
Если бы я хотел распространить с SET TYPE, то делаю так.
__INT64 __USERCALL FUNCION_2@<RAX>(INT CANTIDAD_BYTES_RECIBIDOS@<RCX>, CHAR * P_BUF@<EDX>);
Здесь нужно распространить переменные.
![]()
Мы видим, что программа сравнивает с помощью STRNCMP первые 6 байтов данных, которые вы отправляете, со строкой "Hello".
![]()
![]()
Если функция возвращает ноль, то они равны, и флаг установлен в 1.
Поскольку при выходе из этой функции, если она возвращает 1, происходит рукопожатие, я переименую функцию в CHECK_HANDSHAKE.
![]()
![]()
Хорошо, как мы видим, если мы передали “Hello” будет действительным рукопожатие.
Затем программе возвращает в SEND строку, которая начинается с “Hi и несколько пробелов” длиной 8, чтобы не путать с другим BUF, который находится в секции данных. Я собираюсь переименовать его, чтобы избежать проблем.
![]()
![]()
Теперь смотрится лучше. Длина данных, которые вы мне отправите, будет 8. Здесь нет проблем.
![]()
Здесь программа сохраняет отправленные байты, затем выводит “WAITING FOR REQUEST”, происходит вызов функции, а затем соединение с клиентом закрывается, и программа возвращается, чтобы принять ожидание другого, поэтому все должно быть приготовлено в этой последней функции, которая находится до CLOSESOCKET.
![]()
Мы видим, что у него есть единственный аргумент - HANDLE_CONNECTION.
После сохранения аргумента резервируется место для переменных
![]()
Тот, кто хочет распространять нужно написать так
__INT64 __USERCALL FUNCION_2@<RAX>(__INT64 HANDLE_CONEXION@<RCX>);
![]()
Затем вызывается другой RECV с тем же HANDLE_CONNECTION, но длиной 0x10. Давайте посмотрим буфер, в который поступают данные.
![]()
Поскольку размер буфера составляет 0x10 т.е. 16 байтов, он будет включать переменную BUF и три переменные 224, 220 и 21C.
![]()
Если я хочу, я могу создать структуру для этого буфера.
Здесь у меня есть 16 байтов длиной с 4 DWORDS, тогда я увижу, какое конкретное имя поставить каждому из них.
![]()
И переменную переименую в MY_BUF.
![]()
Здесь сохраняется количество полученных байтов, которых может быть не более 0x10 т.е. 16.
![]()
Сравнивается количество полученных байтов с 0x10, и оно должно быть именно этим значением, потому что, если оно не меньше, оно будет равно или больше, и выше оно не может быть, потому что у recv было максимум 0x10, поэтому он принимает только 0x10.
![]()
Мы видим, что программа собирается выполнить еще одно RECV с тем же HANDLE_CONNECTION, но в этом случае размер равен CAMPO_0 того, что я отправил в предыдущем пакете из 16 байтов, и программа сохранит результат в VAR_218, которая будет буфером для этого RECV.
![]()
Мы видим, что этот буфер составляет 512 байт, и я мог бы отправить ему больше данных, так как размер RECV, который я обрабатываю через CAMPO_0. Проблема заключается в том, что вы перезаписываете COOKIE, и это приводит к закрытию программы, и в 64-битных файлах нет исключения в стеке, так что это пока не приведет к переполнению.
![]()
![]()
Также, я переименую переменную в BUF_512_TERCER_RECV.
![]()
И здесь я переименую CAMPO_0, я также вижу, что я использовал его повторно, чтобы сохранить полученные байты.
![]()
Y bueno quedo largo pero yo me entiendo jeje, con las variables reusadas pasa esto.
![]()
Затем выполняется сравнение со знаком, просто следите, если FIELD_3 меньше нуля.
![]()
Я переименую переменную.
Затем идет сравнение без знака, только если CAMPO_2 меньше или равно 0x200.
![]()
Я переименую её.
![]()
Кроме того, сообщения об ошибках дают мне представление об именах полей, CAMPO_3 будет смещением, а CAMPO_2 будет равно уровню.
![]()
Сейчас смотрится лучше.
![]()
Поле 1 является операцией сравнения с 0x11111111 или 0x22222222, и если , то программа идет на INVALID OPERATION, поэтому мы переименовываем его. Мы видим структуру второго пакета.
Мы видим структуру второго пакета.
Введение в реверсинг с нуля, используя IDA PRO. Часть 65. Часть 2.
Дата публикации 11 июн 2019
| Редактировалось 4 июл 2019