Revision

Revision Date Notes
v1.0 31/05/20002 First revision of the document

Disclaimer

I'm not going to teach you IDA, x86 assembler or Linux syscall conventions here. Those are a must if you want to follow this humble tutorial.

First things first

The first thing to do (or maybe the first thing I do) is try to get a feeling of the binary. This includes looking at strings contained in the binary, output of nm, ldd, file, objdump and maybe hexdump. What can we say about the target in this case? file tells us that the-binary is:
ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, stripped
ldd confirms that it's not a dynamic executable. Probably it is common for binaries destined to run on unknown systems to be statically linked so that it doesn't depend on any particular version of libraries installed on target systems. It also makes the analysis harder (or maybe not harder but simply a little bit longer). To finish this initial look let's read what strings can tell us:
[mingetty]
/tmp/.hj237349
/bin/csh -f -c "%s" 1> %s 2>&1
TfOjG
/bin/csh -f -c "%s"
hmm, surely the most interesting strings :-) Looks like some kind of "secret" file in /tmp, execution of shell, a password (TfOjG). Disassembling should provide more information about those strings.

Next group of strings are "standard" strings contained in binaries statically compiled with libc and libresolv, e.g.:

@(#) The Linux C library 5.3.12
yplib.c,v 2.6 1994/05/27 14:34:43 swen Exp
Those strings might have been put just to mislead the reverser. How can you be sure if the binary is really C code compiled with libc? Experience and disassembly should give the answer :-)

The first command I ran on the-binary was 'file'. It tells you that the-binary is stripped. Maybe we can "dress" it homehow? ;-) A new tool was developed by lcamtuf as a part of fenris package which does just that: dress an unknown binary in symbols. It tries to recognize functions contained in an unknown executable and stores the known ones in ELF symbol table. Let's try 'dress' on the-binary:

$ dress -F fnprints.dat -F support/fn-libc5.dat -F support/fn-2.0.7.dat the-binary the-binary-dressed

[snip]
[+] All set. Detected fingerprints for 214 of 371 functions.
Wow! 214 functions were actually detected and named! This should help disassembly a lot.

Actual disassembly

I've used the freeware IDA version available on www.datarescue.com together with ELF loader provided especially for the challenge. Unfortunately the freeware version has console interface only and it doesn't run under WINE (at least on current Debian sid) so you have to have access to a Windows machine (or run Windows under bochs/VMWare/Win4Lin/whatever emulator you prefer). Load the-binary-dressed into IDA and admire the good work done by dress :-) We can see the initial startup (start procedure). The experience (and the resemblance of libc startup code) tells me that the next call (sub_0_8048134) after _init_proc should be the main. After this call eax is pushed onto the stack and another function is called. The eax contains the value returned by main, so this call, although not fingerprinted by dress, should be the exit function. Name it '_exit' and let's have a look at main.

You can see many local variables, initial assigments and a call to geteuid. If euid is not 0 the-binary exits. Otherwise it proceeds. So the first thing you can learn it has to run as root, otherwise it exits without doing any harm. But is it really true? dress had fingerprinted standard functions but do you really know what's going inside those functions? Maybe the main we're looking at is just a fake and the real functionality lies inside functions we've just skipped? To check this possibility I've setup a UML (User Mode Linux) ) jail and ran the-binary as an unprivileged user under strace. Here's the output:

execve("./the-binary", ["./the-binary"], [/* 38 vars */]) = 0
personality(PER_LINUX)                  = 0
geteuid()                               = 1000
_exit(-1)                               = ?
Hmm, it looks sane :-) Probably we're not delving into a fake main function. UML can help enormously with further analysis, as you can safely run the-binary as root inside the UML (but remember about the 'jail' option in UML). You wouldn't want to run it as root on your own system, would you? ;-) (unless you've got a dedicated sandbox machine where you run all those unknown binaries).

After the binary knows it's running with euid=0 it tries to disguise in the system - first it sets argv[0] to all zeros and then copies the "[mingetty]" string onto its argv[0]. Simple ps would be fooled by this trick and unexperienced sysadmin could overlook this process.

Then it ignores SIGCHLD, forks, exits from parent process, calls setsid() and ignores SIGCHLD in the first child, forks again, exits from the first child. The second child is now the "main" process which will exist through the whole life of the-binary in the system. These 2 forks, ignoring SIGCHLD and calling setsid() is a typical "daemonization" sequence. However, as lcamtuf noted, those two fast consecutive forks could fool ptrace-based tools, as strace or fenris. The reason is flawed ptrace interface which does not provide a reliable way to follow forks or vforks in traced process.

After those 2 forks process closes stdin, stdout, stderr and changes current dir to "/". Then time is called and the result is passed on to next function, which unfortunately wasn't fingerprinted by dress. The function seems complicated and pretty cryptic. That's the point where experience comes in ;-) time is very often used as a seed to random number generator in C programs. The function called uses 12345 constant in several places. Could it be srand()? A quick check (compiling a simple program that does srand(time(NULL)) using gcc 2.7.2 and statically linked libc5) gives positive result :-)

Next called function also isn't fingerprinted however passed arguments look a little bit familiar - 2,3,11. Looking at the body of that function I've found out it's a wrapper for socket syscall (don't believe IDA, int 0x80 is _not_ reserved for BASIC ;-) EDX == 1 is socket() call (see /usr/src/linux/include/linux/net.h). The arguments passed are socket(PF_INET(2), SOCK_RAW(3), 11). So this call creates a raw socket which will receive any packets which have protocol field in IP header set to 11. Probably this is the mechanism used to control the-binary from the outer world.

After creating raw socket SIGHUP, SIGTERM and SIGCHLD are ignored. SIGCHLD is ignored 2 times for unknown reasons. Maybe the author wanted to ignore both SIGCLD and SIGCHLD ;-)

Below another non-fingerprinted function is called. A quick look at this function, consulting net.h and a name can be given: recv(). Using this knowledge we can name var_800 to something more meaningful, e.g. packet. This is the buffer which will hold received packet. A nice thing to have is also IP header structure which will give a better "feeling" of the code that will operate on the packet.

After receiving the packet there is a rather redundant check if the received packet has really protocol field set to 11. The first byte of the payload has to be 2 and the total packet length has to be grater than 200 - otherwise the-binary will reject the packet. If those checks are passed a function is called with a pointer to the 3rd byte of payload, length subtracted by 22 and an unknown pointer. I've deducted from that that the first two bytes of the payload are used as a marker. Actually only the first byte is checked (it has to contain 2).

Looking at the function called we see it only calls sprintf and does some magic on the both pointers passed as arguments. I won't go in detail here how to reverse this function: it's short and after half an hour it doesn't as cryptic as it seem. I've constructed C code, which compiled using gcc 2.7.2 looks astonishigly similar to our unknown function:

void decode(uchar *decoded, uchar *data, int len)
{
        int i = len-1, j, k;
        uchar tmp[len];
        uchar c;

        decoded[0] = zero;
        while (i >= 0) {
                j = i-1;
                if (i) c = data[i]-data[j]; else c = data[0];
                k = c - 0x17;
                while (k < 0) k += 0x100;
                j = 0;
                while (j < len) { tmp[j] = decoded[j]; j++; } // **
                decoded[0] = k;
                j = 1;
                while (j < len) { decoded[j] = tmp[j-1]; j++; }
                sprintf(decoded, "%c%s", k, tmp);
                i--;
        }
}
As you can see it simply decrypts "data" into "decoded". ("Simply" is maybe not a good word here as the 4 lines below ** do exactly the same thing as sprintf below, so this function can be expressed more cleanly) The encryption algorithm is simple: the first byte of data is incremented by 0x17 and the other bytes are processed from left to right (from the 2nd byte till the last one), incrementing every byte by the previous byte and adding 0x17 to the result. So renaming var_44e0 in main to decodedptr and sub_0_804A1E8 to decode would be a good idea ;-)

After the decryption the second byte of decrypted data is taken as an argument to switch statement. It looks like our target has 12 commands (numbered 1-13) and they're recognized right here.

Command no 1

First it sets up some strange values (both constant and variable) in packet and calls sub_0_804A194(decodedptr, &packet, 400). Looking at 804A194 leaves no doubt: it's encoding function, symmetric to decode routine. C code developed by me looks like this:
void encode(uchar *encoded, uchar *data, int len)
{
        int i;

        encoded[0] = zero;
        sprintf(encoded, "%c", data[0]+0x17);
        for (i = 1; i < len; i++)
                encoded[i] = data[i]+encoded[i-1]+0x17;
}
(compiled version isn't as close to the original as decode, but it's close enough :-) Having encrypted 400 bytes (unfortunately author used "decoded" table to hold encrypted data so it makes a little mess) it calls sub_0_8056058 which in turn calls sub_0_8055e38. Looking at the latter we can find already known constant of 12345. Could it be somehow related to random()? Compiling short program which uses rand() and random() reveals that the guess was true: rand() does nothing but calling __random(). Renaming sub_0_8056058 to rand() and sub_0_8055e38 to __random() gives a clear look on what's going on next: the-binary takes the remainder from division by 201 and add 400 to the result. Then sub_0_8048ecc(var_44e4, decodedptr, 400+rand()%200) is called. var_44e4 is not yet known, decodeptr holds the enrypted data. Looking at sub_0_8048ecc you can see that it calls sub_0_8048f94 either once or in a loop. Let's delve into sub_0_8048f94 in order to understand the outside function.

First it creates a raw socket then allocates arg_C+23 bytes of data. Then some memory is moved back and forth and almost at the end a function is called which was fingerprinted as recfrom__sendto (it's a shame dress couldn't recognize between those two ;-). After checking socketcall number you can rename it to sendto :-) Quick check in man page allows to rename var_10 to to sockaddr (and making appropriate sockaddr_in structure) and arg_C to len. After some polishing (telling IDA to rename anonymous number offsets into symbolic names pointing into ipheader or sockaddr_in) the code is now self-commenting :-) To achieve the same effect as in sample IDB base use XREF function exetensively until you name all of the local variables and arguments and change magic numbers into symbols constants and offsets.

I won't explore sub_0_8049138 here as it's very simple and dress has done 90% of the hard work here :-) I will just allow myself to comment on poor skills of the author who uses sprintf(buf, "%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]) and then gethostbyname(buf) to convert char ip[4] into int ;-) Summing up sub_0_8048f94: it creates socket, calls malloc, sets up IP header, copies payload passed as argument (to be exact pointer to payload is passed as an argument) into the packet's payload but at offset 2, reserving first two bytes supposedly for packet signature (only the first byte is set to 3, second is left unmodified after malloc) and finally it sends it. Source and destination address (again pointers to addresses) are passed as arguments. I've named this function send_raw_packet.

(Short digression here: it takes some effort to track all the parameters from already known functions in order to understand the "outer" routines. Full versions of IDA do this automatically for you :-) Such feature speeds up reverse-engineering a lot, as most functions which actually do anything useful have to call system function once and a while. Identyfying used structures and naming variables some sane names gives a much better understanding of the code).

XREFs to send_raw_packet show that it's called only in sub_0_8048ecc. Armed with knowledge about send_raw_packet we can analyse sub_0_8048ecc. If dword_0_807e784 is 0 it only sends only one packet from address stored in byte_0_807e780 to address pointed by arg_0. Pointer to payload is passed as arg_4 and payload's length is stored in arg_8. If dword_0_807e784 is not 0 send_raw_packet is called 10 times, each time dstaddrptr is incremented by 4, so the packet is sent to 10 addresses.

Again looking at cross-references to global variables used in this function (I've called it send_to_all) shows us that all of them are set in main, in the second case of the switch statement.

Now back to main. After this journey into send_to_all it is easy to discover that var_44e4 is pointer to address table (addrtabptr). In the very beginning of main it's initialized to point to var_11b8 and it's never changed again. So var_11b8 is the address table (addrtab). Contents of the packet sent is some magic values and if some variable is non-zero the value of another variable is stored in the packet. Those two variables are unknown now but after finishing this tutorial you'll find all of the variables named and the intention of the author will be clear :-)

Command no 2

In first instruction you can see our old friends - dword_0_807e78 and byte_0_807e780. The first one (I've renamed it to cmd2_param) is set to the value of the third byte of decrypted contents (the first 2 are probably reserved for command number, though only 2nd byte is used), and byte_0_807e780 (and the following 3 bytes) are set to the destination address of the packet just received. It means that the-binary does not try to learn the address of the host it's running by calling any system calls or looking through configuration files - it simply gets this address from packets it's receiving.

Next it fills internal address table (you've seen it already, used in the first command, will be also used again) using contents of the packet. First a random number in range [0..9] is chosen. It is stored in EDI. EBX is current index into the internal address table and iterates from 0 to 9 inclusive. If those 2 registers are equal nothing is done - this entry is skipped. If they're different position in address table pointed by EBX is filled either with IP address from the received packet (indexed again by EBX) - if cmd2_param is 2 - or random IP address (4 calls to rand()) otherwise.

After the whole loop the first address from the packet is copied to the position indexed by EDI (the random number chosen) if cmd2_param is non-zero or to the first position if cmd2_param is 0.

This rather messy (both in implementation and my explanation ;-) was developed in my opinion to disguise real IP address controlling the-binary. Attacker can provide other fake addresses or rely on the-binary to choose some random ones. In testing environment it's also possible to register only one (cmd2_param == 0) controller to avoid generating unnecessary network traffic.

I've named this command "register_controller" because it registers controller address(es) in the-binary allowing attacker to use "status" (and maybe other?) command.

Command no 3

It forks and stores the value (eax) returned by fork in dword_807e770. Note though it checks if eax is zero _after_ storing it in memory. It leaves some space for race condition and actually the value stored in dword_807e770 can be either 0 or PID of the child. It depends on the scheduling algorithm used in the kernel (preferring parent or children) and on the count of processors present in the system (is it UP or SMP) (probably there are more factors involved but these 2 or most obvious). XREF shows this variable is used throughout the code so this bug could have some impact later.

I've renamed dword_807e770 to child_pid and looked at crossreferences. It is used only in two commands: this one and number 7. Looking at those commands you can see they have much in common (similar structure and strings used). child_pid is used to kill some process. Remember that in some circumstances child_pid could be 0, so could we possibly trick this command to kill the-binary (kill(0) kills all the processes in the group the process issuing kill belongs to)? Unfortunately not, because before the-binary calls kill it calls setsid. Hopefully we'll find some other way later ;-)

Looking below the first fork you can see that the parent returns to the main loop and child forks again. The first forked child calls sub_0_80556CC(10) and then kills child_pid with SIGKILL.

After inspection of pretty complicated sub_0_80556CC and recalling libc sources (yes, to reverse engineer you have to read libc sources. Not a very amusing friday evening filler though ;-) you can simply name it 'sleep'. Now it's clear: the 1st child is a 'guard' for the 2nd child. If the 2nd one executes more than 10 seconds it gets killed and 1st one exits too. So what does the 2nd one do? First it gets everything what's after the command number in the decoded packet, sprintfs it into a buffer using this format string:

/bin/csh -f -c "%s" 1> %s 2>&1
Looks suspicious, ha? The second string (used to fill 2nd %s) is even more interesting:
/tmp/.hj237349
Then the whole buffer is passed to sub_0_80557E8. What can it be, hmm... Yes, you've guessed it! It has to be system call. A glance at the function body at you can safely rename it to system. Now you've got all pieces of a puzzle. After system is finished, this mysterious .hj237349 file is opened, it contents read and sent to all registered controllers. Output is sent in 398 byte chunks (first 2 bytes as usual for some kind of signature). Note the technique employed by the author to mark the end of the output. If fread returns positive value (which means it in fact read any bytes) the packet has signature set to 3. If fread returns 0 (meaning EOF) packet is marked with signature 4 and the contents is exact copy of the previous packet (because fread does not touch the buffer). After the whole file has been read the file is closed and unlinked.

As you can see the-binary allows the attacker to issue a command and see it's output. However it will fail if no csh is present on the system. I don't have any statistics here handy which distros have csh as a default (my Debian/sid certainly doesn't ;-) but I would take a guess and say it's not unprobable if the csh was missing ;-)

Command no 4

Not much in here: checks if dword_0_807E774 is zero it forks, copies some data from decoded data and calls sub_0_8049174. The PID of forked child is stored in the same dword, so I've named it child2_pid. If we've already running child simply conrinues. After the soubroutine is finished child exits. It's the first case in the switch statement which only push some arguments on the stack and call a subroutine to perform the real processing. I'll leave them for now and look at the commands which really get processed in the main function.

Command no 6

First you can see standard fork sequence. The only new bit is dword_0_807E77, which holds 6 from now on. References to this variable show only small integers being stored there. Hmm, after a quick glance at other places where dword_0_807E77 is used you can easily see it's the last command issued. It's set only in certain commands and read only in 1st command. It was the status command. Now you can see that status send to the attacker information which command was last issued.

The parent thread of execution after fork as usually goes back to receiving more packets and the childs ignores a bunch of signals (including SIGCHLD twice, anybody knows what can it be?) and creating an ordinary SOCK_STREAM socket (not RAW this time). Another thing which can draw reverser attention is a strange constant 0xf15a. Analysing setsockopt arguments you can easily reveal that the-binary is setting SO_REUSEADDR on the newly created socket.

Below you can find two unexplored subroutines which turn out to be bind and listen respectively. Now it's time to recall listen and bind arguments. Having done so it's no surprise that var_11c8 is sockaddr_in structure which describes address and port we're binding to. Now it's clear what 0xf15a is: it's the port we're binding to (it's network byte order, so in fact it's 0x05a1f). After binding a typical network server does accept() and the-binary is no exception - rename sub_0_8056A2C to accept, var_11d8 to sockaddr and var_44cc to accsockfd. After accepting a connection the-binary forks again. Parent loops and tries to accept another connection and the child processes incoming connection (so this command can easily be used as a local DoS). At most 19 bytes are read, and crypted until the first \r or \n character. Crypting here means adding 1 to every byte ;-) Encrypted input is compared with TfOjG. (Devising a plaintext from "TfOjG" ciphertext is left as an excerise to the reader ;-) If they're identical a shell is spawned (surprise, surprise) with stdin, stdout and stderr bound to incoming connection. This time /bin/sh is used instead of /bin/csh. If the password is not correct some garbage (0x1fbff) is sent to the poor h4x0r and the process ends.

Command no 7

Very, very similar to 3rd command. This time however no output is recorded and the timeout after the child executing shell is killed is set much higher: to 1200 seconds. As XREF shows child_pid is used only in 3rd & 7th command so maybe a more useful name for this variable could be shell_pid.

Commands no 5, 9,10,11 & 12 are similar to command no 4. I will describe only 4th command, the rest is similiar and in my opinion even easier. They have similar structure and technique used to reverse engineer them is the same: identify all of the structures and arguments used in syscalls, give some same names to local variables and arguments and with this power in hand analyse the execution flow making educated guesses and comment thoroughly. It's worth to note that command 4 & 9 and 10 & 11 call the same subroutine. The only difference is in one parameter which is set to 0 in commands 4 & 10, and is taken from the received packet in command 9 & 11. I have no idea why there are two almost identical commands. Maybe it's an early beta version?

Command 4 (again)

Some memory is copied to local variables, another variables are set to their initial values and arg_10 is decremented if it's not 0. Then a raw socket is created. Created socket is used at the end of the routine in sendto call.

Basing on previous experience var_228 becomes addr (of type sockaddr_in) and var_638 becomes packet (of type ip_packet). var_24 seems to be a table of lengths (indexed by edi). Value from this table is incremented by 28 (hmm, 20 is IP header, 8 is UDP header length, could it be a UDP packet?). In XREFs you can see that packet is pointed by esi throughout the function. So find all the places where esi is used as an index register and change the numerical offsets into symbolic ones into ip_header structure (hint for IDA developers: this could be automated ;-). Now you can easily see that the packet the-binary is constructing is indeed a UDP one :-), so var_624 is in fact a udp_header structure and var_65c becomes udpheadptr. XREFing this pointer you can see how the UDP header is constructed - destination port is set to 0x3500 which in host byte order becomes 53 decimal...

Yes, it's a DNS attack! Not sure now what exactly kind of attack but you're certainly moving in the right direction. As a side-effect of this little discovery is knowledge about two arguments: arg_14 is the LSB of source port and arg_18 is MSB. Now var_61c can be renamed to dnshead and var_660 to dnsheadptr. XREFing the latter you can see how the DNS queries are actually set up. The routine takes the length of the query from previously identified lengths table (of dwords, indexed by edi) and copies that many bytes from var_218 indexed by var_670 into DNS packet. What is var_670? To answer this question you have to go back to the very beginning of the function. 125 dwords from word_0_80676BC are copied into var_670. Looking at word_0_80676BC you can quickly realize they're actual DNS queries :-) Rename it to some sane name (like dns_queries) and take a good look at them. There is total of 9 queries. They look up SOA of com. net. de. edu. org. usc.edu. es. gr. ie. respectively. However all of the two-letter queries (i.e. for de. es. gr. and ie.) are buggy! The author has used a popular programming technology called 'cut-and-paste' but failed to properly test his masterpiece (or she didn't understand at all what this weird DNS is about). Length marker in front of strings should be definetely 2, not 3. All of DNS servers (haven't tried _that_ many, I admit) should respond with an error to such a query. Just above the copying of queries you can see that the routine is making a local copy of queries lengths.

Now almost all elements of this simple puzzle are coming together. The only thing not yet discovered are source and destination addresses of IP packets sent. Looking through the function body (or using text search if you've correctly spotted all esi occurences) you can spot 2 occurences of srcaddr.

The first one is just after gethostbyname. It is called with arg_20 as a parameter. To understand this snippet you have to look a little bit above gethostbyname: the lookup is performed only when arg_1C is 1 and var_668 is zero or less. If the lookup succeeded (returned non-NULL) var_63c (renamed to resolvedaddr) is filled with the first returned IP address. Then this address is copied to its proper place in IP header. If gethostbyname returned NULL the process sleeps for 10 minutes and tries resolve it in infinite loop.

The second one is just after setting up of UDP header. It checks if arg_1C is 0. If it is 4 local variables are used to set up source address. arg_1C seems to be a some kind of flag. Checking how those 4 local variables are set up gives the final answer: arg_1C == 0 means the source address is given as an IP number and arg_1C != 0 means there is a hostname to resolve.

Destination address is set in 2 places: in the IP header and in the sockaddr_in structure. Both are set (surprise, surprise) to the same value taken from the location pointed by var_66c. This variable points to some element inside dword_0_806D22C table. Seems like the-binary is holding a table of 11418 DNS servers which are used as reflectors in this attack.

Now all of the pieces are in their places and it's to easy to follow execution flow. First local variables are initialized and a local copy of DNS queries and their lengths is made. A raw socket is created (in case of a failure the routine ends) and the buffer which will hold a DNS packet is zeroed.

Then loc_0_8049250 is reached which is referenced from the code below (as you can see by the arrow pointing down) which probably indicated a loop. The first reference points to a small loop in which given hostname (if any) is resolved (I've already covered this snippet). The second one is way down, almost at the of the routine. As you can see the epilogue can only be reached if creation of a raw socket fails so the jump to loc_0_8049250 makes an infinite loop.

loc_0_80492C4 seems to be another loop. Looking at the condition at loc_0_8049530 it's easy to deduct that this one interates over all of the 9 DNS queries hadrcoded in the-binary. This loops starts with choosing a random number in range [0..7999] (and storing it in EDX) if var_654 is 1. It's initialized to 1 outside all loops and set to 0 after positive check. If var_654 is already 0 EDX is set 0. var_654 is not modified anywhere else so it's a flag variable which indicates if the code executes first iteration. After the first iteration flag is cleared.

EDX is then used as and index to dns_servers table which holds the IP addresses of DNS servers. If we accidentaly found a 0 on the list the query pointer is dvanced to the next query and the outer loop continues. Howver with more than 11000 positions in the table it's not possible to find a 0 in the first 8000 so this check is rather useless.

Now execution reaches loc_0_8049308 which is the most inner loop found in this routine. This one iterates over DNS servers on the list until 0 is reached. If it is the pointer is advanced and the next query is used. Inside the loop the packet is prepared: actual query copied, random DNS query id chosen, a random UDP source port (in range [0..29999]) is chosen if none given (to be exact: if 0 given), UDP destination port set to 53, length computed (based on queries' length table), source and destination address filled, UDP checksum zeroed (UDP packets are not requiered to have checksum computed, 0 means no checksum), proper value in IP version and header length set, random TTL chosen (120+random()%130), random IP ID (in range [0..254]) chosen, IP protocol set to UDP, fragmentation and flags set to 0 and finally IP checksum computed. Then the packet is sent and complex formulaes are evaluated in order to determine if it's time to sleep ;-) The code generated here was optimized with gcc so I won't try to describe how it works with natural language, I'll use C for that matter:

if (arg_10 == 0) {
  usleep(300);
  var_668--;
} else if (arg_10 == var_664) {
  usleep(300);
  var_664 = 0;
  var_668--;
} else var_664++;
var_664 seems to count how many packets was sent after last sleep and arg_10 controls how many packets have to be sent before sleeping. If arg_10 is 0 usleep is called after every packet avd var_664 is not used. If arg_10 is positive the-binary calls usleep every arg_10-th packet. The only mystery here is var_668. After looking at XREFs it becomes less mysterious: var_668 is set to 40000 after a successful hostname lookup and decremented after every usleep. It's value is taken only once: in the most outer loop before gethostbyname. If var_668 is 0 or less the hostname is resolved, otherwise previously acquired address is used. With this mechanism attacker can control how often the hostname is resolved. Setting arg_10 to a big value (remember though it's only in range [0..255] because it's a single byte taken from the received packet) makes the-binary resolve the hostname only once, whereas small value resolves the hostname every pass or two. I've renamed arg_10 to sleep_after, var_668 to slept_after_sendto and var_664 to pack_sent.

After this analysis you can clearly see it's a DNS reflector attack. It uses a hard-coded list of DNS servers which serve as reflectors and a hard-coded list of 9 queries used in the attack. 4 of the queries are buggy and don't trigger the effect of bandwidth amplification but the rest are properly constructed DNS queries and the response sent to the victim is almost twice the size of request.