Sockets em ASM

Iniciado por Dark_Side, 27 de Novembro , 2006, 07:36:09 PM

tópico anterior - próximo tópico

0 Membros e 1 Visitante estão vendo este tópico.

Dark_Side

Hi,

Hoje eu acordei cedo e fiquei um bom tempo sem nada para fazer. Para não perder esse tempo, tomei a decisão de escrever algo. Procurando um assunto, me deparei com o FASM, que há muito tempo não utilizava. Então, tive a idéia de escrever algo relacionado a sockets em ASM. É exatamente isso que veremos hoje =)

Antes de começarmos, irei disponibilzar o link para download do compilador utilizado:

http://flatassembler.net/fasmw167.zip

Eu, particularmente, gosto muito do FASM, pois além de ser apresentar como um compilador bem simples e leve, nos permite aplicar funções da API do Windows entre outros recursos com muita facilidade.

Baixe o arquivo e descompacte-o. Eu criei a pasta "C:\FASM" para descompactar os arquivos.

Após extrair os arquivos, abra o arquivo "FASMW.EXE". A partir daí, já podemos introduzir nosso código =)

Vamos ver a estrutura básica de um programa WIN32:

format PE GUI 4.0

include "c:\fasm\include\win32ax.inc"

start:
invoke MessageBox,0,"HI!","LOL!",0x40
invoke ExitProcess,0


data import

 library kernel32,'KERNEL32.DLL'
         user32,'USER32.DLL'

 import kernel32,\
        ExitProcess,'ExitProcess'

 import user32,\
        MessageBox,'MessageBoxA'

end data

O que o programa acima faz? Lolz, apenas mostra uma mensagem e encerra imediatamente após ser carregado.

Vamos desmembrar o código:

format PE GUI 4.0

Define que o programa é para Windows.

include "c:\fasm\include\win32ax.inc"
Inclui o cabeçalho do Windows. Algumas funções vitais estão incluídas nele.
Podemos optar pelo uso do header "win32a.inc" ao invés deste. Mas qual seria a diferença? Bem, utilizando o header "win32a.inc" teremos que abrir mão de algumas facilidades ao programar, como por exemplo passar strings diretamente a funções não necessitando declarar variáveis. Um exemplo:

WIN32AX.INC

MessageBox,0,"HI!","LOL",0


-----------------------------------

WIN32A.INC

MessageBox,0,msg,0,tit,0

msg db "HI!",0
tit db "LOL",0


Note que "c:\fasm\include" é o diretório onde os headers estão contidos. Lembre-se de mudar para o diretório correto, caso necessário.
 
start:
invoke ExitProcess,0
É o ponto de entrada do programa. Invocamos (chamamos) a função MessageBox para exibir a mensagem e, logo após, ExitProcess para encerrar o programa.


data import

 library kernel32,'KERNEL32.DLL'
         user32,'USER32.DLL'

 import kernel32,\
        ExitProcess,'ExitProcess'

 import user32,\
        MessageBox,'MessageBoxA'

end data

A parte acima é destinada à importação de funções e bibliotecas.


library kernel32,'KERNEL32.DLL'
         user32,'USER32.DLL'

O programa importa as DLL's "KERNEL32.DLL" e "USER32.DLL".
 
import kernel32,\
        ExitProcess,'ExitProcess'

O programa importa a função "ExitProcess" dentro da DLL "KERNEL32".
import user32,\
        MessageBox,'MessageBoxA'


O programa importa a função "MessageBox" dentro da DLL "USER32".


Para fazer o teste, salve o código como "exemplo.asm", e em seguida, vá até o menu RUN -> Execute.

Após uma pequena base, vamos ver a estrutura básica de um programa que utiliza sockets:

format PE GUI 4.0

include "c:\fasm\include\win32ax.inc"


start:

sair:
invoke ExitProcess,0



data import

 library kernel32,'KERNEL32.DLL',\
         user32,'USER32.DLL',\
         winsock,'WSOCK32.DLL'

 import kernel32,\
         ExitProcess,'ExitProcess'

 import user32,\
        MessageBox,'MessageBoxA'


 import winsock,\
        WSAStartup,'WSAStartup',\
        socket,'socket',\
        connect,'connect',\
        accept,'accept',\
        bind,'bind',\
        listen,'listen',\
        send,'send',\
        sendto,'sendto',\
        recv,'recv',\
        recvto,'recvto',\
        closesocket,'closesocket',\
        inet_addr,'inet_addr',\
        htons,'htons',\
        gethostbyname,'gethostbyname',\
        WSACleanup,'WSACleanup'



end data

wsadata WSADATA
sock dd 0
sock_addr sockaddr_in 
size_addr dd 16

Além das DLLS já utilizadas anteriormente, importamos a DLL "WSOCK32.DLL" - biblioteca do WINSOCK.


Vejamos as funções importadas:

import winsock,\
        WSAStartup,'WSAStartup',\
        socket,'socket',\
        connect,'connect',\
        accept,'accept',\
        bind,'bind',\
        listen,'listen',\
        send,'send',\
        sendto,'sendto',\
        recv,'recv',\
        recvto,'recvto',\
        closesocket,'closesocket',\
        inet_addr,'inet_addr',\
        htons,'htons',\
        gethostbyname,'gethostbyname',\
        WSACleanup,'WSACleanup'


CitarWSAStartup - utilizada para inicializar o winsock;
socket - criar um socket;
connect - conectar-se a um host;
accept - aceitar uma conexão;
bind - configurar socket localmente;
listen - escuta em uma porta;
send - enviar dados para uma conexão ativa;
sendto - enviar dados para um host com conexão não necessariamente ativa (geralmente protocolo UDP);
recv - receber dados de uma conexão ativa;
recvto - receber dados de um host com conexão não necessariamente ativa (geralmente protocolo UDP);
closesocket - fechar socket;
inet_addr - transformar um IP de string (xxx.xxx.xxx.xxx) para valor numérico;
htons - transformar uma porta em network byte;
gethostbyname - resolver um hostname;
WSACleanup - finalizar winsock;

Embora existam outras funções, estas são as principais.

Abaixo, nós temos a declaração das variáveis que serão utilizadas para trabalhar com sockets:

wsadata WSADATA
sock dd 0
sock_addr sockaddr_in 
size_addr dd 16

Citarwsadata - variável auxiliar para inicializar o winsock;
sock - nome atribuído ao socket;
sock_addr - struct que contém as configurações do socket: porta, host e família;
size_addr - contém o tamanho (bytes) da estrutura sockaddr_in.

Irei dividir o artigo em duas partes: esta, onde iremos abordar o protocolo TCP e uma segunda, onde o protocolo UDP será abordado.

Para começar, vamos ver como criar um socket simples:

start:

invoke WSAStartup,0x101,addr wsadata
cmp eax,-1
je erro_wsa

invoke socket,AF_INET,SOCK_STREAM,0
cmp eax,-1
je erro_socket

mov [sock],eax
invoke WSACleanup
jmp sair


erro_wsa:
invoke MessageBox,0,"Ocorreu um erro ao inicializar o winsock","Erro",0x10
jmp sair

erro_socket:
invoke MessageBox,0,"Ocorreu um erro ao criar o socket","Erro",0x10
jmp sair


sair:
invoke ExitProcess,0


Veja o trecho seguinte:

invoke WSAStartup,0x101,addr wsadata
cmp eax,-1
je erro_wsa

Temos a incialização do winsock neste bloco. O processo é feito pela função WSAStartup:

Citarinvoke WSAStartup,VERSAO,PTWSADATA

    VERSAO:
       versão do winsock que será utilizada. (0x101 ou 101h = 1.1).
 
    PTWSADATA:
       ponteiro para uma variável do tipo WSADATA;

Em caso de erro, a função retorna o valor -1 em EAX.

Note que se o valor de EAX for -1, o programa pula para "erro_wsa".

Se você observar bem, o segundo parâmetro da função exige um ponteiro para uma variável do tipo WSADATA. É por este mesmo motivo que utilizamos a instrução "addr" antes da variável "wsadata" - para passar o seu endereço de memória para o ponteiro =).

A criação do socket é feita no seguinte bloco de código:

invoke socket,AF_INET,SOCK_STREAM,0
cmp eax,-1
je erro_socket

A função socket é a responsável por criá-lo. Sua sintaxe:

Citarinvoke socket,FAMÍLIA,PROTOCOLO,TIPO

   FAMÍLIA:
        seria a família cuja a qual o socket pertence. A família AF_INET é utilizada para protocolos relacionados à internet.

   PROTOCOLO:
        protocolo a usar. No artigo veremos dois deles: SOCK_STREAM (protocolo TCP) e SOCK_DGRAM (protocolo UDP).

  TIPO:
       valor opcional. Geralmente utilizado em RAW_SOCKETS. No artigo, seu valor é usado como ZERO.

Novamente, caso um erro ocorra ao criar o socket, o registrador EAX terá o valor -1. Caso um erro de fato ocorra, o programa pula para "erro_socket". Na ausência de erro, o valor de EAX passa a ser o número de identificação do socket.


Caso a inicialização e criação de sockets não apresentarem erros, atribuímos à variável do socket, o valor apropriado para o novo socket:

mov [sock],eax
invoke WSACleanup
jmp sair

Obs:: para passar um valor de um registrador para uma variável fazemos:

mov [nome_var],REGISTRADOR
Note que apenas criamos um socket, finalizamos o winsock e encerramos o programa.

As mensagens de erro são chamadas da seguinte forma:

erro_wsa: ' Erro ao incializar
invoke MessageBox,0,"Ocorreu um erro ao inicializar o winsock","Erro",0x10
jmp sair

erro_socket: ' Erro ao criar socket
invoke MessageBox,0,"Ocorreu um erro ao criar o socket","Erro",0x10
jmp sair

Repare que os dois trechos chamam pelo label "sair", que contém:


sair:
invoke ExitProcess,0
Ou seja, finaliza o programa.


Aguardando e recebendo conexões
-------------------------------------------

Abaixo nós temos um socket que aguarda e aceita conexões na porta 1234:

start:

invoke WSAStartup,0x101,addr wsadata
cmp eax,-1
je erro_wsa

invoke socket,AF_INET,SOCK_STREAM,0
cmp eax,-1
je erro_socket

mov [sock],eax

mov [sin.sin_family],AF_INET
mov [sin.sin_addr],0

invoke htons,1234
mov [sin.sin_port],ax


invoke bind,[sock],addr sin,[size_addr]
cmp eax,-1
je erro_bind

invoke listen,[sock],1
cmp eax,-1
je erro_listen

invoke accept,[sock],0,0

mov [sock],eax

invoke MessageBox,0,"Um pedido de conexão foi feito =)","Lol",0x40
invoke closesocket,[sock]

invoke WSACleanup
jmp sair


erro_wsa:
invoke MessageBox,0,"Ocorreu um erro ao inicializar o winsock","Erro",0x10
jmp sair

erro_socket:
invoke MessageBox,0,"Ocorreu um erro ao criar o socket","Erro",0x10
jmp sair

erro_bind:
invoke MessageBox,0,"Ocorreu um erro ao configurar o socket localmente","Erro",0x10
jmp sair

erro_listen:
invoke MessageBox,0,"Ocorreu um erro ao colocar o socket na escuta","Erro",0x10
jmp sair


sair:
invoke ExitProcess,0   


O programa cria o socket, coloca-o aguardando na porta 1234, mostra uma mensagem quando um pedido de conexão é feito, fecha o socket e encerra (lol!).

Vamos destacar os pontos importantes:


mov [sin.sin_family],AF_INET
mov [sin.sin_addr],0

invoke htons,1234
mov [sin.sin_port],ax

O trecho acima é responsável pela configuração dos seguintes parâmetros: família do socket, porta e host.

mov [sin.sin_family],AF_INET -> ajusta a família: sin.sin_family = AF_INET

mov [sin.sin_addr],0 -> zera o host permitindo-o ficar em escuta.

invoke htons,1234
mov [sin.sin_port],ax

Primeiramente, chamamos a função htons() para transformar o valor 1234 (porta) para network byte - valor requerido pela estrutura. A função htons() retorna um valor do tamanho WORD, isto é, 16 bits que é armazenado no registrador AX. O membro
"sin.sin_port" também possui 16 bits, portanto, atribuímos a ele o valor em AX.

invoke bind,[sock],addr sin,[size_addr]
cmp eax,-1
je erro_bind

As instruções acima fazem com que o socket seja configurado localmente, isto é, o prepara para ficar no modo de escuta (listen).

CitarA função bind() é responsável por tal. Sua sintaxe é:

invoke bind,SOCKET,PTADDR,sizeADDR


    SOCKET:
       variável que representa o nosso socket;

    PTADDR:
       ponteiro para a estrutura sockaddr_in;

    sizeADDR:
       tamanho da estrutura sockaddr_in (que é igual a 16).


A função retorna em EAX o valor -1 caso o processo falhe. Se, de fato falhar, o programa pula para "erro_bind".

Note que passamos o terceiro argumento da função bind() através da variável "size_addr".

O socket é verdadeiramente colocado em escuta através da função listen():

invoke listen,[sock],1
cmp eax,-1
je erro_listen

A sintaxe é:

Citarinvoke listen,SOCKET,NUM

    SOCKET:
       variável do socket;

    NUM:
       número de conexões que serão escutadas.

Grande parte das funções relacionadas a sockets retorna, em EAX,  o valor -1 quando um erro ocorre. Com a função listen() não é diferente. Desta vez, o label "erro_listen" seria chamado.

invoke accept,[sock],0,0

A linha acima faz com que o socket fique escutando na porta 1234 até que um pedido de conexão seja feito.

A sintaxe é:
Citarinvoke accept,SOCKET,NOVO_SOCKADDR,PTsizeADDR

     SOCKET:
        socket que está em modo listen;


     NOVO_SOCKADDR:
        parâmetro opcional. Especifica uma nova estrutura sockaddr_in contendo informações sobre o host remoto;

     NOVO_SOCKADDR:
        também opcional. Ponteiro para uma variável contendo o tamanho da estrutura sockaddr_in.

A função retorna -1 na ocorrência de falhas ou retorna um novo valor para o socket que contém novas informações sobre a conexão.

Após a conexão ter sido feita, devemos associar novamente um valor ao socket, desta vez, o novo valor retornado em EAX:

mov [sock],eax
invoke MessageBox,0,"Um pedido de conexão foi feito =)","Lol",0x40
Já sabemos que após uma conexão ter sido aceita, uma mensagem é mostrada.


Simplesmente fechamos o socket com:

Citarinvoke closesocket,[sock]

Onde "sock" é a variável do socket ;)

Conectando-se
-------------------------------------------

O código abaixo mostra um exemplo de um programa que se conecta na porta 1234 do host local:

start:

invoke WSAStartup,0x101,addr wsadata
cmp eax,-1
je erro_wsa

invoke socket,AF_INET,SOCK_STREAM,0
cmp eax,-1
je erro_socket

mov [sock],eax
mov [sin.sin_family],AF_INET

invoke inet_addr,"127.0.0.1"
mov [sin.sin_addr],eax

invoke htons,1234
mov [sin.sin_port],ax

invoke connect,[sock],addr sin,[size_addr]
cmp eax,-1
je erro_con

invoke MessageBox,0,"Conectado!","Lol",0x40
invoke closesocket,[sock]

invoke WSACleanup
jmp sair


erro_wsa:
invoke MessageBox,0,"Ocorreu um erro ao inicializar o winsock","Erro",0x10
jmp sair

erro_socket:
invoke MessageBox,0,"Ocorreu um erro ao criar o socket","Erro",0x10
jmp sair

erro_con:
invoke MessageBox,0,"Ocorreu um erro ao se conectar","Erro",0x10
jmp sair


sair:
invoke ExitProcess,0   

Os pontos importantes:


invoke inet_addr,"127.0.0.1"

Chamamos pela função "inet_addr" responsável por transformar um IP no formato STRING é valor numérico - valor requerido pela estrutura.

Este valor é retornado em EAX, por isso, fazemos:

mov [sin.sin_addr],eax -> Define o IP remoto a se conectar
Novamente, temos a especificação da porta:

invoke htons,1234
mov [sin.sin_port],ax

Desta vez, porém, essa será utilizada na conexão com o computador remoto.


invoke connect,[sock],addr sin,[size_addr]
cmp eax,-1
je erro_con

Tentamos nos conectar ao host através da função connect() cuja sintaxe é:

Citarinvoke connect,SOCKET,PTSockAddr,sizeADDR
     
     SOCKET:
        variável do socket;

     PTSockAddr:
        ponteiro para a estrutura sockaddr_in;

     SizeAddr:
        tamanho da estrutura (16).

Um possível erro faria com que o valor -1 fosse retornado em EAX. No fato, o label "erro_con" seria chamado.

As linhas abaixo apenas mostram uma mensagem e fecha o socket, respectivamente:

invoke MessageBox,0,"Conectado!","Lol",0x40 ; Mostra uma notificação caso o programa se conecte ao host
invoke closesocket,[sock]

Note que especificamos o host pelo seu IP. Contudo, podemos obter este IP resolvendo o DNS de um determinado host.
Em outras palavras, quero dizer que não podemos fazer por exemplo:

invoke inet_addr,"www.google.com.br"

Temos que resolver o host "www.google.com.br" em seguida obter seu IP. Isto é feito através da função gethostbyname().

Esta função requere uma nova estrutura - a estrutura HOSTENT. Iremos inclui-la junto às variáveis:

wsadata WSADATA
sock dd 0
sock_addr sockaddr_in 
size_addr dd 16
host hostent ; Declaração da estrutura HOSTENT

Vejamos agora o exemplo:

start:

invoke WSAStartup,0x101,addr wsadata
cmp eax,-1
je erro_wsa

invoke socket,AF_INET,SOCK_STREAM,0
cmp eax,-1
je erro_socket

mov [sock],eax
mov [sin.sin_family],AF_INET

invoke gethostbyname,"localhost"
cmp eax,0
je erro_host

mov eax,[eax+12]
mov eax,[eax]
mov eax,[eax]

mov [sin.sin_addr],eax

invoke htons,1234
mov [sin.sin_port],ax

invoke connect,[sock],addr sin,[size_addr]
cmp eax,-1
je erro_con

invoke MessageBox,0,"Conectado!","Lol",0x40

invoke closesocket,[sock]

invoke WSACleanup
jmp sair


erro_wsa:
invoke MessageBox,0,"Ocorreu um erro ao inicializar o winsock","Erro",0x10
jmp sair

erro_socket:
invoke MessageBox,0,"Ocorreu um erro ao criar o socket","Erro",0x10
jmp sair

erro_con:
invoke MessageBox,0,"Ocorreu um erro ao se conectar","Erro",0x10
jmp sair

erro_host:
invoke MessageBox,0,"Ocorreu um erro ao resolver o host","Erro",0x10
jmp sair

sair:
invoke ExitProcess,0


Vejamos:

invoke gethostbyname,"localhost"
cmp eax,0
je erro_host

Nós tentamos resolver o host "localhost". Caso a função gethostbyname() não consiga resolver o host, é retornado o valor ZERO em EAX, fazendo com que o programa pule para "erro_host".

A sintaxe seria:

 
Citargethostbyname,HOSTNAME

Onde "HOSTNAME" é o host que se tenta resolver =)

A seqüência abaixo é responsável por obter o IP do host:

mov eax,[eax+12] ; Move para EAX o membro h_addr_list - o que contém a lista de IPs para o hostname
mov eax,[eax] ; Obtém o primeiro IP - principal
mov eax,[eax] ; Atribui o valor numérico deste IP a EAX

Já que temos o IP em EAX já convertido para o valor requerido, fazemos:

mov [sin.sin_addr],eax ; Agora o membro sin_addr contém o IP do host "localhost" ;)

A partir daí, podemos utilizar a função connect() para estabelecer uma conexão:

invoke connect,[sock],addr sin,[size_addr]

Recebendo e enviando
-------------------------------------------

Para receber e enviar dados através de um socket, utilizamos, respectivamente, as funções send() e recv().

O envio de dados é muito simples, veja:

invoke send,[sock],"LOL",3,0

O programa enviaria "LOL" para o computador remoto.

Sintaxe:

Citarinvoke send,SOCKET,DADOS,TAM,0

    SOCKET:
       o nosso socket;

    DADOS:
       dados a serem enviados;

    TAM:
       número de bytes a serem enviados;

    FLAGS:
       valores opcionais, geralmente deixado como ZERO.

Outra maneria de utilizar a função send() é passando um buffer como argumento, veja:

invoke send,[sock],Mensagem,3,0


; Na seção da variáveis:

Mensagem db "Lol",0

Para receber dados, devemos declarar um buffer na seção das variáveis:

buffer db 512 dup(0)
O buffer acima pode armazenar 512 bytes =)

Na seção do código, fazemos:
invoke recv,[sock],buffer,512,0

Sintaxe:
Citarinvoke recv,SOCKET,BUFFER,TAM,FLAGS

    SOCKET:
       o nosso socket de novo ;)

    BUFFER:
       buffer que irá armazenar os dados;

    TAM:
       tamanho deste buffer;

    FLAGS:
       valores opcionais, geralmente deixado como ZERO.


A função recv retorna em EAX o valor -1 na ocorrência de falhas, como a perda da conexão por exemplo, ou retorna o número de bytes recebidos.

Um exemplo:
invoke recv,[sock],buffer,512,0
invoke MessageBox,0,buffer,"O computador remoto enviou",0x40

; Nas declarações de variáveis:
buffer db 512 dup(0)

O programa acima, assim que recebesse dados do socket, mostraria-os.
Muito bem, termino de abordar algo bem básico sobre sockets em ASM e espero que tenha entendido alguma coisa do que eu escrevi =)

Na próxima parte, iremos abordar o protocolo UDP.


Bye.

colt7r

Excelente cara, e ninguém pra comentar... mto bom mesmo.

lcs

Excelente.. ótimo.. bom a logica deu pra entender. agora o código ja eh mais complicado.
Pra que viver sem sentido.

unn4m3D_BR

Antigo o post mais sendo do Dark vale a pena comentar em qualquer circunstancia.
Realmente ninguém comenta em coisas assim e realmente não sei o porque.

Parabéns Dark, sabe que falo que sou um admirador nato teu toda vez que falamos sobre codar. rs

bjxx ..
Reversing is my life!
www.reversing4life.com