SmartRM.com is an venture capital-backed service (founded by italian people in the USA) that allows to protect user documents and videos, letting the user to store locally a copy of the encrypted document for easy and secure distribution via email, USB keys etc.

In order to view the original document, the user needs to login into SmartRM and then use a proprietary viewer to open the encrypted PDF, in our test case.

The problem is that the viewer can be easily hacked in order to obtain a copy of the original document, starting from an encrypted version of the document itself.
This is pretty obvious and easy to accomplish, in any case.

The promises of the service are listed in their homepage, and sum up the nature of the application:
  • Alice cannot read the document before tomorrow morning
  • Bob can watch the video only once
  • Everyone can listen to my song for a week
  • Everyone can ask me the permission to open my file
Although the first and the last statements remain allegedly true, a simple viewer hack makes the second and third statements misleading and exposes the user’s sensitive data to an high risk of leakage.

There are 2 “solutions” to this problem:
  • “Security through obscurity”, ie by using “tricks” to complicate the work of an hacker, as Windows Media Player does, for example. But “security through obscurity” is not security, as someone has said, and is a “solution” doomed to failure in the short-term.
  • Completely redesign the security and application model, considering, for example, the idea of storing the user data on a central server, and then serving the content as images or in a streaming way (as SmartRM's competitors do). But in this case there are a lot of other security problems and considerations that need to be taken into consideration… Out of the scope of this demonstration.
This video demonstrates the simple hack:



Technically speaking, the hack proposed in this post is pretty simple: it required only 3 hours to be discovered and developed.

The SmartRM viewer is implemented as a FireFox extension: any time you need to open a document (currently only PDF files are supported by SmartRM), the extension decrypts locally the document, calls a third party PDF library (Quick PDF Library - www.quickpdflibrary.com) passing the original unencrypted version of the document and then the file is showed to the user as a series of images, that the user can only view or, eventually, print, if given the appropriate permission. The PDF rendering job is done by the Quick PDF Library (QuickPDFDLL0717.dll).

The hack, and then the “concept flaw” of the whole idea is simple: it is enough to “intercept” the call from the SmartRM extension to the Quick PDF Library dll to obtain an original, unencrypted version of the PDF.

As said before and considering that having the user's encrypted files stored locally is the main competitive advantage of the service against its competitors, the only remedy to this problem is employing some form of “security through obscurity”, that, as you probably know, is a very short-term solution to the problem and, for a company involved in protecting sensitive data for its clients, is simply a credibility suicide.

As a computer user, some years ago, I remember the case of the live streaming of RealMedia video files (rtsp protocol). The premise of that technology was to allow an internet user to gain access to a video file only through live streaming, without the ability to download and then potentially share the content with other (non-paying) internet users. The response of the internet community was a very smart tool (Streambox VCR) which allowed to “record” a rtsp live stream into a file saved into the user’s hard drive. The response of Real.com was a lawsuit to discontinue the support and distribution of Streambox, making it one of the most famous “underground” tools of that time: people started to reverse engineer Streambox in order to make it up-to-date with the changes in the rtsp protocol… A lawsuit was the only option in this case for RealMedia and this is the point of the whole discussion.

Another (in)famous case was that of the “Sony Rootkit”, intended for copy protection, discovered by Mark Russinovich (search Google for more info).

Windows Media Player, for example, in a typical spirit of “security through obscurity”, doesn’t allow to play DRM-protected files if a debugger is attached to the player’s Windows process… This should stop only the very inexperienced hacker !

As it should be clear, it is impossible to guarantee protection of sensitive data from technological premises like these.

How the hack works (and the Proxy Dll hooking trick)

As said before, the SmartRM extension calls into the QuickPDFDLL0717.dll through its exported functions in order to render a PDF page into a GDI Device Context. The relevant flow of calls into QuickPDFDLL0717.dll is as follow:

QuickPDFCreateLibrary : initializes the library.
QuickPDFUnlockKey : unlocks the library.
QuickPDFCreateBuffer : creates a “buffer”.
QuickPDFAddToBuffer : copies the unencrypted PDF bytes into the buffer.
QuickPDFRenderPageToDC : renders the PDF to a Device Context.


So, simply by intercepting the call to QuickPDFAddToBuffer is enough to get an unencrypted version of the PDF.

As you may already know, there are a lot of systems and tricks to intercept an API call (take a look at my site). The case in question is the most simple: a DLL calling into an other DLL through an exported reference… The most simple interception system is to build a “proxy dll”, ie a module with the same name of the one we want to monitor and, above all, with the exact same exports of the original image (in this case QuickPDFDLL0717.dll), respecting the same calling convention, function names, number and order of parameters, type of each parameter and return value of the original module.

So I downloaded the Quick PDF Library (which includes a C++ SDK) and found 2 files (a source and an header file) that allow to late bind to QuickPDFDLL0717.dll: the most important information is the declaration of each exported symbol; for example in the .h file we find a list of type definitions, as this one:

typedef char* (__stdcall *QuickPDFFuncType7)(int, double, double, double, double, char*);

and then, in the .cpp file, we discover the association between the type name (QuickPDFFuncType7 in this case) and the exported symbol name:

QuickPDFDrawHTMLTextBox = (QuickPDFFuncType7)AttachFunction("QuickPDFDrawHTMLTextBox");

By the way, the type name doesn’t directly reflect the function name only for space saving reasons (many exports share the same prototype).

So, after an hour of “Replace All” and “Quick Macros” I come out with something like this:

#define FuncDecl1(fnname) extern "C" char* __stdcall fnname(int paramEND)

#define FuncParams1 (paramEND)

typedef char* (__stdcall *QuickPDFFuncType1)(int);

HMODULE DllRef = NULL;

#define FuncInit(ord, fnname) \
    if ( DllRef == NULL ) DllRef = LoadLibraryA("original_QuickPDFDLL0717.dll"); \
    QuickPDFFuncType##ord FnPtr = (QuickPDFFuncType##ord) GetProcAddress( DllRef, #fnname );

FuncDecl1(QuickPDFFontFamily)
{
    FuncInit( 1, QuickPDFFontFamily )
    AddToLog( "QuickPDFFontFamily" );
    return FnPtr FuncParams1 ;
}


These entry points in the proxy DLL simply are dinamically bound to the calling module, log or do something and then call the original function.

This is only an example of one function: it may seem an huge work, but, a bit of knowledge of Visual Studio Macros and a bit of “creative editing” (and a good keyboard) is enough to create a proxy like the one used in this demonstration in about 1 hour.

So, how the SmartRM hack works ? This is the proxy QuickPDFAddToBuffer:

FuncDecl28(QuickPDFAddToBuffer)
{
    FuncInit( 28, QuickPDFAddToBuffer )
    AddToLog( "QuickPDFAddToBuffer" );
    AddToLog( paramEND );

    // save.
    FILE* fp = ::fopen( "c:\\unencrypted.pdf", "wb" );
    if ( fp )
    {
        ::fwrite( param2, 1, paramEND, fp );
        ::fclose( fp );
    }

    return FnPtr FuncParams28 ;
}


That's it.

To apply the patch, follow these steps:
  1. Go to this folder (under Windows 7): C:\Users\<your_name>\AppData\Roaming\Mozilla\Firefox\Profiles\<your_firefox_profile_name>\extensions\smart-rm@smartrm.com\libraries
  2. Create a folder with name: “test”.
  3. Copy (copy, don’t move!) all the files in the libraries folder (log4cxx.dll, msvcp71.dll, msvcr71.dll, QuickPDFDLL0717.dll, smartrm_comp.dll, xerces-c_3_0.dll) to the newly created “test” folder.
  4. In the libraries folder, replace QuickPDFDLL0717.dll with QuickPdfProxy.dll (ie delete the first file and rename the second file with the name of the first file).
That’s it. From now on SmartRM will save a “plain-text” version of each opened PDF in c:\unencrypted.pdf.

P.S. Note that the SmartRMInterceptor.exe in the video does just this (replacing
QuickPDFDLL0717.dll with QuickPdfProxy.dll ).

Source codes and DLL available here.


 

Invece di realizzare una seconda parte per questo post, che probabilmente avrebbe potuto estendersi all'infinito, ho preferito, nel mese di Gennaio, riprendere seriamente in mano BugChecker (che, per chi non lo sapesse, è un "clone" del famoso Numega Softice, oramai "dismesso" da Compuware, interamente sviluppato dal sottoscritto un 6/7 anni or sono) e quindi portarlo da Windows 2000 SP4 (unica piattaforma supportata fino al 2009) a Windows XP (tutti i service pack).

La compatibilità con Windows 2000 e Windows XP (tutti i service pack) è ottenuta tramite una utility (scritta ad-hoc) che permette di scaricare dal symbol server di Microsoft i file DBG e/o PDB di una data immagine del kernel di Windows, quindi di generare automaticamente un file di riferimento per il driver kernel del debugger con puntatori ed offset a funzioni chiave del sistema (necessarie al debugger per intercettare chiamate interne del kernel per vari scopi, come per esempio mantenere un suo Page Frame Db).

Inoltre ho deciso di "aprire" i codici sorgente di BugChecker, sotto la GNU General Public License, versione 2, nella speranza che qualche volenteroso si unisca allo sviluppo per il completamento e la realizzazione di tutte quelle feature che mancano al tool per renderlo una efficace e "free" (=gratis) alternativa a quello che oggi è l'unico competitor in commercio, ossia Syser (che non ho mai usato e che, a quanto pare, è avvolto nell'oscurità, per ciò che concerne l'identità degli sviluppatori --- il sito è ospitato da un ISP del Texas, mentre l'articolo sulla Wikipedia è stato recentemente rimosso, poichè troppo "promozionale"). Ovviamente Syser è un software a pagamento.

Il sito di BugChecker è http://bugchecker.com/:


Il mio indirizzo email, per domande o altro, è sempre lo stesso: planta__AT__vitoplantamura.com.


 

Sbirciando dopo quasi 5 anni nel codice sorgente di BugChecker (un kernel debugger "alla SoftICE" che ho sviluppato all'epoca con l'intento primario di approfondire le mie conoscenze del sistema ai più bassi livelli possibili e per collaudare tecniche di reverse engineering non frequentemente utilizzabili in scenari quotidiani) mi sono appassionato non poco a ripercorrere l'iter di attivazione del debugger nel caso di sistemi multiprocessore. Questo è uno screenshot del debugger al lavoro sul codice del kernel di Windows 2000 --- con supporto per tastiera e mouse PS/2 (il sistema, un Pentium 3 bi-processore, è bloccato in ogni sua funzione in attesa di un input alla console di BugChecker):



Per chi non lo sapesse, un kernel debugger, a differenza di Visual Studio, che è uno "user mode debugger", per così dire, o del CLR Debugger, permette di "debuggare" (o più comunemente "analizzare") il codice assembler del sistema a tutti i livelli, sia quello kernel (dove le componenti core del sistema e parte dei driver risiedono) che quello user (dove le applicazioni di tutti i giorni vengono caricate ed eseguite). Il supporto tipico a questa attività sono i file di simboli dei principali moduli kernel e user del sistema, facilmente scaricabili da una sorta di Web Service di Microsoft (i dettagli del protocollo web utilizzato dal Symbol Server di Microsoft sono discussi in questo mio articolo). Senza i simboli, che danno un nome alle procedure e alle aree di memoria, l'interpretazione del codice assembler sarebbe estremamente più impegnativa. Come è risaputo i codici sorgente di Windows sono preclusi ai più, e libri essenziali come Windows Internals di Russinovich possono soddisfare la curiosità dei più arditi fino ad un certo punto: poi devono intervenire le capacità personali di analisi e deduzione dell'individuo, attraverso quella attività che è detta di "reverse engineering". Sviluppare BugChecker è stata una delle esperienze professionali più divertenti che abbia mai condotto, poichè l'intera base di conoscenze che sono state presupposto per il suo sviluppo derivano da una attività sistematica e metodica di reverse engineering di parte del kernel di Windows 2000 (attraverso SoftICE stesso, IDA, un decompilatore, e la bibbia già citata di Russinovich).

Per questa sua caratteristica peculiaria, questo tipo di software viene spesso utilizzato per "sbirciare" nel codice di applicazioni (per capirne il funzionamento, a fini più o meno leciti) e nel codice del sistema e delle sue componenti chiave (comunemente per scoprirne vulnerabilità o simili). Tipicamente, malware come i rootkit nascono da analisi condotte attraverso strumenti di questo tipo.

La base di funzionamento di BugChecker (e di SoftICE) è un kernel driver destinato ad intercettare l'indirizzo di memoria virtuale del framebuffer video e il suo formato, in modo da poter disegnare la propria interfaccia senza passare dai servizi di sistema (DDI/GDI). Un driver secondario è richiesto (rispetto al modulo del debugger vero e proprio) poichè tale driver viene caricato al boot del sistema, prima che avvenga l'inizializzazione di DirectDraw (e dando la possibilità di intercettare gli entry point primari in fase di inizializzazione del sottosistema video al fine di intercettare certe strutture contenenti le informazioni di cui parlavo prima). Un articolo sul mio sito (qui) spiega in dettaglio il funzionamento di questa componente, con codici sorgente e binari inclusi. Lo sviluppo di questa parte ha richiesto uno studio approfondito di DirectDraw lato kernel (via MSDN) e lunghe sessioni di "debugging" kernel per capire come in effetti il sistema funzionava.

La prima cosa che il debugger fà è allocare memoria. La funzione utilizzata è ExAllocatePool, col parametro NonPagedPool, che indica al sistema di riservare della memoria virtuale non paginabile su disco (quindi perennemente associata a della memoria fisica). Questo è particolarmente importante, poichè il debugger potrebbe trovarsi a fare trace in codice di sistema ad un IRQL maggiore o uguale a DISPATCH_LEVEL. In parole povere, l'IRQL (Interrupt Request Level) è un intero assegnato per processore (con costanti quali DISPATCH_LEVEL) che regola la prioritizzazione degli interrupt per quella CPU. In sostanza, gli interrupt (ossia eventi software o hardware) che accadono ad un IRQL minore o uguale a quello attualmente impostato per un processore vengono bloccati e messi in attesa, mentre interrupt che accadono ad un IRQL maggiore interrompono l'attività corrente (poichè più prioritari) vengono eseguiti ed una volta completati viene ripresa l'attività precedente, all'IRQL precedente. Le routine di sistema che gestiscono lo swap di pagine di memoria nel paging file verso memoria fisica vengono eseguite ad un IRQL di livello DISPATCH_LEVEL: questo significa che se ci trovassimo ad un IRQL maggiore e provassimo a leggere/scrivere/eseguire della memoria "non presente", il sistema non potrebbe leggerla dal disco e si andrebbe direttamente in Blue Screen (con errore IRQL_NOT_LESS_OR_EQUAL).

post_kernel_1_bsod.jpg

Una delle cose successive che vengono fatte è determinare l'indirizzo virtuale della memoria video testo (indirizzo fisico: 0xB8000). Il debugger infatti viene caricato subito dopo il boot ed è possibile fare trace di codice kernel durante l'avvio del sistema, quando ancora non si è passati ad una modalità video grafica. Questo può essere fatto navigando la Page Directory (che tiene traccia del rapporto tra memoria fisica e virtuale) oppure attraverso l'API di sistema MmMapIoSpace:

#ifdef USE_PAGETABLE_TO_OBTAIN_TEXTMODE_VIDEOADDRESS

      PhysAddressToLinearAddresses( & extension->pvTextVideoBuffer, 1, NULL, 0xB8000 );

#else

      {

            PHYSICAL_ADDRESS        paPhysAddr;

            paPhysAddr.QuadPart = 0xB8000;

            extension->dglLayouts.pvTextVideoBuffer = MmMapIoSpace( paPhysAddr, 1, MmNonCached );

      }

#endif


La procedura PhysAddressToLinearAddresses, riportata di seguito, è piuttosto complessa. Conto in un post successivo di discutere circa i meccanismi di gestione della memoria virtuale in Windows NT-Vista Kernel: Page Tables, Page Directories, Translation lookaside buffers, differenze tra x86/PAE/x64/Itanium e quant'altro.

NTSTATUS PhysAddressToLinearAddresses( OUT PVOID* ppvOutputVector, IN ULONG ulOutputVectorSize, OUT ULONG* pulOutputVectorRetItemsNum, IN DWORD dwPhysAddress )

{

      NTSTATUS                nsRetVal = STATUS_SUCCESS;

      DWORD*                       pdwPageDir = (DWORD*) 0xC0300000;

      ULONG                   i, j;

      DWORD                   dwPageDirEntry, dwPageTblEntry;

      DWORD*                       pdwPageTable;

      DWORD                   dwPageTblEntryPhysAddress[ 2 ];

      ULONG                   ulOutputVectorPos = 0;

 

      if ( pulOutputVectorRetItemsNum )

            * pulOutputVectorRetItemsNum = 0;

 

      // Search in the Page Directory for the Specified Address.

 

      for ( i=0; i<1024; i++ )

      {

            dwPageDirEntry = pdwPageDir[ i ];

 

            // Check if this Page Table has an Address and if its Present bit is set to 1.

 

            if ( ( dwPageDirEntry >> 12 ) &&

                  ( dwPageDirEntry & 0x1 ) )

            {

                  pdwPageTable = (DWORD*) ( (BYTE*) 0xC0000000 + i * 0x1000 );

 

                  for ( j=0; j<1024; j++ )

                  {

                        dwPageTblEntry = pdwPageTable[ j ];

 

                        // Check if this Page Table Entry has an associated Physical Address.

 

                        if ( dwPageTblEntry >> 12 )

                        {

                             // Calculate the MIN and MAX Phys Address of the Page Table Entry.

 

                             dwPageTblEntryPhysAddress[ 0 ] = dwPageTblEntry & 0xFFFFF000;

                             dwPageTblEntryPhysAddress[ 1 ] = dwPageTblEntryPhysAddress[ 0 ] + 0x1000 - 1;

 

                             // Check if our Address is between the Interval.

 

                             if ( dwPhysAddress >= dwPageTblEntryPhysAddress[ 0 ] &&

                                   dwPhysAddress <= dwPageTblEntryPhysAddress[ 1 ] )

                             {

                                   // Add this Linear Address.

 

                                   if ( ulOutputVectorPos < ulOutputVectorSize )

                                   {

                                         ppvOutputVector[ ulOutputVectorPos ++ ] = (PVOID)

                                               ( i * 0x400000 + j * 0x1000 +

                                               ( dwPhysAddress - dwPageTblEntryPhysAddress[ 0 ] ) );

                                   }

                                   else

                                   {

                                         if ( pulOutputVectorRetItemsNum )

                                                * pulOutputVectorRetItemsNum = ulOutputVectorPos;

 

                                         return STATUS_SUCCESS;

                                   }

                             }

                        }

                  }

            }

      }

 

      // Return to the Caller.

 

      if ( pulOutputVectorRetItemsNum )

            * pulOutputVectorRetItemsNum = ulOutputVectorPos;

 

      return nsRetVal;

}


Passo successivo è il caricamento del file di configurazione del debugger. Questo viene fatto attraverso la funzione LoadFile, riportata qui di seguito:

PVOID LoadFile( IN PCWSTR pszFileName, IN POOL_TYPE ptPoolType, OUT ULONG* pulSize )

{

      PVOID                              retval = NULL;

      NTSTATUS                           ntStatus;

      HANDLE                                   handle = NULL;

      OBJECT_ATTRIBUTES            attrs;

      UNICODE_STRING                     unicode_fn;

      IO_STATUS_BLOCK                    iosb;

      FILE_STANDARD_INFORMATION    info;

      ULONG                              size = 0;

      PVOID                              mem;

      LARGE_INTEGER                      zeropos;

 

      memset( & zeropos, 0, sizeof( zeropos ) );

 

      // Load the File.

 

      RtlInitUnicodeString( & unicode_fn, pszFileName );

 

      InitializeObjectAttributes( & attrs,

            & unicode_fn,

            OBJ_CASE_INSENSITIVE,

            NULL,

            NULL );

 

      ntStatus = ZwCreateFile( & handle,

            FILE_READ_DATA | GENERIC_READ | SYNCHRONIZE,

            & attrs,

            & iosb,

            0,

            FILE_ATTRIBUTE_NORMAL,

            0,

            FILE_OPEN,

            FILE_NON_DIRECTORY_FILE | FILE_RANDOM_ACCESS | FILE_SYNCHRONOUS_IO_NONALERT,

            NULL,

            0 );

 

      if ( ntStatus == STATUS_SUCCESS && handle )

      {

            ntStatus = ZwQueryInformationFile(

                  handle,

                  & iosb,

                  & info,

                  sizeof( info ),

                  FileStandardInformation );

 

            if ( ntStatus == STATUS_SUCCESS )

            {

                  size = info.EndOfFile.LowPart;

 

                  mem = ExAllocatePool( ptPoolType, size );

 

                  if ( mem )

                  {

                        ntStatus = ZwReadFile(

                             handle,

                             NULL,

                             NULL,

                             NULL,

                             & iosb,

                             mem,

                             size,

                             & zeropos,

                             NULL );

 

                        if ( ntStatus != STATUS_SUCCESS || iosb.Information != size )

                        {

                             ExFreePool( mem );

                        }

                        else

                        {

                             retval = mem;

                        }

                  }

            }

 

            ZwClose( handle );

      }

 

      // Return.

 

      if ( pulSize && retval )

            * pulSize = size;

 

      return retval;

}


Successivamente vengono letti e salvati alcuni export del kernel come *MmUserProbeAddress (indirizzo virtuale che determina l'inizio della memoria riservata al kernel) e *KeNumberProcessors, che si commenta da solo.

Il passo successivo, ben più importante, è di scoprire l'indirizzo virtuale della DriverSection del kernel, ossia del file NtOsKrnl.exe. Per inciso il nome del modulo del kernel nel CD di installazione del sistema dipende dall'architettura del processore:

  • NTOSKRNL.EXE, single-processor without PAE
  • NTKRNLMP.EXE, multi-processor without PAE
  • NTKRNLPA.EXE, single-processor with PAE
  • NTKRPAMP.EXE, multi-processor with PAE

Nota: Le Physical Address Extension (PAE) (architettura Intel IA-32) è un meccanismo che permette a processori IA-32 di indirizzare fino a 64 GB di memoria RAM.

La DriverSection di un driver kernel è una struttura di tipo _LDR_DATA_TABLE_ENTRY. Ciascun driver caricato attraverso l'SCM, ha un entry point del tipo:

NTSTATUS DriverEntry( IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath )

dove DriverObject->DriverSection punta alla struttura _LDR_DATA_TABLE_ENTRY del driver sys di BugChecker. La DriverSection, grazie al comando dt di WinDbg, rivela questi field:

lkd> dt _LDR_DATA_TABLE_ENTRY

+0×000 InLoadOrderLinks : _LIST_ENTRY

+0×008 InMemoryOrderLinks : _LIST_ENTRY

+0×010 InInitializationOrderLinks : _LIST_ENTRY

+0×018 DllBase : Ptr32 Void

+0×01c EntryPoint : Ptr32 Void

+0×020 SizeOfImage : Uint4B

+0×024 FullDllName : _UNICODE_STRING

+0×02c BaseDllName : _UNICODE_STRING

+0×034 Flags : Uint4B

+0×038 LoadCount : Uint2B

+0×03a TlsIndex : Uint2B

.. .. .. .. .. .. .. .. .. ..

Il field che ci interessa è InLoadOrderLinks, che è un nodo alla linked list (di tipo _LIST_ENTRY) di tutti i moduli mappati nel kernel. Il primo modulo caricato nel kernel è NtOsKrnl.exe; quindi è ragionevole cercare per la DriverSection di NTOSKRNL in modo da individuare la "testa" della linked list, e quindi avere una maniera semplice e veloce per iterare tra tutti i moduli kernel caricati in un dato momento (navigando la linked list dalla testa verso la coda, per così dire). Questo è fondamentale per BugChecker e per il suo motore dei simboli: partendo dall'informazione di dove una specifica posizione di memoria si trova (sia codice che dati), in termini di modulo!section+displacement, è possibile estrarre dalle informazioni dei simboli di un dato modulo il nome di una funzione o di una variabile.

post_kernel_1_symloc.gif

La funzione DiscoverNtoskrnlDriverSection è piuttosto semplice, e riceve come parametro la DriverSection di BugChecker (o di qualsivoglia driver caricato dall'SCM), ossia DriverObject->DriverSection.

static CHAR             g_szDiscoverNtoskrnlDriverSectionTempBuffer[ 2 * 1024 ]; // NOTE: sizeof < sizeof( System Page Size )

 

VOID* DiscoverNtoskrnlDriverSection( IN VOID* pvDriverSection )

{

      LIST_ENTRY*             pleListNodePtr;

      WORD*                   pwImageNameLengthPtr;

      WORD                    wImageNameLength;

      WORD*                   pwImageNameUnicodePtr;

      DWORD*                       pdwImageNameUnicodePtrPtr;

      WORD*                   pwWordPtr;

      CHAR*                   pcCharPtr;

      ULONG                   ulI;

 

      // Do the Requested Operation.

 

      pleListNodePtr = (LIST_ENTRY*) pvDriverSection;

 

      while( TRUE )

      {

            // Get the Pointer to the Previous Node.

 

            if ( pleListNodePtr == NULL ||

                  IsPagePresent_DWORD( (DWORD*) ( ( (BYTE*) pleListNodePtr ) + FIELD_OFFSET( LIST_ENTRY, Blink ) ) ) == FALSE )

                        return NULL;

 

            pleListNodePtr = pleListNodePtr->Blink;

 

            if ( pleListNodePtr == NULL ||

                  pleListNodePtr == (LIST_ENTRY*) pvDriverSection ||

                  IsPagePresent( pleListNodePtr ) == FALSE )

                        return NULL;

 

            // Get the Name of the Module.

 

            pwImageNameLengthPtr = (WORD*) ( ( (BYTE*) pleListNodePtr ) + MACRO_IMAGENAME_FIELDOFFSET_IN_DRVSEC );

 

            if ( IsPagePresent_WORD( pwImageNameLengthPtr ) == FALSE )

                        return NULL;

 

            wImageNameLength = * pwImageNameLengthPtr / sizeof( WORD );

 

            if ( wImageNameLength == 0 ||

                  wImageNameLength > sizeof( g_szDiscoverNtoskrnlDriverSectionTempBuffer ) - 1 )

                        return NULL;

 

            pdwImageNameUnicodePtrPtr = (DWORD*) ( ( (BYTE*) pleListNodePtr ) +

                  MACRO_IMAGENAME_FIELDOFFSET_IN_DRVSEC + FIELD_OFFSET( UNICODE_STRING, Buffer ) );

 

            if ( IsPagePresent_DWORD( pdwImageNameUnicodePtrPtr ) == FALSE )

                        return NULL;

 

            pwImageNameUnicodePtr = (WORD*) * pdwImageNameUnicodePtrPtr;

 

            if ( pwImageNameUnicodePtr == NULL ||

                  IsPagePresent( pwImageNameUnicodePtr ) == FALSE )

                  return NULL;

 

            if ( IsPagePresent_WORD( pwImageNameUnicodePtr + wImageNameLength - 1 ) == FALSE )

                  return NULL;

 

            pwWordPtr = pwImageNameUnicodePtr;

            pcCharPtr = g_szDiscoverNtoskrnlDriverSectionTempBuffer;

 

            for ( ulI = 0; ulI < wImageNameLength; ulI ++ )

                  * pcCharPtr ++ = (CHAR) ( ( * pwWordPtr ++ ) & 0xFF );

 

            * pcCharPtr = '\0';

 

            _strupr( g_szDiscoverNtoskrnlDriverSectionTempBuffer );

 

            // Check for the Presence of the NTOSKRNL Module Name.

 

            if ( strstr( g_szDiscoverNtoskrnlDriverSectionTempBuffer, MACRO_NTOSKRNL_MODULENAME_UPPERCASE ) )

                  return pleListNodePtr;

      }

}


La funzione DiscoverBytePointerPosInModules è utilizzata appunto per risalire all'informazione modulo!section+displacement partendo da un indirizzo virtuale (che può essere il primo byte disassemblato della finestra di debug, oppure l'Instruction Pointer attuale). In base alla "posizione" del byte di memoria richiesto (se kernel o user, in base all'export *MmUserProbeAddress) vengono adottati due approcci diversi. Nel caso della memoria kernel, viene utilizzata la linked list della DriverSection dell'NTOSKRNL.EXE. In sostanza, percorrendo la linked list da sinistra verso destra (partendo dalla driver section di NtOsKrnl), seguendo i vari puntatori forward della struttura LIST_ENTRY, è possibile avere un riferimento a tutti i moduli caricati nel kernel. La struttura IMAGE_DOS_HEADER di ciascun modulo caricato nel kernel (che sia .sys, .exe o .dll) si trova ad uno specifico offset di byte rispetto alla stessa struttura LIST_ENTRY. Questo permette facilmente di avere accesso al modulo, alla relativa sezione con la lista delle "section" (tipo .text o .data) e quindi di determinare se una posizione di memoria virtuale "cade" all'interno del modulo, in quale section, e con quale displacement rispetto alla base della section stessa. Questa informazione (modulo!section+displacement) è la chiave di lookup nei file di simboli per ottenere in fase di debugging nomi di funzioni, variabili e quant'altro (questo vale anche per le applicazioni user e relativi PDB).

Se il byte di memoria "cade" nello spazio user, il discorso è un poco diverso. In questo caso, viene analizzato il VAS (Virtual Address Space) associato al processo attualmente attivo nel sistema, per risalire alle stesse informazioni di modulo!section+displacement necessarie per ottenere le info di debug. Il VAS è una risorsa di sistema che rappresenta la memoria allocata da un dato processo attraverso le API VirtualAlloc* e VirtualFree*, che stanno alla base di ogni operazione su memoria virtuale user in Windows (dal malloc/free di C ai memory mapped files). Il VAS si basa su una struttura ad albero di descrittori VAD (Virtual Address Descriptor) che rappresentano le caratteristiche di una determinata area di memoria virtuale in quello specifico processo (per inciso, ad ogni processo Windows corrisponde un unico specifico VAS, che a sua volta gestisce un albero dettagliato di descrittori VAD per rappresentare lo stato della memoria virtuale in quel dato processo). Un descrittore VAD ha questo formato:

#pragma pack(push, 1)

 

      typedef struct _VAD

      {

            VOID*             pvStartingAddress;

            VOID*             pvEndingAddress;

            struct _VAD*      pvadParentLink;

            struct _VAD*      pvadLeftLink;

            struct _VAD*      pvadRightLink;

            DWORD             dwFlags;

            DWORD             dwUndocumented_DWORD;

 

      } VAD, *PVAD;

 

#pragma pack(pop)


La struttura ad albero risultante è facilmente intuibile. Il codice sorgente della funzione DiscoverBytePointerPosInModules è visionabile qui:

post_kernel_1_dbpim.gif

La funzione non è particolarmente complessa: prima viene determinata al "zona" del puntatore in input, se kernel o user. Nel caso di puntatore user, viene chiamata la funzione VadTreeWalk che in maniera ricorsiva determina, se presente, un descrittore VAD facente riferimento alla memoria che stiamo cercando. La seconda parte della procedura, quella relativa alla ricerca nello spazio kernel, fa ciò che ho illustrato precedentemente, in buona sostanza (ricerca del modulo partendo da NtOsKrnl). Particolare interessante è l'utilizzo estensivo della funzione IsPagePresent:

//====================================

// IsPagePresent Function Definition.

//====================================

 

BOOLEAN IsPagePresent( IN PVOID pvVirtAddress )

{

      DWORD*                        pdwPageDir = (DWORD*) 0xC0300000;

      DWORD                   dwPageDirEntry;

      DWORD*                        pdwPageTables = (DWORD*) 0xC0000000;

      DWORD                   dwPageTableEntry;

 

      // Check the Page Tables.

 

      dwPageDirEntry = pdwPageDir[ ( (DWORD) pvVirtAddress ) / 0x400000 ];

 

      if ( ( dwPageDirEntry >> 12 ) &&

            ( dwPageDirEntry & 0x1 ) )

      {

            if ( dwPageDirEntry & (1<<7) )

                  return TRUE;

 

            dwPageTableEntry = pdwPageTables[ ( (DWORD) pvVirtAddress ) / 0x1000 ];

 

            if ( ( dwPageTableEntry >> 12 ) &&

                  ( dwPageTableEntry & 0x1 ) )

                        return TRUE;

      }

 

      // Return to the Caller.

 

      return FALSE;

}


Lo scopo di questa funzione e delle sue varianti è di verificare (nella Page Directory) se una zona di memoria è "presente", cioè se è fisicamente accessibile in lettura. Considerando che il codice del debugger deve funzionare a qualsiasi IRQL (vedi discorso di prima su NonPagedPool) ogni lettura esterna alle strutture del debugger deve essere prima verificata per evitare STOP di tipo IRQL_NOT_LESS_OR_EQUAL.

- fine prima parte -


 

Ultimamente sto avendo parecchi problemi nel debug di una grossa applicazione .NET 2.0 scritta in VB.NET che conta più di 70 progetti. Molto frequentemente, quando attivo di debugger di Visual Studio oppure provo ad "attaccarmi" al processo di ASP.NET (aspnet_wp.exe) l'operazione fallisce miseramente con un dialog del tipo:

error.GIF

Questo molte volte provoca il crash dell'IDE più l'abort del thread ASP.NET dell'applicazione che cercavo di analizzare. Il tutto alla fine si traduce in una enorme perdita di tempo.

Per risolvere il problema ho individuato due soluzioni:

1) Riavvio di Visual Studio prima del debug + IISRESET + Riavvio del servizio "Machine Debug Manager" (che si occupa direttamente dei debug locali e remoti).

Partendo da questa situazione "pulita" 1 volta su 2 riesco a debuggare, fissare breakpoint e fare trace nella succitata applicazione. Immagino che il problema origini dal fatto che l'enorme complessità e vastità di tale applicazione determini una quantità di simboli da caricare in fase di debug non indifferente, e per una ragione o per l'altra (out of memory, probabilmente) qualcosa va storto.

2) Soluzione più sicura e con maggiore percentuale di successo (100% per il momento) è di installare ed utilizzare per i soli debug il Microsoft CLR Debugger, tool decisamente più snello e veloce di Visual Studio per questo genere di cose.

Attaccarsi al processo di ASP.NET è una operazione veloce e senza problemi, e fare trace nel codice è particolarmente veloce. Ovviamente è possibile ispezionare il contenuto di oggetti live (Watch) e di posizionare breakpoint nel codice (anche condizionali). A tale scopo, è sufficiente aprire un file di codice relativo alla build che si sta debuggando, quindi inserire il breakpoint dove si vuole. Questo è dovuto al fatto che nei file di simboli (.PDB) i riferimenti alle path dei file sorgente sono sempre assoluti.

Il Microsoft CLR Debugger è incluso nel .NET Framework 2.0 Software Development Kit (SDK), scaricabile qui.


 
March 12, 2008
@ 01:43 PM

Ho scritto due articoli su due tecnologie recenti e molto interessanti di Microsoft, su UGIdotNET: 

E’ necessario registrarsi al sito prima di poter accedere ai contenuti all’interno.

 

Buona Lettura !


 

Per poter fare Inter-Process Communication tra processi Win32, storicamente, sono disponibili i seguenti strumenti:

  • Clipboard
  • COM
  • Data Copy
  • DDE
  • File Mapping
  • Mailslots
  • Pipes
  • RPC
  • Windows Sockets

Ognuno di essi ha dei pro, dei contro e delle applicazioni specifiche:

Clipboard

Probabilmente si tratta del meccanismo meno idoneo per scambiare dati tra processi. La clipboard è condivisa da tutta la sessione Windows corrente e soprattutto l'utente nè ha accesso trasparente quando fà copia ed incolla da un semplice textbox, per esempio.

Inoltre manca la possibilità di specificare criteri di sicurezza associati al canale e/o ai dati trasferiti.

COM

OLE (basato su COM) supporta i "compound documents", dove è possibile edittare parti di documenti provenienti e associati ad applicazioni (processi) Win32 differenti.

DCOM

DCOM permette di avere client e server distribuiti su processi Win32/64 diversi, ed anche su macchine diverse.

Il vantaggio (trasparente) del Marshalling di parametri tra processi diversi rispetto al COM tradizionale permette di oltrepassare i "limiti" del processo corrente.

Inoltre, il sofisticato modello di sicurezza implementato permette che solamente parti autorizzate possano connettersi ad un server DCOM.

Data Copy

Si tratta di un sistema di comunicazione basato sul messaggio Windows "WM_COPYDATA".

Il problema principale con questo sistema è la sicurezza: non è possibile (in modo semplice) determinare l'identità di chi sta tendando di inviare messaggi ad un recipient WM_COPYDATA.

Un mio articolo (in inglese) su una classe che implementa questo meccanismo di IPC è reperibile qui:

http://www.vitoplantamura.com/index.aspx?page=wmcopydata

DDE

DDE è di solito utilizzato per supportare vecchie applicazioni legacy che possono farne uso.

In soluzioni di nuova implementazione si preferisce usare uno degli altri meccanismi di IPC disponibili.

Mailslots

Nella mia esperienza di sviluppatore, ho avuto (veramente) poco a che fare con le Mailslot.

Solitamente vengono utilizzate per fare "broadcast" di piccoli messaggi tra computer nel contesto di un dominio di rete.

Pipes

Tra gli strumenti ed il software Microsoft (per Windows NT e superiori), le Named Pipe sono il secondo meccanismo di elezione per realizzare infrastrutture di Interprocess Communication tra processi Win32/64 (dopo i File Mapping, come detto sopra).

Una mia classe (e relativo articoletto in inglese) che utilizza le Named Pipe per fare Interprocess Communication è presente negli Archivi del mio sito qui:

http://www.vitoplantamura.com/index.aspx?page=namedpipes

RPC

RPC permette di chiamare funzioni remote, in maniera estremamente performante (in un altro processo sullo stesso computer o anche in un'altra macchina nella rete locale)

Per queste caratteristiche di velocità, l'RPC è utilizzato estensivamente a livello di sistema su piattaforma Windows NT e superiori.

Windows Sockets

I Windows Sockets sono lo strumento di elezione per l'IPC tra computer in una stessa rete (LAN, WAN o Internet), ma raramente sono utilizzati come meccanismo unico di comunicazione tra 2 o più processi sulla stessa macchina.

File Mapping, e la classe C++ qui presentata:

Il File Mapping è uno dei meccanismi più utilizzati per scambiare dati tra processi sulla stessa macchina, e, sicuramente, uno dei preferiti nelle stesse applicazioni sviluppate da Microsoft.

In sostanza, il File Mapping permette di trattare un file come se fosse una regione di memoria virtuale mappata nello spazio di indirizzamento del processo corrente. L'opportunità di IPC si presenta quando un secondo processo apre un oggetto di File Mapping oppure quando lo eredita dal processo padre, come accade nella classe associata a questo post.

Windows, internamente, mappa la regione di memoria virtuale associata al File Mapping nel primo processo alla stessa memoria fisica (o posizione nel Paging File) anche nel secondo processo: il risultato è che quando si scrive o legge da tale memoria (sia nel primo che nel secondo processo) internamente si sta accedendo alla stessa memoria fisica della macchina.

E' possibile associare delle informazioni di sicurezza all'oggetto di File Mapping tramite la struttura SECURITY_ATTRIBUTES, così come accade quando si crea un file, per esempio.

Nel caso specifico della classe qui presentata, l'handle all'oggetto di File Mapping viene passato al secondo processo tramite command line dal primo processo (ossia il processo "padre", tramite una chiamata a CreateProcess). Per rendere ciò possibile è necessario specificare esplicitamente che gli oggetti kernel che si stanno creando (nel primo processo) possono essere ereditati da tutti i processi "figli". Questo impostando un bit nella struttura SECURITY_ATTRIBUTES, passata poi alle funzioni "CreateFileMapping" e "CreateEvent":

  SECURITY_ATTRIBUTES sa;

  ::memset( & sa, 0, sizeof( sa ) );

  sa.bInheritHandle = TRUE;

 

  m_hDataAvailEvent = ::CreateEvent( & sa, FALSE, FALSE, NULL );

  m_hDataReceivedEvent = ::CreateEvent( & sa, FALSE, FALSE, NULL );

  m_hFileMapping = ::CreateFileMapping( INVALID_HANDLE_VALUE, & sa, PAGE_READWRITE, 0, m_dwFileMappingDim, NULL );

La classe ha due costruttori, a seconda se si è nel primo o nel secondo processo:

  CSharedMemoryChannel( DWORD dwFileMappingDim )

 

  CSharedMemoryChannel( charstring& csInitParams )

Il primo processo chiama il primo costruttore, specificando semplicemente la dimensione della Shared Section da creare.

Il secondo processo, invece, passa una stringa (eventualmente estratta dalla propria Command Line) con un formato di questo tipo:

  handle al File Mapping [PIPE] dimensione del File Mapping [PIPE] handle al primo Evento (DataAvailEvent) [PIPE] handle al secondo Evento (DataReceivedEvent)

L'applicazione può chiamare il metodo "GetSharedMemoryDataString" per ottenere in automatico questa stringa.

Come per le altre classi di IPC simili presenti nella sezione Archivi del mio sito, due tipi di puntatore a funzione sono definiti per permettere al consumer della mia classe di ricevere notifica quando nuovi dati sono presenti oppure quando l'altra parte si è "disconnessa":

  typedef VOID ( __cdecl * PFNSMCRECV )( IN DWORD dwParam, IN LPVOID pvBuffer, IN LONG lBufferSize );

  typedef VOID ( __cdecl * PFNSMCDISCONNECT )( IN DWORD dwParam );

 

Tali puntatori vengono specificati chiamando l'API "SetRecvFunction":

 

  BOOL SetRecvFunction ( PFNSMCRECV pfnRecv, PFNSMCDISCONNECT pfnDisconnect, DWORD dwRecvParam )

 

La classe è scaricabile qui: SharedMemoryChannel.h (10,19 KB).

 


 

Nel tentativo di aggiornare questo blog quanto più frequentemente possibile, mi piacerebbe postare qui del codice che ho scritto qualche tempo fà allo scopo di implementare qualcosa di simile all’effetto di menù a tendina fatto conoscere al grande pubblico (se non sbaglio) da Google Suggest qualche anno orsono:

Dunque, immagino che su internet si possano trovare decine di esempi simili (anche su CodeProject, probabilmente) ma ritengo che la bellezza della mia soluzione consista nel fatto che:

  • L’intera implementazione è veramente esigua. Questo significa che è possibile, per chi si sta avvicinando adesso ad AJAX, di capire un poco di più di che cosa si tratti, da un punto di vista molto di “basso livello”, se così si può dire. Nella mia esperienza di consulente e formatore, noto sempre più spesso la carenza di competenze Javascript (anche nei più abili programmatori ASP.NET !!) e contemporaneamente come tali competenze siano sempre più richieste dal mercato (vedi ad esempio la miriade di siti, anche italiani, recentemente usciti o rifatti, dove popup e menù a tendina come quello in oggetto sono ovunque).
  • In virtù del punto precedente, eventuali “customizzazioni” al mio lavoro risultano estremamente semplici da implementare: in altri controlli simili al mio che ho visto in giro (anche blasonati), tale semplicità si perde molto spesso in una miriade di eventi client o server nei quali il programmatore medio (e non) tende ad affogare (senza alla fine raggiungere il risultato desiderato).

Di contro però, c’è da dire che il codice che segue è stato espressamente scritto per Internet Explorer (quindi ideale per una applicazione di tipo intranet dove i requisiti dei client sono prefissati e conosciuti). Comunque, adeguare questo codice affinchè funzioni anche con altri browser è cosa assai semplice.

Per cominciare c’è la parte HTML. Questo markup permette di renderizzare un editbox predisposto a visualizzare il menù di Auto-Completion quando si preme un tasto, per esempio:

<div style="position: relative;">

    <input name="ctrlAcSearchBox" id="ctrlAcSearchBox" type="text" autocomplete="off" Style="width: 250px;"

        onkeydown='<%= "AutoCompletion_onEditBoxKey(\"" + "ctrlAcSearchBox" + "\", event.keyCode);" %>'

        onkeyup='<%= "AutoCompletion_onEditBoxKeyChange(\"http://localhost/folder/AutoCompletionInterface.aspx\", \"" + "ctrlAcSearchBox" + "\", event.keyCode);" %>'

        ondeactivate='<%= "document.getElementById( \"" + "ctrlAcSearchBox" + "\" + \"div\" ).style.display = \"none\";" %>' />

    <div id='<%= "ctrlAcSearchBox" + "div" %>' style="position: absolute; top: 23px;

        left: 0px; width: 250px; display: none;">

    </div>

</div>

La presenza qui delle due DIV è fondamentale: infatti la seconda DIV (ossia quella innestata), con l’attributo di stile “position: absolute” permette di visualizzare il box di Auto-Completion esattamente sotto l’editbox: infatti la direttiva “absolute”, che normalmente renderizzerebbe il contenuto della DIV rispetto all’angolo superiore-sinistro della finestra di browsing, qui viene invece riferita alla posizione della prima DIV, che possiede un attributo di stile di tipo “position: relative”. Togliendo infatti la prima DIV, si ottiene l’effetto di vedere il box di Auto-Completion nell’angolo superiore-sinistro della finestra, confermando quindi questa regola dell’HTML.

Il controllo INPUT TYPE=”TEXT” gestisce una serie di eventi che permettono al box di Auto-Completion di apparire e scomparire all’occorrenza (onkeydown, onkeyup, ondeactivate). Ricordo che da Code-Behind è possibile leggere il valore inserito nell’editbox scrivendo del codice come questo:

Request.Form[“ctrlAcSearchBox”]

 

Inoltre in una posizione idonea della pagina è necessario includere il file .js che di fatto implementa il box di Auto-Completion lato-client:

 

<script type="text/javascript" src="http://localhost/folder/AutoCompletion.js"></script>

 

E’ possibile scaricare tale file da questo indirizzo: AutoCompletion.js (3,43 KB). In ogni caso, di seguito, è riportato integralmente:

 

function AutoCompletion_getSelection (n, def)

{

    for( i=0; i<10; i ++ )

    {

        var tr = document.getElementById( n + "tr" + i );

        if ( tr == null )

            return def;

        else if ( tr.style.color == "#ffffff" )

            return i;

    }

   

    return def;

}

 

function AutoCompletion_putSelection (n, s)

{

    for( i=0; i<10; i ++ )

    {

        var tr = document.getElementById( n + "tr" + i );

        if ( tr == null )

            return;

       

        if ( s == i )

        {

            tr.style.backgroundColor = "#0040D0";

            tr.style.color = "#ffffff";

        }

        else

        {

            tr.style.backgroundColor = "#ffffff";

            tr.style.color = "#000000";

        }

    }

}

 

function AutoCompletion_getNum (n)

{

    var i;

    for( i=0; i<10; i ++ )

    {

        var tr = document.getElementById( n + "tr" + i );

        if ( tr == null )

            return i;

    }

    return i;

}

 

function AutoCompletion_processResponse (n)

{

    var req = document.getElementById( n + "div" ).req;

    if ( req.readyState == 4 && req.status == 200 )

    {

        document.getElementById( n + "div" ).style.display = "block";

        document.getElementById( n + "div" ).innerHTML = req.responseText;

    }

}

 

function AutoCompletion_setEditText (n)

{

    var sel = AutoCompletion_getSelection (n);

    if ( sel == null )

        return;

 

    document.getElementById( n ).value = document.getElementById( n + "span" + sel ).innerHTML;

    document.getElementById( n + "div" ).style.display = "none";

    document.getElementById( n ).focus ();

}

 

function AutoCompletion_onEditBoxKey (n, key)

{

    if ( key == 27 ) // esc.

    {

        document.getElementById( n + "div" ).style.display = "none";

        event.returnValue = false;

        return;

    }   

 

    if ( key == 38 ) // up.

    {

        var s = AutoCompletion_getSelection (n, 0) - 1;

        if ( s < 0 )

            s = AutoCompletion_getNum(n) - 1;

        AutoCompletion_putSelection( n, s );

        event.returnValue = false;

    }

    else if ( key == 40 ) // down.

    {

        var s = AutoCompletion_getSelection (n, AutoCompletion_getNum(n)-1) + 1;

        if ( s >= AutoCompletion_getNum (n) )

            s = 0;

        AutoCompletion_putSelection( n, s );

        event.returnValue = false;

    }

    else if ( key == 13 ) // enter.

    {

        AutoCompletion_setEditText (n);

        event.returnValue = false;

    }

}

 

function encodeHtml ( s )

{

     s = escape(s);

     s = s.replace(/\//g,"%2F");

     s = s.replace(/\?/g,"%3F");

     s = s.replace(/=/g,"%3D");

     s = s.replace(/&/g,"%26");

     s = s.replace(/@/g,"%40");

     return s;

}

 

function AutoCompletion_onEditBoxKeyChange (base, n, key)

{

    if ( key == 8 || (key >= 32 && key != 38 && key != 40) )

    {

        var url = base + "?str=" + encodeHtml( document.getElementById( n ).value ) + "&rnd=" + Math.floor(Math.random()*1000000000) + "&n=" + n;

   

        var req = new ActiveXObject("Microsoft.XMLHTTP");

        document.getElementById( n + "div" ).req = req;

        req.onreadystatechange = new Function ( "AutoCompletion_processResponse('" + n + "');" );

        req.open ( "GET", url, true );

        req.send ();

    }

}

 

Di seguito è riportata una descrizione di ciascuna funzione JS:

 

AutoCompletion_getSelection: semplicemente ritorna l’indice dell’elemento attualmente selezionato nel box.

 

AutoCompletion_putSelection: seleziona un elemento nel box.

 

AutoCompletion_getNum: restituisce la dimensione del box, come numero di item attualmente visualizzati.

 

AutoCompletion_setEditText: in base alla selezione corrente nel box, popola l’editbox opportunamente.

 

AutoCompletion_onEditBoxKey (associata all’evento “onkeydown” dell’editbox): gestisce la pressione dei tasti quando il focus appartiene all’editbox (tasti: esc, su, giù, enter).

 

AutoCompletion_onEditBoxKeyChange (associata all’evento “onkeyup” dell’editbox): scatena una richiesta GET al server (attraverso l’oggetto "Microsoft.XMLHTTP") alla pressione di un tasto nell’editbox. Come specificato nell’evento “onreadystatechange”, la gestione della risposta da parte del server è demandata alla funzione JS con nome “AutoCompletion_processResponse”.

 

AutoCompletion_processResponse: questa funzione riceve la risposta del server. In questo caso, il server restituisce HTML (direttamente il contenuto del box di Auto-Completion). In altri casi è frequente trovare l’oggetto "Microsoft.XMLHTTP” coinvolto nello scambio di markup XML tra server e client (questo argomento, però, esula dagli obiettivi di questo post).

 

Dietro le quinte, una pagina ASPX si occupa di restituire il markup HTML che costituisce il box di Auto-Completion visualizzato. L’URL di tale pagina ASPX è fornito alla funzione “AutoCompletion_onEditBoxKeyChange” nel gestore dell’evento “onkeyup” del nostro INPUT TYPE=”TEXT” (ossia dell’editbox).

 

Il codice di questa pagina è scaricabile qui (AutoCompletionInterface.aspx (0,45 KB) + AutoCompletionInterface.aspx.cs (2,4 KB)). Il markup della pagina ASPX è trascurabile. La struttura del file di Code-Behind, invece, è molto intuitiva: nel gestore della Page_Load, viene generato dell’HTML secondo la stringa di ricerca passata (ossia ciò che l’utente ha inserito nell’editbox) quindi il markup risultante viene restituito come risposta alla GET della parte JS.

 

Tale markup verrà quindi visualizzato nella seconda DIV di cui sopra, mostrando il box di Auto-Completion:

Nel file di Code-Behind di esempio, viene chiamata una Stored Procedure che potrebbe essere qualcosa del genere:

      SELECT TOP 10

           u.nome_cognome

      FROM Utenti u

     WHERE u.nome_cognome like @Nome_Cognome + '%'

  ORDER BY u.nome_cognome

 


 

Giusto un piccolo update al Blog, in modo da poter dimostrare di essere ancora vivo ed "attivo"... :-)

Dunque un pò di tempo fà mi sono imbattuto nella necessità di dover aggiungere al run-time un file di Style Sheet (tipicamente con estensione ".css") ad una pagina ".aspx" (ASP.NET 2.0). Tipicamente le direttive al browser circa quali file css caricare vengono specificate nella sezione "<head>" di una pagina html: poichè il contesto nel quale mi trovavo era una webpart per DotNetNuke (ossia un controllo ".ascx") e poichè il file ".css" da tirare sù dipendeva da diverse variabili che potevo conoscere solo al run-time, ho dovuto trovare una soluzione al problema, forse un poco fuori dagli schemi.

 

Dunque, il codice è il seguente:

 

 

   Control page = null;

 

   Control c = Parent;

   while (c != null)

   {

       page = c;

       c = c.Parent;

   }

 

   HtmlLink stylesheet = new HtmlLink();

 

   stylesheet.Href = "http://www.someserver.com/stylesheet.css";

   stylesheet.Attributes["rel"] = "stylesheet";

   stylesheet.Attributes["text"] = "text/css";

   ((System.Web.UI.Page)page).Header.Controls.Add(stylesheet);

 

 

In sostanza, nella prima parte viene ottenuta una reference alla pagina principale (.ASPX), mentre nella seconda si procede alla specificazione del file .CSS da caricare sul client.

 

L'esigenza di fare una cosa del genere potrebbe sembrare piuttosto remota, ma, al contrario, se si pensa per esempio alla necessità di incorporare un controllo di terze parti in una webpart per DotNetNuke (come nel mio caso) che magari sia closed-source e che magari richieda un particolare foglio di stile per funzionare correttamente, il codice sopra potrebbe essere di fondamentale aiuto...

 

Se qualcuno dovesse conoscere una soluzione più semplice per ottenere lo stesso risultato, si senta libero di postarla qui come risposta.


 

Per un mio grosso cliente di Milano, sto dirigendo tecnologicamente lo sviluppo di un portale basato su ASP.NET 2.0, dove, funzionalmente, è richiesta l'integrazione di certe logiche di workflowing nella gestione dei loro processi aziendali.

A tale scopo ho proposto l'utilizzo di Windows Workflow Foundation, in modo da poter testare sul campo (aldilà di semplici applicazioni di prova), nel contesto dello sviluppo di un grosso progetto di classe enterprise questa nuova interessante tecnologia di Microsoft.

Dopo qualche ora di ricerca sono riuscito ad accumulare abbastanza conoscenza per mettere sù un proof of concept al fine di collaudare le idee ed ipotesi che avevo maturato. Allegata a questo post, c'è una applicazione ASP.NET di esempio derivata da quel mio primo prototipo.

La Web Application di Esempio

E' possibile scaricare l'applicazione di esempio qui: WwfExample.rar (386,08 KB).

L'applicazione (la solution) consta di due progetti: la Web Application ed il progetto Workflows, che racchiude tutto il codice specifico che consuma i servizi di Workflow Foundation (WF).

Prima di poter utilizzare l'applicazione in questione, è necessario "attaccare" il file di database fornito alla istanza di SQL Server 2005 Express presente sulla macchina di sviluppo.

L'applicazione utilizza tre tabelle per mantenere il suo stato:

La tabella Users mantiene una lista di tutti gli utenti abilitati ad utilizzare l'applicazione, con relativi ruoli separati da virgola. La tabella WorkflowsList mantiene una lista di tutti i workflow che l'applicazione è abilitata a far partire, con, annesse, informazioni circa quali utenti e quali gruppi hanno tale privilegio e, soprattutto, indicazioni circa il tipo .NET e l'assembly dal quale recuperare la definizione della classe di Workflow. La tabella UserActivities mantiene uno stato della applicazione "parallelo" a quello del Workflow ed è la tebella che viene consultata dal sistema per capire in quale stato si trova l'intera applicazione. I record in questa tabella vengono aggiunti direttamente dalle Activity del Workflow ed essa è una prima ma importante forma di comunicazione tra codice ASP.NET e codice di Workflow Foundation.

Sono inoltre presenti anche queste due tabelle:

In particolare, la tabella InstanceState viene utilizzata da Workflow Foundation (specificatamente dal servizio SqlWorkflowPersistenceService) per persistere lo stato di un determinato Workflow: il Workflow viene serializzato attraverso il BinaryFormatter, poi compresso, quindi depositato nel campo blob state di questa tabella. Per creare queste due tabelle, e relative stored procedure, è necessario recuperare ed eseguire gli script presenti tipicamente qui:

C:\WINDOWS\Microsoft.NET\Framework\v3.0\Windows Workflow Foundation\SQL\EN\SqlPersistenceService_Schema.sql

C:\WINDOWS\Microsoft.NET\Framework\v3.0\Windows Workflow Foundation\SQL\EN\SqlPersistenceService_Logic.sql

NOTA: nel file di db incluso in questo esempio, le tabelle e le stored procedure di WF sono già presenti.

Fatta partire, l'applicazione mostra una semplice mascherina di login:

Dove viene appurato il nome dell'utente e, soprattutto, i ruoli a lui assegnati (tramite lookup su db). Verificata la veridicità delle credenziali, viene creato un FormsAuthenticationTicket contenente le informazioni di interesse applicativo sull'utente.

Immediatamente dopo si accede alla form principale dell'applicazione di esempio:

Questa form permette di avviare un dato workflow (se si dispone delle autorizzazioni necessarie per poterlo fare) e di gestire attività pendenti (sempre se si è autorizzati a farlo). La griglia delle attività pendenti (la seconda) viene popolata attraverso la tabella UserActivities su database, che a sua volta viene popolata dal codice del Workflow avviato e gestito dall'applicazione ASP.NET, con nome WorkflowExample.cs:

Questo semplice workflow sequenziale definisce una serie di form ASP.NET da visualizzare una dopo l'altra. A ciascuna form sono associati diritti a livello di utente e ruolo, in modo che soltanto gli utenti abilitati abbiano modo di accedervi. E' naturalmente possibile definire workflow di una complessità molto superiore rispetto a quella del workflow qui rappresentato. Nella fattispecie, è possibile inserire qualsiasi tipo di Activity nel flusso di esecuzione del workflow: se necessario, è possibile definire uno stato privato al workflow aggiungendo proprietà (eventualmente bindabili) nel file WorkflowExample.cs. La proprietà bindabile DataSet nella classe Workflows.UserActivity, come spiegato sotto, può essere utilizzata per scambiare dati tra il workflow e l'applicazione ASP.NET.

Da interfaccia utente, in Visual Studio (ossia dal Workflow Designer) è possibile, per ciascuna form, definire proprietà salienti, tra le quali quelle di autorizzazione appena descritte:

La proprietà DataSet (quando bindata) permette di passare informazioni da una Activity ad un'altra, passando dal codice ASP.NET, nel nostro caso specifico: il sistema, infatti, serializza di volta in volta il contenuto di questa proprietà della Activity nel campo DetailsXml della tabella UserActivities; questo permette al codice della pagina web di modificare il dataset, che poi, trasparentemente, viene ripassato alla Activity del Workflow, in modo che lo stato della proprietà bindabile sia aggiornato opportunamente.

Cliccando su una attività pendente, si accede ad una form di gestione:

Il link Complete Activity scatena il codice che realizza l'interoperabilità con Workflow Foundation: in questo caso e per il nostro Workflow di esempio, l'attività "Form2" viene chiusa e si passa all'attività "Form3", che è giusto la successiva nel nostro workflow sequenziale.

Il funzionamento dell'intera applicazione è piuttosto semplice, se si intendono i meccanismi di intercomunicazione di WF (vedi code di item, o WorkflowQueue). Questo schema dovrebbe riassumere a grandi linee ciò che accade:

La discussione seguente è un poco più complessa, poichè tratta specificatamente dei problemi di interoperabilità tra ASP.NET e WF e delle soluzioni che ho ricercato e deciso di adottare caso per caso.

Threading

Normalmente, Workflow Foundation utilizza le caratteristiche del servizio DefaultWorkflowSchedulerService per creare e coordinare i thread responsabili dell’esecuzione di tutti i workflow associati ad uno stesso runtime (WorkflowRuntime). Nella fattispecie, questo implica l’esecuzione asincrona delle istanze di workflow gestite da un runtime, che vengono opportunamente e trasparentemente messe nella coda di esecuzione del thread pool .NET associato all’applicazione host.

Nel contesto di una applicazione ASP.NET, questo, generalmente, non è desiderabile. Infatti, il DefaultWorkflowSchedulerService, quando utilizzato in una applicazione web, và ad impegnare un thread aggiuntivo per ciascuna richiesta HTTP che il server di IIS, in un dato momento, sta processando e che richiede le funzionalità di Workflow Foundation. Per indirizzare questo problema, è necessario specificare il servizio ManualWorkflowSchedulerService, quando si inizializza una istanza del WorkflowRuntime:

Runtime = new WorkflowRuntime();

[...]

Runtime.AddService(new ManualWorkflowSchedulerService(true));

[...]

Questo permette di controllare l’esecuzione di un dato workflow, di coordinarla rispetto al thread che sta servendo la richiesta HTTP e di disimpegnare un thread dal thread pool dell’applicazione host, poichè WF và ad eseguire le nostre activity nel contesto dello stesso thread che sta eseguendo la pagina ASP.NET.

Quanto detto è possibile evincerlo empiricamente andando a sbirciare nel call stack .NET durante l’esecuzione dell’applicazione di esempio allegata a questo post:

UserActivity.Execute è il nostro codice applicativo che implementa la nostra Activity (il primo contesto evidenziato in blu). API.CompleteActivity (il secondo contesto in blu) è un nostro metodo chiamato direttamente da un gestore eventi di un pulsante di una webform, che chiama l’API ManualWorkflowSchedulerService.RunWorkflow, che provoca l’esecuzione immediata del workflow il cui ID passiamo come parametro. Come anticipato, sia il codice della pagina web che quello del workflow vengono eseguiti nel contesto dello stesso thread.

Transazionalità e Persistenza

La classe TransactionScope (System.Transactions) permette di definire contesti transazionali in codice .NET 2.0 e 3.0 in maniera semplice ed immediata. Specialmente se si utilizza SQL Server 2005, l’uso della classe TransactionScope diviene ancora più semplice, poichè si viene liberati del tutto da tutte quelle considerazioni di cui tener conto circa eventuali, inutili e costosi coinvolgimenti del MSDTC per transazioni puramente locali, importanti invece quando si lavora, per esempio, con SQL Server 2000. Sorvolando sulle specifiche caratteristiche della classe TransactionScope (fuori dagli obiettivi di questo post), nell’esempio fornito viene usata questa nuova funzionalità di .NET 2.0 per la scrittura di codice transazionale che scrive su SQL Server.

Uno dei requisiti principali per realizzare una consistente interoperabilità tra WF e ASP.NET è la corretta gestione della persistenza dei workflow su database. Per comunicare a Workflow Foundation di persistere tutti i workflow relativi ad uno stesso WorkflowRuntime su database, è necessario registrare il servizio SqlWorkflowPersistenceService all’atto della inizializzazione dell’istanza del WorkflowRuntime:

Runtime = new WorkflowRuntime();

[...]

NameValueCollection parameters = new NameValueCollection();
parameters.Add("ConnectionString", connectionString);
parameters.Add("UnloadOnIdle", "false");
Runtime.AddService(new SqlWorkflowPersistenceService(parameters));

[...]

Come parametri, specifichiamo la stringa di connessione al database ed indichiamo al servizio di NON persistere un workflow su database quando questo è inattivo (parametro UnloadOnIdle): infatti, per un maggiore controllo e per ragioni di consistenza transazionale che vedremo tra poco, ci preoccupiamo noi di chiamare il metodo WorkflowInstance.Unload quando intendiamo persistere una istanza di Workflow su db.

Infatti uno dei maggiori problemi quando si intende integrare della logica che accede ad un db in una istanza di Workflow con della logica dello stesso tipo eseguita esternamente nel contesto dell’applicazione host (in questo caso ASP.NET) è che WF, per ragioni di design, impedisce alla transazione ambientale associata al thread di esecuzione nel momento in cui si esegue il workflow (ManualWorkflowSchedulerService.RunWorkflow) di propagarsi fino al contesto di esecuzione nel quale vengono eseguite le Activity del workflow. Questo è possibile evincerlo empiricamente, per esempio, debuggando l’applicazione di esempio di questo post: prima di chiamare l’API ManualWorkflowSchedulerService.RunWorkflow, la proprietà statica System.Transactions.Transaction.Current è correttamente valorizzata, poichè siamo in uno scope transazionale definito da un costrutto TransactionScope. Al contrario, all’interno del metodo UserActivity.Execute (che implementa la nostra activity) tale proprietà statica ha valore nullo, benchè la chiamata al metodo UserActivity.Execute sia originata a partire da del codice configurato per essere transazionale.

Volendo approfondire la questione ed i motivi di tale comportamento, è necessario ritornare al call stack rappresentato all’inizio di questo post: nella fattispecie, in una delle 9 chiamate che separano il codice host dal codice della Activity, WF ha intenzionalmente “soppresso” la nostra transazione ambientale. Alla fine, come risultato, questo ci impedisce di associare del codice che accede al db in una Activity di un Workflow ad una transazione iniziata dall’host.

Volendo scendere nel merito, magari utilizzando l’ottimo .NET Reflector di Roeder e spulciando rapidamente in quei 9 metodi di cui sopra, è possibile trovare la spiegazione di questo comportamento nel metodo System.Workflow.Runtime.WorkflowExecutor.RunSome, che, decompilato, appare così:

internal void RunSome(object ignored)
{
   [...]

   using (TransactionScope scope = new TransactionScope(TransactionScopeOption.Suppress))
   {
      try
      {
         this.FireWorkflowExecutionEvent(this, WorkflowEventInternal.Executing);
         this.RunScheduler();
      }
      catch (Exception exception)
      {

         [...]

      }
      finally
      {
         this.FireWorkflowExecutionEvent(this, WorkflowEventInternal.NotExecuting);
      }

      scope.Complete();
   }

   [...]
}

Questo ci obbliga a trovare nuove soluzioni per scrivere sul database in maniera consistente.

La soluzione che ho ricercato ed adottato sfrutta i meccanismi di serializzazione di .NET, per mettere “in coda” una o più operazioni che è necessario associare ad una transazione controllata dal codice host (ossia da noi). Per ottenere l’effetto desiderato, bisogna, per prima cosa, creare una coda di Workflow dove andare ad aggiungere istanze di un nostro tipo custom (con nome DbAccessCallDetails), che rappresenta una chiamata ad un metodo di qualsiasi tipo che interagisce col db. L’idea di base è di registrare semplicemente l’operazione di interazione col db in questa coda (quando si è all’interno di una Activity), quindi, andare a chiamare il metodo che implementa tale operazione in un secondo momento, per esempio all’atto della serializzazione del Workflow su database. Considerando che la coda di Workflow (WorkflowQueue) è un oggetto che appartiene al Workflow e che le nostre istanze di DbAccessCallDetails si trovano depositate in tale coda, quando il servizio SqlWorkflowPersistenceService, tramite il BinaryFormatter, andrà a serializzare, quindi comprimere e persistere su db il nostro Workflow, noi avremo l’occasione che aspettiamo per svuotare la nostra coda e quindi eseguire quei metodi di cui avevamo procrastinato l’esecuzione.

La classe DbAccessCallDetails si presenta semplicemente così:

[Serializable()]
public class DbAccessCallDetails : ISerializable
{
   // constructor(s).
   public DbAccessCallDetails(Delegate fnParam, object[] psParam)
   {
      fn = fnParam;
      ps = psParam;
   }
   public DbAccessCallDetails(SerializationInfo info, StreamingContext context)
   {
   }

   // data.
   public Delegate fn = null;
   public object[] ps = null;

   // ISerializable stuff.
   void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
   {
      // call the method.
      if (fn != null)
         fn.DynamicInvoke(ps);
   }
}

In breve, quando si cerca di serializzare una istanza di questa classe, viene chiamato il metodo ISerializable.GetObjectData, che semplicemente invoca il metodo ad essa associato. Questo metodo inserisce dei record su db, nel caso del mio esempio (tabella UserActivities).

Come detto precedentemente, la serializzazione del Workflow avviene all’atto della chiamata al metodo WorkflowInstance.Unload. Come emerge da vari documenti e testimonianze su internet, e come può essere approfondito direttamente attraverso Reflector, questo metodo (intenzionalmente) è l’unico pezzo di codice di WF che non sopprime attivamente la transazione ambientale del thread che lo sta chiamando. Questo significa che è possibile associare ad una stessa transazione ambientale gestita dall’host sia la persistenza dell’istanza del Workflow su database, sia il codice rappresentato dalle varie istanze di DbAccessCallDetails eventualmente presenti nella coda del Workflow. Questo “trucco” ci permette di avere sul database uno stato dei dati di interoperabilità tra la nostra applicazione e Workflow Foundation sempre consistenti:

public static void CompleteActivity(Guid workflowGuid, Guid activityGuid, DataSet activityData, System.Web.HttpRequest Request, System.Web.HttpResponse resp)
{
   // notify the completion to the wf.
   WorkflowInstance wi = Runtime.GetWorkflow(workflowGuid);
   wi.EnqueueItem(
      activityGuid.ToString(),
      new ActivityCompletedEventArgs(activityData),
      null, null);

   // tell the workflow to progress.
   string redirectUrl = null;
   using (TransactionScope scope = new TransactionScope())
   {
      // complete on the db.
      using (Workflows.DataTableAdapters.UserActivitiesTableAdapter ta = new Workflows.DataTableAdapters.UserActivitiesTableAdapter())
      {
         // complete on the db.
         ta.CompleteActivity(activityGuid.ToString());

         // get the redirect page url.
         redirectUrl = (string)ta.GetDataByActivityGuid(activityGuid.ToString()).Rows[0]["ReturnPageUrl"];
         redirectUrl = "/" + (Request.ApplicationPath + "/" + redirectUrl).Trim('/');
      }

      // call the workflow.
      Runtime.GetService<ManualWorkflowSchedulerService>().RunWorkflow(workflowGuid);

      // persist the workflow on the db.
      wi.Unload();

      // commit.
      scope.Complete();
   }

   // redirect.
   resp.Redirect(redirectUrl);
}

Il metodo CompleteActivity permette di chiudere l’Activity attualmente aperta, di avanzare nell’esecuzione del Workflow (quindi eseguendo le Activity successive) e soprattutto di aggiornare lo stato sul db. Questo avviene all’atto della chiamata al metodo wi.Unload(), che, secondo quanto detto in precedenza, persiste il workflow su db ed esegue le varie istanze di DbAccessCallDetails, tutto quanto associato alla stessa System.Transactions.Transaction (TransactionScope).


 
August 26, 2007
@ 10:33 PM

Salve, mi chiamo Vito Plantamura. Come è abitudine in tutti i blog che si rispettino, il primo post deve essere di tipo introduttivo.

Come potete intuire dalla natura del sito, questo blog conterrà principalmente informazioni, notizie ed esperienze di tipo tecnico. Le tecnologie che saranno coperte coincidono sostanzialmente con quelle che sono le mie specializzazioni professionali:

  • .NET Framework / C# / Sviluppo di applicazioni Enterprise.
  • ASP.NET / HTML / Javascript.
  • Win32 / MFC / COM (+) / C++.
  • .NET Framework 3.0 / WinFX.
  • Assembler IA32 / IA64 / X64 e sviluppo di driver per Windows NT.
  • Argomenti relativi alla sicurezza internet e Windows di ogni tipo.

Personalmente, potete trovare informazioni dettagliate su alcuni dei miei prodotti e tecnologie che ho sviluppato e "ricercato" negli anni proprio in questo sito (www.VitoPlantamura.com). Mi sento di consigliare di provare GoToTerminal, il nostro servizio di connettività remota basata su Terminal Services e VNC, NDIS Monitor, un packer sniffer sviluppato mistamente con .NET ed il DDK di Windows, BugChecker, un prodotto di ricerca sviluppato interamente dalla mia azienda ricalcato sul modello del famoso kernel debugger SoftICE, MapGen, un editor di tipo CSG di classe professionale, sviluppato diversi anni fà dal sottoscritto per la creazione di contenuto di gioco per un motore tridimensionale alla Quake3.

Concludendo, al momento mi occupo di consulenza sulle tecnologie sopra elencate (attraverso la mia azienda: VPC Technologies) e parallelamente seguo lo sviluppo tecnologico e commerciale dei miei prodotti. Se intendete ottenere maggiori informazioni sulle nostre attività di consulenza e sul nostro/mio curriculum, vi pregherei di contattarmi direttamente al mio indirizzo personale (o, alternativamente, attraverso il sito della mia company).

Inoltre spero di poter replicare quanto prima questo blog (inizialmente solo in italiano) anche in inglese, in modo che anche i miei amici/colleghi/clienti non italiani possano seguirmi in questa nuova avventura... :-) :-) :-)

Detto questo, posso solo augurarvi una buona lettura... :-)

vito