vuln.sg  

vuln.sg Vulnerability Research Advisory

Lhaca LHA Extended Header Handling Buffer Overflow Vulnerability

by Tan Chew Keong
Release Date: 2007-07-01

   [en] [jp]

Summary

An exploitable buffer overflow has been confirmed in Lhaca. When exploited, the vulnerability allows execution of arbitrary code when the user double-clicks on a malicious LZH archive in Windows Explorer.


Tested Version

  • Lhaca version 1.21 (Japanese)

  • Details

    On 25 June 2007, Symantec Security Response released information of Trojan.Lhdropper, which installs through a strcpy()-based stack-based buffer overflow vulnerability in Lhaca version 1.20. On 26 June 2007, Lhaca version 1.21 was released. According to Lhaca's website, version 1.21 fixes a buffer overflow caused by the use of strcpy().

    However, a binary analysis of Lhaca version 1.21 reveals that it is still exploitable via a stack-based buffer overflow due to the unsafe handling of the LHA "Extended Header Size" value that was read from an LZH file. In particular, Lhaca does not sufficiently validate the size of that value before using it to copy the indicated number of bytes of "Extended Header Data" into a stack-based buffer.

    This causes a buffer overflow when the value of "Extended Header Size" is larger than 255. It is possible to overwrite the saved EIP and gain execution control by setting the "Extended Header Size" to be 0x412 (1042) bytes or more via a specially-crafted LZH file. In addition, an incorrect use of strncat() can also be exploited to further overflow that buffer. Arbitrary code execution using the vulnerability has been confirmed on WinXP SP2 and on Win2K SP4.

    In order to exploit this vulnerability successfully, the user must be convinced to open a malicious LZH file.


    The buffer overflow occurs in a function that resembles the following in Lhaca.exe.

    
    function_40D974(FILE *fp, char *outbuffer)
    {
    	char var_1408[4096];
    	char overflowedBuffer[255];		//var_408
    	char var_309[769];
    	DWORD var_8;
    	DWORD var_4;
    	
    	
    	var_4 = 0;
    	memset(outbuffer, 0, 0x12c);		// clear 300-byte buffer
    	...
    	...
    	fseek(fp, 1, SEEK_SET);
    	...
    	...
    	if(fread(&var_1408[1], 1, 0x24, fp) < 0x24)
    	{
    		// exit with error "Invalid header (LHarc file ?)"
    	}
    	
    	globalReadBufPtr = &var_1408[20];		// header-level byte position
    	outbuffer[0x15] = *globalReadBufPtr;
    	
    	globalReadBufPtr++;
    	
    	if(outbuffer[0x15] != 2)		// check header level
    	{	
    		if(fread(&var_1408[0x25], 1, 2, fp) < 2)	// read length of extended header
    		{
    			// exit with error "Invalid header (LHarc file ?)"
    		}
    	}
    	
    	if(outbuffer[0x15] >= 3)
    	{
    		// exit with error "Unknown level header"
    	}
    	
    	globalReadBufPtr = &var_1408[2];
    	
    	outbuffer[0] = 0x25;
    	memcpy(&outbuffer[1], &var_1408[2], 5);
    	...
    	...
    	...
    	// globalReadBufPtr points within the var_1408 buffer and currently points 
    	// to the filename read from the LZH header.
    	40DAB9_copy_filename_in_lzh_header_to_outbuffer(&outbuffer[0x16], 
    	                                                globalReadBufPtr, 
    	                                                length_of_filename);
    	...
    	...
    	...
    	while(...)		// loc_40DC67 (loop that reads all headers)
    	{
    		// globalReadBufPtr points within the var_1408 buffer and currently points to
    		// the next Extended Header Size field (two-bytes) that was read from the LZH file. 
    		// The getNextExtendedHeaderSize() function will increment the "globalReadBufPtr" 
    		// pointer by two bytes.
    		
    		hdrSize = getNextExtendedHeaderSize(globalReadBufPtr);
    		
    		// stop processing if the next header size is 0
    		if(hdrSize == 0)
    			break;
    			
    		if(outbuffer[0x15] != 2)		// header level
    		{
    			// This code checks that there is enough space to read header. 
    			// The result of overflowedBuffer - globalReadBufPtr can be up 
    			// to 0xFD9 (4057 bytes).
    			// So up the 4057 bytes can be read in this buffer. i.e. "hdrSize" 
    			// can be up to 0xFD9. 
    			// Note that globalReadBufPtr points within var_1408.
    			
    			if(overflowedBuffer - globalReadBufPtr >= hdrSize)
    			{
    				if(fread(globalReadBufPtr, 1, hdrSize, fp) != hdrSize)
    				{
    					// exit with error "Invalid header (LHa file ?)"
    				}
    				
    				switch(*globalReadBufPtr)
    				{
    					globalReadBufPtr++;
    					
    					case 40:	// MS-DOS attribute header : 0x40
    					{
    						...
    					}
    					
    					case 2:		// Directory name header : 0x02
    					{
    					// BUFFER OVERFLOW.
    					// (hdrSize - 3)-bytes of directory name in the 
    					// extended header is copied from the buffer
    					// pointed to by globalReadBufPtr into
    					// overflowedBuffer.
    					
    					// From above, it should be noted that up to
    					// 0xFD9 (4057) bytes can be read into the buffer
    					// pointed to by globalReadBufPtr. i.e. hdrSize
    					// can be up to 0xFD9.
    						
    					// The size of overflowedBufer is only 0xFF (255).
    					// Note that there is another buffer below
    					// overflowedBuffer, so more than 0x408 (1032)
    					// bytes must be copied into overflowedBuffer to
    					// overwrite the saved EIP. This is possible since
    					// 4057 bytes can be read into globalReadBufPtr.
    					
    					if(hdrSize-3 > 0)
    					{
    						copyloop(overflowedBuffer, globalReadBufPtr, 
    							hdrSize-3);
    					}
    					...
    					}
    					...
    				}
    			}
    			else
    			{
    				// exit with error "Invalid header (LHa file ?)"
    			}
    		}
    	}
    	
    	if(outbuffer[0x15] != 2)		// header level
    	{
    		...
    	}
    	
    	// &outbuffer[0x16] points to the filename read from the LZH header
    	// WRONG USE OF strncat() !!!!
    	// "overflowedBuffer" already contains the directory name copied from
    	// the extended header. So fixing strncat()'s length argument at
    	// 0xFF allows more bytes to be appended into the buffer that is already
    	// overflowed.
    	
    	strncat(overflowedBuffer, &outbuffer[0x16], 0xFF);
    	
    	strncpy(&outbuffer[0x16], overflowedBuffer, 0xFF);
    	...
    	...
    	...
    	...
    }
    

    By overwriting the saved EIP using the buffer overflow, it is possible to execute arbitrary code.

     


    POC / Test Code

    The following POC LZH file will exploit the vulnerability in Lhaca to execute the harmless calculator (calc.exe). The POC files have been successfully tested on English WinXP SP2 and Japanese Windows 2000 SP4.


    Instructions:

    1. Download and save the POC files in C:\test (Do not change the filename)
    2. Open c:\test in Explorer.
    3. Double click on the POC file to extract it using Lhaca.
    4. Successful exploit will run calculator (calc.exe). Failed exploit will crash Lhaca.
     


    Patch / Workaround

    The vendor fixes the vulnerability in version 1.22. However, version 1.22 do not work properly as some test code was not removed. Hence, version 1.23 is released on the same day.

    Update to version 1.23


    Disclosure Timeline

    2007-06-25 - Symantec Security Response releases information of Trojan.Lhdropper.
    2007-06-26 - Vendor releases Lhaca version 1.21.
    2007-06-29 - Confirmed an exploitable stack-based buffer overflow in Lhaca version 1.21.
    2007-06-29 - Initial Vendor Notification.
    2007-06-30 - Initial Vendor Reply.
    2007-06-30 - Vulnerability report and POC files sent to vendor.
    2007-07-01 - Vendor Released Fixed Version (version 1.22 and 1.23).
    2007-07-01 - Public Release.
    2007-07-09 - Corrected typo error. Changed strncpy() to strncat() in vulnerability description.


    Contact
    For further enquries, comments, suggestions or bug reports, simply email them to