Port knocking from the scratch

Raw sockets hacking - Date: 18/12/2022

Introduction

Hi folks, we are going into the fantastic world of low-level sockets in this story. In a Linux system, raw sockets allow a program to directly access the underlying communication protocols, which means the program can send custom packets without the regular protocol headers. This can be useful for creating custom network tools or implementing low-level network protocols. Tools like Nmap are written using raw sockets. Another point of curiosity, we can read how to construct a port scan from scratch using raw sockets in the legendary paper from Phrack magazine, "the art of port scanning" by Fyodor. Phrack is a long-running underground magazine that focuses on topics related to computer security, hacking, and technology. It was founded in 1985 and has been published continuously since then, making it one of the oldest and most well-known publications in the hacking community.

Phrack has gained a reputation for publishing high-quality articles by knowledgeable and experienced authors. Many of the articles in Phrack are technical and provide in-depth coverage of a wide range of topics related to computer security and hacking.

Academia noticed the high quality of Phrack papers,
started citing them, and basing their offensive and defensive work on them.
Did that alienate the underground that Phrack represented for so many
years? Yes, we think it did. But the underground also changed. Some of it
became involved in malware, spyware, and also the "infosec" industry. And
this mutated the underground. 
 Volume 0x10, Issue 0x46, Phile #0x01 of 0x0f 
 http://www.phrack.org/issues/70/1.html

Another point of curiosity, we can use raw sockets resources to create a solution for sniffing. The Wireshark tool, for example, uses libpcap the same as the tcpdump, the tool of a group from Berkeley lab, which uses raw sockets in internals looking for UNIX-like scenarios. Yes has portability to other systems like Windows and macOS and diverse architectures like AVR and MIPS.

The Sniffers and port scanners can be helpful in security studies, as they can help Sniffers identify potential vulnerabilities and security risks in a network. For example, a port scanner can identify open ports that may be potential entry points for attackers, and a sniffer can detect malicious traffic or suspicious activity on the network.

Using raw sockets allows these tools to operate at a lower level, closer to the network hardware, which can provide more detailed and accurate information about network traffic and network hosts. This can be especially useful for security studies, as it allows for a more in-depth analysis of network activity and can provide a complete picture of the security posture of a network.

Motivation

Port knocking is a technique that involves a client sending a series of connections with custom requests to a server using specific sequences and port numbers. The server is configured to ignore all connections outside the scope of custom requests except those that follow the correct port-knocking sequence. Once the correct sequence has been received, the server opens a specific port, allowing the client to establish a connection.

The interesting point here, the sysadmin cannot view the open port by port scanning results. So by writing a custom port-knocking tool in C, we can better understand how the port-knocking technique works at a technical level. This can involve learning about different networking concepts and protocols, such as UDP, TCP and ICMP, and understanding how to write code that can send and receive network packets and handle network connections.

Additionally, writing a custom port-knocking tool can give us more property to work with network tools. Maybe we can create a patch to open source software like Nmap, tcpdump anything else for new features.

Using raw sockets

So to use raw sockets in a Linux system, we will need to use the socket function to create a socket descriptor. The socket function takes three arguments: the address family (Maybe AF_INET6 for IPv6 or AF_INET for IPv4), the type of socket (SOCK_RAW), and the last argument, the protocol. So please look at the following:

int sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_RAW);

Looking at the scenario when we have a socket descriptor, we can use the sendto() function to send packets through the socket. The sendto() function takes three arguments: the socket descriptor, a pointer to the data to be sent, and the data size. So please look at the following:

int bytes_sent = sendto(sockfd, data, data_size, 0, (struct sockaddr *) &dest_addr, sizeof(dest_addr));

We can also use the recvfrom() function to receive packets through the raw socket. The recvfrom() function takes four arguments: the socket descriptor, a buffer to store the received data, the size of the buffer, and a pointer to a sockaddr structure containing the source address of the received packet. For example:

int bytes_received = recvfrom(sockfd, buffer, buffer_size, 0, (struct sockaddr *) &src_addr, &addrlen);

Remember that raw sockets are not for everyone with a weak heart. Sometimes, we must make checksums in the TCP/IP context ourselves. In the context of raw sockets in Linux, the checksum is a value calculated for the packet data and included in the packet header. The checksum helps to ensure the integrity of the packet data during transmission.

When a packet is sent over a network using a raw socket, the operating system will calculate the checksum for the packet data and include it in the packet header. When the packet is received, the checksum is recalculated based on the received data and compared to the checksum value in the packet header. If the two values do not match, it indicates that the packet data has been corrupted during transmission, and the packet is discarded.

So to calculate the checksum for a packet, the operating system divides the packet data into small blocks and performs a series of mathematical operations on each block. The resulting values are then combined to produce the final checksum value. This process is known as checksumming.

The checksum is typically calculated automatically by the operating system in a raw socket program. However, it is also possible to specify a custom checksum value by setting the appropriate field in the packet header. For the sake of knowledge, look at the following:

unsigned short in_cksum(unsigned short *ptr, int nbytes) 
{    
	register u_short    answer;     // u_short == 16 bits   
	register long       sum;        // long == 32 bits    
	u_short         oddbyte;     
     
 	sum = 0;   
 
 	while(nbytes > 1)  
 	{    
  		sum += *ptr++;   
  		nbytes -=, if desired 2;    
 	}    
                        
 	if(!(nbytes^1)) 
 	{    
  		oddbyte = 0;       
  		*((u_char *) &oddbyte) = *(u_char *)ptr;      
  		sum += oddbyte;    
 	}    
     
 	sum = (sum >> 16) + (sum & 0xffff);  // addicina auto-16 para baixo-16     
 	sum += (sum >> 16);           
 	answer = ~sum;         

 	return(answer);    
}        

So to get more property to understand the code chunk "(sum >> 16) + (sum & 0xffff)", please read the paper about bitwise here.

Let's go to the port knocking

During the year 2013, I wrote my first port-knocking tool in C language and another version for assembly(AT&T syntax), following the influence of another tool, the nemesis and IP sorcery used to send custom TCP/IP packets for test purposes, using those resources to bring wisdom to get more property to write a port-knocking from scratch. So over the years, I have made little improvement, which is not perfect. In the future, with the proper time, I can implement modern cryptography like Chacha20 from lib sodium or another resource else.

Interesting points in the source code is the possibility to change keys using the AES256 GCM and to change TCP/IP flags to customize the port knocking, for the sake of knowledge please look at the following:

	//flags 
	packet_prepare.tcp->fin = 1;    
	packet_prepare.tcp->syn = 0;    
	packet_prepare.tcp->rst = 0;    
	packet_prepare.tcp->psh = 1;  
	packet_prepare.tcp->urg = 1;  
	packet_prepare.tcp->ack = 0; 
                
	sin.sin_family = AF_INET;    
	sin.sin_port = packet_prepare.tcp->source;    
	sin.sin_addr.s_addr = packet_prepare.ip->daddr;       
        
	tcp_socket = socket(AF_INET, SOCK_RAW, IPPROTO_RAW);    

	if(tcp_socket < 0) 
	{    
		perror("socket");    
		exit(1);    
	}          

TCP flags

So looking at the TCP/IP protocol suite, flags indicate specific conditions or actions in the header of a TCP (Transmission Control Protocol) packet. Six flags are used in the TCP header. For the sake of knowledge, please look at the following:

  1. SYN (Synchronize) - This flag initiates a TCP connection. When a client wants to start a TCP connection with a server, it sends an SYN packet to the server.

  2. ACK (Acknowledgment) - This flag is used to acknowledge the receipt of a TCP packet. When a device receives a TCP packet, it sends an ACK packet back to the sender to confirm receipt.

  3. FIN (Finish) - This flag terminates a TCP connection. When a device wants to end a TCP connection, it sends a FIN packet to the other device.

  4. RST (Reset) - This flag is used to reset a TCP connection. If a device receives a TCP packet that is invalid or unexpected, it can send an RST packet to reset the connection.

  5. PSH (Push) - This flag is used to push data to the receiving device. When a sender sets the PSH flag, it indicates to the receiver that the data in the packet should be delivered to the application as soon as possible.

  6. URG (Urgent) - This flag is used to indicate that the data in the packets is urgent and should be given priority over other data. The URG flag is typically used in conjunction with the PSH flag to indicate that the data should be delivered to the application as soon as possible.

The TCP protocol uses these flags to manage data flow between devices and ensure that data is delivered reliably and efficiently.

Empirical points in TCP flags

The interesting resource here that we can view in the other project like my custom firewall in Linux kernel module, where customised TCP flags are necessary to block a port scanning, is used in conditions to DROP packets with SYN flag; please look that following:

https://github.com/CoolerVoid/HiddenWall/blob/master/module_generator/output/SandWall.c#L55

		if(	tcp_header->syn == 1 && tcp_header->ack == 0
			&& tcp_header->urg == 0 && tcp_header->rst == 0 
			&& tcp_header->fin == 0 && tcp_header->psh == 0)
			return NF_DROP;

From a port scanner perspective, if we read the Nmap source code, I can bring code chunks like the following:

https://github.com/nmap/nmap/blob/master/scan_engine_raw.cc#L1723

          if (USI->scantype == SYN_SCAN && (tcp->th_flags & (TH_SYN | TH_ACK)) == (TH_SYN | TH_ACK)) {
            /* Yeah!  An open port */
            newstate = PORT_OPEN;
            current_reason = ER_SYNACK;
          } else if (tcp->th_flags & TH_RST) {
            current_reason = ER_RESETPEER;

Symmetric encryption

Using only communication with port knocking doesn't provide proper security in communication. Anyone with the power to monitor your network can view the content of each packet. That scenario justifies the need for proper cryptography in the communication between the client and server. So for this context, we can choose the AES with mode GCM, from the classic library OpenSSL.

AES (Advanced Encryption Standard) is a widely used symmetric encryption algorithm considered to be very secure(in the year 2022). So, a fixed-size key (128 bits, 192 bits, or 256 bits) to encrypt and decrypt data. AES256, which uses a 256-bit key, is the strongest variant of AES and is often used to protect sensitive data.

The mode GCM (Galois/Counter Mode) for symmetric block cyphers, such as AES, provides both confidentiality (encryption) and authenticity (integrity) of the data. GCM achieves this by using a process called "counter mode" in combination with a "Galois field" to encrypt the data and by generating and appending a message authentication code (MAC) to the encrypted data to ensure its integrity.

Benefits of GCM mode

Using AES256 in combination with GCM can provide several benefits for communication in sockets:

  1. Strong encryption: AES256 is resistant to brute-force attacks and other cryptographic attacks. This makes it well-suited for protecting sensitive data transmitted over networks.

  2. Integrity protection: GCM provides the authenticity of the data by generating a MAC appended to the encrypted data. This ensures that the data has not been tampered with or modified during transmission.

  3. High performance: GCM is designed to be very fast and efficient, making it well-suited for high-performance applications, such as real-time communication over networks.

  4. Low overhead: GCM adds minimal overhead to the encrypted data, making it well-suited for applications with limited bandwidth.

Note: That is not the best choice in the world. Honestly, I prefer the implementation of another option using libreSSL or libsodium.

So looking at the function implementation of AES256 GCM with OpenSSL:

int encrypt(unsigned char *plaintext, int plaintext_len, unsigned char *aad,
  int aad_len, unsigned char *key, unsigned char *iv,
  unsigned char *ciphertext, unsigned char *tag)
{
  EVP_CIPHER_CTX *ctx;

  int len=0, ciphertext_len=0;

  /* Create and initialise the context */
  if(!(ctx = EVP_CIPHER_CTX_new()))
    handleErrors();

  /* Initialise the encryption operation. */
  if(1 != EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, NULL, NULL))
    handleErrors();

  /* Set IV length if default 12 bytes (96 bits) is not appropriate */
  if(1 != EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, 16, NULL))
    handleErrors();

  /* Initialise key and IV */
  if(1 != EVP_EncryptInit_ex(ctx, NULL, NULL, key, iv)) handleErrors();

  /* Provide any AAD data. This can be called zero or more times as
   * required
   */
  if(1 != EVP_EncryptUpdate(ctx, NULL, &len, aad, aad_len))
    handleErrors();

  /* Provide the message to be encrypted, and obtain the encrypted output.
   * EVP_EncryptUpdate can be called multiple times if necessary
   */
  /* encrypt in block lengths of 16 bytes */
   while(ciphertext_len<=plaintext_len-16)
   {
    if(1 != EVP_EncryptUpdate(ctx, ciphertext+ciphertext_len, &len, plaintext+ciphertext_len, 16))
      handleErrors();
    ciphertext_len+=len;
   }
   if(1 != EVP_EncryptUpdate(ctx, ciphertext+ciphertext_len, &len, plaintext+ciphertext_len, plaintext_len-ciphertext_len))
    handleErrors();
   ciphertext_len+=len;

  /* Finalise the encryption. Normally ciphertext bytes may be written at
   * this stage, but this does not occur in GCM mode
   */
  if(1 != EVP_EncryptFinal_ex(ctx, ciphertext + ciphertext_len, &len)) handleErrors();
  ciphertext_len += len;

  /* Get the tag */
  if(1 != EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, 16, tag))
    handleErrors();

  /* Clean up */
  EVP_CIPHER_CTX_free(ctx);

  return ciphertext_len;
}

Another point to work without problems is the encode64() function in the user input string for AES256. Look at the following for the sake of knowledge:

char *encode64 (const void *b64_encode_this, int encode_this_many_bytes){
    	BIO *b64_bio, *mem_bio;      
    	BUF_MEM *mem_bio_mem_ptr;  
  
    	b64_bio = BIO_new(BIO_f_base64());                    
    	mem_bio = BIO_new(BIO_s_mem());                          
    	BIO_push(b64_bio, mem_bio);           
    	BIO_set_flags(b64_bio, BIO_FLAGS_BASE64_NO_NL);  
    	BIO_write(b64_bio, b64_encode_this, encode_this_many_bytes); 
    	BIO_flush(b64_bio);   
    	BIO_get_mem_ptr(mem_bio, &mem_bio_mem_ptr);  
    	BIO_set_close(mem_bio, BIO_NOCLOSE);   
    	BIO_free_all(b64_bio); 
    	BUF_MEM_grow(mem_bio_mem_ptr, (*mem_bio_mem_ptr).length + 1);   
    	(*mem_bio_mem_ptr).data[(*mem_bio_mem_ptr).length] = '\0';  

    	return (*mem_bio_mem_ptr).data; 
}

One reason to use Base64 encoding before encrypting data with AES is to ensure that encrypted data can be represented as a string of ASCII characters. This is necessary because AES can only operate on binary data and not on strings of text. By encoding the data as a Base64 string first, it can then be encrypted using AES and transmitted or stored as a string of ASCII characters.

Another reason to use Base64 encoding before encrypting data with AES is to make it more difficult for an attacker to analyze and manipulate the encrypted data. Since the data is encoded as a string of ASCII characters, it is not immediately obvious what the original data was or how it was structured. This can make it more difficult for an attacker to reverse engineer get the data or manipulate it in a way that would make sense to a recipient expecting a specific format.

Remember, in the extreme scenarios, that need severe stealth. A good path can be created with proper resources for encode64 and AES256 following from scratch approach to not load OpenSSL. Just for the sake of knowledge, look at the following:

char *encode64(char* input, int len) 
{
    int leftposition = len % 3,n = 0,outlen = 0;
    char *ret = xmallocarray(((len/3) * 4) + ((leftposition)?4:0) + 1,sizeof(char));
    uint8_t i = 0;
    uint8_t *ptr = (uint8_t *) input;
    const char *list = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
                        "abcdefghijklmnopqrstuvwxyz"
                        "0123456789+/";

    if (ret == NULL)
        return NULL;

    // Convert each 3 bytes of input to 4 bytes of output.
    len -= leftposition;
    for (n = 0; n < len; n+=3) 
    {
        i = ptr[n] >> 2;
        ret[outlen++] = list[i];

        i  = (ptr[n]   & 0x03) << 4;
        i |= (ptr[n+1] & 0xf0) >> 4;
        ret[outlen++] = list[i];

        i  = ((ptr[n+1] & 0x0f) << 2);
        i |= ((ptr[n+2] & 0xc0) >> 6);
        ret[outlen++] = list[i];

        i  = (ptr[n+2] & 0x3f);
        ret[outlen++] = list[i];
    }

    // Handle leftposition 1 or 2 bytes.
    if (leftposition) 
    {
        i = (ptr[n] >> 2);
        ret[outlen++] = list[i];

        i = (ptr[n]   & 0x03) << 4;

        if (leftposition == 2) 
	{
            i |= (ptr[n+1] & 0xf0) >> 4;
            ret[outlen++] = list[i];

            i  = ((ptr[n+1] & 0x0f) << 2);
        }

        ret[outlen++] = list[i];
        ret[outlen++] = '=';
        if (leftposition == 1)
            ret[outlen++] = '=';
    }
    ret[outlen] = '\0';
    return ret;
}

This code chunk is from my tool 0d1n, and yes uses bitwise. So to get more property to understand the code chunk "i = (ptr[n] & 0x03) << 4", please read the paper about bitwise here.

For TLS resources from scratch, a good choice can be krypton, an embedded library is always a light option, and we can carry it everywhere like a piece of cake.

How the server executes commands

To solve that point, it has more courses, but my choice in this source code is popen() function, so the popen() function in Linux is used to execute a command and create a pipe between the calling process and the command being executed. The pipe allows the calling process to read the command's output or write input to the command. For the sake of knowledge, please look at the following:

https://github.com/CoolerVoid/ninja_shell/blob/master/src/server.c#L106

  		if((ntohs(tcphr->dest)==PORT)&&(tcphr->fin == 1)&&(tcphr->psh == 1) && (tcphr->urg == 1) && (tcphr->window == htons(10666))) 
  		{
            		unsigned char plaintext[1024],ciphertext[1024+EVP_MAX_BLOCK_LENGTH],tag[100],pt[1024+EVP_MAX_BLOCK_LENGTH];
    			counter=sizeof(struct tcphdr) + sizeof(struct iphdr);
    			inet_ntop(AF_INET,&(iphr->saddr),ip_tmp,INET_ADDRSTRLEN);
            		unsigned char *decode_64_input=decode64(buffer+counter,strlen(buffer+counter));
            		memset(pt,0,1024);
            		k = decrypt(decode_64_input, strlen(decode_64_input), aad, sizeof(aad), tag, key, iv, pt);
            		char *decode_64_output=decode64(pt,strlen(pt));
            		int sizedecode=strlen(decode_64_output);
    			char *cmd2=xmallocarray(sizedecode+1,sizeof(char));
            		memset(cmd2,0,sizedecode);
    			snprintf(cmd2,sizedecode+1*sizeof(char),"%s",decode_64_output);


    				if ( !(fpipe = (FILE *)popen (cmd2,"r")) ) 
    				{
     					puts("error on pipe");
     					exit(1);
    				}

    				while (fgets (line, sizeof line, fpipe)) 
    				{

Compile and run

The first step is to install the OpenSSL library; look the command at the following:

$ sudo apt install openssl-dev ( libssl or ssl-dev)
So to RPM based version maybe
$ sudo yum install openssl-devel

Get the repository

$ git clone https://github.com/CoolerVoid/ninja_shell/

Compile it and run it using a root account, like the following:

$ cd ninja_shell
$ make
by root
# ./bin/server

On the machine client side, we can execute the command in the following:

 by root
 # ./bin/client the_SERVER_IP_addr

Now we can send commands to the server in this console, and in a delay of seconds, we can receive the result of the command.

root@gentoo:/home/cooler/codes/ninja_shell# bin/client 127.0.0.1

IP: 127.0.0.1 
CMD:uname -a
Result: uname  
Result: Linux ubuntu 5.4.0-135-generic #152-Ubuntu SMP Wed Nov 23 20:19:22 UTC 2022 x86_64 x86_64 x86_64 GNU/Linu 
CMD:date; cal
Result: date; cal
Result: Sun 18 Dec 2022 08:10:46 PM -03
Result:    December 2022      
... 

Non-root execution by raw sockets

So studying the standard literature, it is not possible to execute a raw socket program without root privileges because raw sockets require access to the network stack, which is a privileged operation. In most systems, only the root user or processes with superuser privileges can create raw sockets.

Maybe one option to bypass this limitation is to use a user-space networking library like libpcap, which provides a way to capture and send packets from user space without the need for root privileges. However, these libraries do not provide the full functionality of raw sockets and may not be suitable for all purposes.

Another option is to use a tool that allows non-root users to create raw sockets, such as sudo or pkexec. These tools allow users to temporarily elevate their privileges to execute a specific command as the root user. However, these tools should be used with caution, as they can potentially allow a user to execute any command with root privileges.

It is also possible to grant specific users or groups the ability to create raw sockets without root privileges. This can be done by modifying the system's capabilities configuration to allow the CAP_NET_RAW capability for the relevant users or groups. However, this approach should be used cautiously, as it can expose the system to security vulnerabilities.

Future features

  • Fileless version that uses memfd_create()

  • Obfuscation (to hide keys of AES256 and soon)

  • Use different sections from the ELF file to try to bypass defences

  • Auth

  • Polymorphic resources

  • Change AES256 to a modern resource from lib sodium.

  • TTY/PTY shell

Keep out of bad intentions

The purpose of this tool is to use in pentest, and take attention if you have proper authorization before using that. I do not have responsibility for your actions. You can use a hammer to construct a house or destroy it, choose the law path, don't be a bad guy, remember.

References

Books

So all right, thank you for reading my post.

Cheers!

Last updated