Basic Command and Control
Building Basic Command and Control Server
In order to understand how c2 servers work, I decided to build my on c2 in order to understand the internals of command and control servers. After a few weeks of researching and coding, I came up with a minimal basic c2 and implant.
Goal
Build a working implant in C that connects to a Linux server over TLS and executes remote commands securely.
Please note, Basic-C2 is purely for educational purposes and is not designed for real-world engagements. It’s a learning tool, and its basic functionality reflects that.
You can find the command and control server and implant here
Overview
Server Side
The server is really simple. I wrote it for Linux . It sets up a secure TLS listener, accepts incoming connections, and spawns a thread for each client. From there:
- Type command
- It sends it to implant via TLS
- The implant executes it
- It sends back the output
The c2 server also has some basic logging functionality.
Thanks to OpenSSL’s abstrations things are a bit simple to make the server use TLS communications
The shell logic reads commands from the operator and forwards them via SSL_write()
. The client reads them, runs them, and returns the output.
This is super basic, but it works.
Client Side (Implant)
I wrote the implant for the windows operating system, the implant basically resembles a backdoor (or a basic RAT). the implant uses Schannel API for encrypted TLS communication. Schannel was really hard to deal with, it took me days to get around. How the implant works:
- Try to connect to c2 server
- Receive and execute commands using
_popen
- Send output back to c2 server
Unlike OpenSSL, Schannel doesn’t abstract everything. You manually manage the encryption handshake, message buffers, and stream decryption.
This gives more control — and more complexity. But it avoids shipping OpenSSL with the implant, keeping it lightweight and native.
Details
Server
Socket Programming
The server uses socket programming with SSL/TLS to communicate with implants.
The server first creates a socket
1
2
3
4
5
serverSock = socket(AF_INET, SOCK_STREAM, 0);
if (serverSock == -1) {
perror("Socket creation failed");
return -1;
}
Then binds address: It binds the socket and address structure together. The address structure contains the following:
- IP Address
- Port
- Transport Protocol (TCP)
- IP version (4)
1
2
3
4
5
if (bind(serverSock, (struct sockaddr*)&addr, sizeof(addr))) {
perror("binding failed");
close(serverSock);
return -1;
}
The server then listens for incoming connections
1
2
3
4
5
if (listen(serverSock, 20) == -1) {
perror("Listen Failed");
close(serverSock);
return -1;
}
Another function accepts connections (ONLY SSL/TLS).
The function first ssl certs if they do not exits from a function in the certifications.h
header and then it loads them into the program.
1
2
3
4
5
6
if (access("certs/cert.pem", F_OK) != 0 && access("certs/key.pem", F_OK) != 0) {
generate_key_and_cert();
}
SSL_CTX_use_certificate_file(ctx, "certs/cert.pem", SSL_FILETYPE_PEM);
SSL_CTX_use_PrivateKey_file(ctx, "certs/key.pem", SSL_FILETYPE_PEM);
The server is setup that it will count down from 10 and accept connections in that time and if there is a connection the timer resets, if there are connections the program performs a TLS handshake and accepts connection. Here is how i implanted it
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
int duration = 10; // default duration is 10 seconds
start:
do {
memset((void*)&client_addr, 0, len);
memset((void*)&agentSock, 0, sizeof(agentSock));
printf("[-] Waiting for connections\n");
// add log "Waiting for connections"
// wait
fd_set read_fds;
struct timeval timeout;
FD_ZERO(&read_fds);
FD_SET(serverSock, &read_fds);
timeout.tv_sec = duration;
timeout.tv_usec = 0;
// Wait for a connection or timeout
int activity = select(serverSock + 1, &read_fds, NULL, NULL, &timeout);
if (activity < 0) {
perror("select error");
sleep(1);
goto start;
}
if (activity == 0) {
// Timeout occurred, no connection
printf("[-] No connection within %d seconds. Continuing...\n", duration);
sleep(1);
break;
} else {
// Connection is available
if (FD_ISSET(serverSock, &read_fds)) {
agentSock = accept(serverSock, (struct sockaddr*)&client_addr, &len);
if (agentSock == -1) {
perror("Accept Failed");
return -1;
}
SSL *ssl = SSL_new(ctx);
SSL_set_fd(ssl, agentSock);
// perform tls handshake
if (SSL_accept(ssl) <= 0) {
perror("TLS handshake Failed");
ERR_print_errors_fp(stderr);
SSL_free(ssl);
close(agentSock);
return -1;
}
After accepting connections we increment the connections counter variable and connection to a list (this is all logged from log.h
).
1
2
3
4
5
6
7
8
9
10
11
12
char ip[buffer_len];
strncpy(ip, inet_ntoa(client_addr.sin_addr), sizeof(ip));
connections_counter++;
printf("[+] Connection from %s | shell %d \n", ip, connections_counter);
log_connections(ip);
addAgent(agentSock, ip, ssl);
sleep(1);
continue;
}
}
} while (true);
Shell
Why I built this
I was curious with how red-team command and control work under the hood. I wanted to understand the lower-level mechanics. Writing this from scratch in C — and with native APIs — gave me deep insight into how TLS, sockets, and remote execution work under the hood.
This is for educational use only.