Программирование сокетов в контексте реверс-инжиниринга
В этой статье мы рассмотрим программирование сокетов и разберём простейшую систему клиент-серверного TCP-чата. Понимание аспектов сетевого программирования поможет вам продвинуться в изучении реверс-инжиниринга.
Но прежде, чем мы приступим к разбору кода сервера либо клиента, необходимо (и это важно) внести в код следующую строку:
#define PORT 1337
Данная строка определяет константу PORT как 1337. Константа станет использоваться и на клиенте, и на сервере в роли сетевого порта, применяемого для создания соединения.
Рассмотрим серверную часть
Итак, код:
int Server() { // инициализируем переменные, нужные для работы сервера int server, sock, value; struct sockaddr_in address; int opt = 1; int addrlen = sizeof(address); char buffer[1024] = {0}; const char *serverhello = "Server Hello"; // создаём сокет server = socket(AF_INF, SOCK_STREAM, 0); // настраиваем сокет setsockopt(server, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt)); // настраиваем адрес address.sin_family = AF_INET; address.sin_addr.s_addr = INADDR_ANY; address.sin_port = htons(PORT); // привязываем сокет к серверу bind(server, (struct sockaddr *)&address, (socklen_t*)&addrlen); // ожидание клиентов listen(server, 3); // принимаем соединение sock = accept(server, (struct sockaddr *)&address, (socklen_t*)&addrlen); // читаем сообщение, полученное посредством сокета value = read(sock, buffer, 1024); printf("%s\n", buffer); // отсылаем сообщение посредством сокета send(sock, serverhello, strlen(serverhello), 0); return 0; }
Поначалу мы создаём файловый дескриптор сокета server с доменом AF_INET, кодом протокола 0 и типом SOCK_STREAM. Потом происходит настройка параметров сокета и адрес. Далее сокет привязывается к порту (сетевому адресу), а сервер начинает прослушивать указанный порт с наибольшей длиной очереди 3. В результате, после получения соединения сервер примет его в переменную sock и считает переданное значение в переменную value.
В конце концов, сервер отправит строку serverhello по соединению до возврата функции.
Что же, пришло время посмотреть, как выглядит инициализация серверных переменных в машинном коде:
Поначалу создаются и инициализируются переменные сервера.
Потом происходит создание файлового дескриптора сокетов server посредством системной функции _socket. В качестве параметров для функции выступают протокол, тип и доменное имя, которые передаются посредством регистров edx, esi и edi.
Далее происходит вызов _setsockopt, что необходимо для задания параметров сокета в файле дескриптора “server».
Инициализация серверного адреса происходит посредством adress.sin_family, address.sin_addr.s_addr и address.sin_port.
После конфигурирования сервера он привязывается к интернет-адресу посредством _bind.
А после привязки сервер слушает сокет, передавая файловый дескриптор server. При этом наибольшая длина очереди равняется трём.
После установления соединения сервер будет принимать соединение сокета в переменную sock.
Далее сервер считает сообщение, переданное в переменную value, используя для этого _read.
В конечном итоге, сервер через переменную s в машинном коде отошлёт сообщение serverhello.
Рассмотрим клиентскую часть
Код выглядит следующим образом:
int Client() { struct sockaddr_in address; int sock = 0, value; struct sockaddr_in server_addr; char* helloclient = "Client Hello"; char buffer[1024] = {0}; // создаём сокет server = socket(AF_INF, SOCK_STREAM, 0); // настраиваем объект сокета memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(PORT); // форматируем IP-адрес в бинарный формат inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr); // подсоединяемся к серверу connect(sock, (struct sockaddr *)&server_addr, sizeof(server_addr)); // отсылаем сообщение серверу send(sock, helloclient, strlen(helloclient), 0); // читаем ответ от сервера value = read(sock, buffer, 1024); printf("%s\n", buffer); return 0; }
Итак, поначалу создаётся файловый дескриптор сокета sock посредством кода протокола 0 и переменной домена AF_INET типа SOCK_STREAM. Далее memset применяется в целях заполнения области памяти server_addr нулями. Это происходит до того, как будет установлена информация об адресе посредством server_addr.sin_family и server_addr.sin_port. До подключения клиента к серверу информация об адресе будет преобразована из текстового в двоичный формат посредством inet_pton. После подключения клиент отправит строку helloclient, а потом примет ответ сервера в переменную value. В итоге, переменная value выведется на экран, и произойдёт возврат из функции.
Машинный код инициализации переменных клиента:
В первую очередь, инициализируем локальные переменные клиента.
Дескриптор файла сокета «sock» создастся путём вызова системной функции _socket и благодаря передаче информации о протоколе, типе и домене с помощью регистров edx, esi и edi.
Переменная server_address (это «s» в машинном коде) заполняется нулями (0x30) посредством системного вызова _memset.
Далее происходит настройка адресной информации сервера.
Потом осуществляется перевод адреса из текстового в двоичный формат посредством системной функции _inet_pton. Здесь важно заметить, что так как адрес в коде явно не указан, мы предполагаем localhost (127.0.0.1).
Далее осуществляется подключение клиента к серверу посредством системного вызова _connect.
После того, как произойдёт подключение, клиент отправит на сервер строку helloClient.
В конце концов, клиент получит ответ сервера в переменную value посредством системного вызова _read.