Basic MFC Reversing
From UIC
Guide linea al reversing di MFC
Contents |
| Infos | |
|---|---|
| Author: | Pnluck |
| Email: | pnluck@virgilio.it |
| Website: | http://pnluck.netsons.org |
| Date: | 25/08/2008 (dd/mm/yyyy) |
| Level: |
|
| Language: | Italian |
| Comments: | Questo è reversing, non è cracking! |
Tools - Riferimenti
IDA
Reversing Microsoft Visual C++ Part II: Classes, Methods and RTTI
Crackme
Prologo: Che cosa è MFC?
Microsoft Foundation Classes, meglio conosciuta con l'acronimo MFC, è una libreria di classi C++ prodotta da Microsoft con lo scopo di incapsulare le Windows API. È definita una classe per ciascun oggetto di Windows dotato di handle, e anche per le finestre predefinite e per i controlli comuni
Introduzione
I programmi scritti con MFC possono importare o no MFC80U.dll (è il nome dell'ultima versione, mentre sto scrivendo, della dll che gestisce MFC), tutto dipende dall' opzione usata per compilare MFC: static library o shared DLL.
In questa guida analizzeremo un programma che importa quella dll e che ha le info per il debug, tutto ciò per facilitarci l'analisi.
Una volta capito come funge MFC, vi sarà più semplice comprendere qualsiasi programma scritto con MFC, anche compilato staticamente: in questo caso basta applicare le signature MFC e VisualC in IDA.
Essay
Inizio col mostrarvi una parte di source, di un programma scritto solo col C:
{
switch(uMsg)
{
case WM_COMMAND:
switch(LOWORD(wParam))
{
case IDC_ABOUT:
DialogBoxParam(GetModuleHandle(NULL), MAKEINTRESOURCE(IDD_DIALOG1), NULL, (DLGPROC)MainDialogProc, 0);
break;
// ...
}
}
}
Ed ecco l'equivalente in MFC:
{
public:
CAboutDlg();
// Dialog Data
enum { IDD = IDD_ABOUTBOX };
protected:
virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV support
// Implementation
protected:
DECLARE_MESSAGE_MAP()
};
CAboutDlg::CAboutDlg() : CDialog(CAboutDlg::IDD) //al costruttore viene passato l'ID della dialog
//presente nel resource file
{
}
void CAboutDlg::DoDataExchange(CDataExchange* pDX)
{
CDialog::DoDataExchange(pDX);
}
BEGIN_MESSAGE_MAP(CAboutDlg, CDialog) //Mappa dei messaggi della dialog: e' tipo la DialogProc della Dialog.
END_MESSAGE_MAP()
// App command to run the dialog
void CProvaRevApp::OnAppAbout()
{
CAboutDlg aboutDlg;
aboutDlg.DoModal();
}
Come potete immaginare il disasm di un programma sviluppato con MFC è pò più complesso da analizzare.
Il Main di MFC
Il disasm del main si presenta in questo modo:
.text:00401CBB call ___security_init_cookie
.text:00401CC0 jmp ___tmainCRTStartup
.text:004019FB ___tmainCRTStartup proc near ; CODE XREF: start+5�j
.text:004019FB
.text:004019FB push 5Ch
.text:004019FD push offset unk_403DD8
.text:00401A02 call __SEH_prolog4
;... altro codice di inizializzazione
.text:00401B3E push ecx ; nShowCmd
.text:00401B3F push eax ; lpCmdLine
.text:00401B40 push ebx ; hPrevInstance
.text:00401B41 push 400000h ; hInstance
.text:00401B46 call _wWinMain@16 ; wWinMain(x,x,x,x)
; int __stdcall wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int nShowCmd)
_wWinMain@16 proc near
jmp ?AfxWinMain@@YGHPAUHINSTANCE__@@0PA_WH@Z ; AfxWinMain(HINSTANCE__ *,HINSTANCE__ *,wchar_t *,int)
_wWinMain@16 endp
Come vedete la funzione WinMain non fa altro che chiamare AfxWinMain. Se avete il VisualStudio potete vedere il source di MFC, qui vi riporto solo le funzioni necessarie.
_In_ LPTSTR lpCmdLine, int nCmdShow)
{
ASSERT(hPrevInstance == NULL);
int nReturnCode = -1;
CWinThread* pThread = AfxGetThread();
CWinApp* pApp = AfxGetApp();
// AFX internal initialization
if (!AfxWinInit(hInstance, hPrevInstance, lpCmdLine, nCmdShow))
goto InitFailure;
// App global initializations (rare)
if (pApp != NULL && !pApp->InitApplication())
goto InitFailure;
// Perform specific initializations
if (!pThread->InitInstance())
{
if (pThread->m_pMainWnd != NULL)
{
TRACE(traceAppMsg, 0, "Warning: Destroying non-NULL m_pMainWnd\n");
pThread->m_pMainWnd->DestroyWindow();
}
nReturnCode = pThread->ExitInstance();
goto InitFailure;
}
nReturnCode = pThread->Run();
InitFailure:
AfxWinTerm();
return nReturnCode;
}
Ora vi mostro il disasm di questa funzione
.text:7831D2D2 AfxWinMain proc near
.text:7831D2D2 push ebx
.text:7831D2D3 push esi
.text:7831D2D4 push edi
.text:7831D2D5 or ebx, 0FFFFFFFFh
.text:7831D2D8 call AfxGetModuleThreadState
.text:7831D2DD mov esi, [eax+4] ;pThread
.text:7831D2E0 call AfxGetModuleState
.text:7831D2E5 push [esp+0Ch+arg_C]
.text:7831D2E9 mov edi, [eax+4] ;pApp
.text:7831D2EC push [esp+10h+arg_8]
.text:7831D2F0 push [esp+14h+arg_4]
.text:7831D2F4 push [esp+18h+arg_0]
.text:7831D2F8 call AfxWinInit
.text:7831D2FD test eax, eax
.text:7831D2FF jz short loc_7831D33D
.text:7831D301 test edi, edi
.text:7831D303 jz short loc_7831D313
.text:7831D305 mov eax, [edi]
.text:7831D307 mov ecx, edi
.text:7831D309 call dword ptr [eax+98h]
.text:7831D30F test eax, eax
.text:7831D311 jz short loc_7831D33D
.text:7831D313
.text:7831D313 loc_7831D313:
.text:7831D313 mov eax, [esi]
.text:7831D315 mov ecx, esi
.text:7831D317 call dword ptr [eax+58h]
.text:7831D31A test eax, eax
.text:7831D31C jnz short loc_7831D334
.text:7831D31E cmp [esi+20h], eax
.text:7831D321 jz short loc_7831D32B
.text:7831D323 mov ecx, [esi+20h]
.text:7831D326 mov eax, [ecx]
.text:7831D328 call dword ptr [eax+68h]
.text:7831D32B
.text:7831D32B loc_7831D32B:
.text:7831D32B mov eax, [esi]
.text:7831D32D mov ecx, esi
.text:7831D32F call dword ptr [eax+70h]
.text:7831D332 jmp short loc_7831D33B
.text:7831D334
.text:7831D334 loc_7831D334:
.text:7831D334 mov eax, [esi]
.text:7831D336 mov ecx, esi
.text:7831D338 call dword ptr [eax+5Ch]
.text:7831D33B
.text:7831D33B loc_7831D33B:
.text:7831D33B mov ebx, eax
.text:7831D33D
.text:7831D33D loc_7831D33D:
.text:7831D33D call AfxWinTerm
.text:7831D342 pop edi
.text:7831D343 pop esi
.text:7831D344 mov eax, ebx
.text:7831D346 pop ebx
.text:7831D347 retn 10h
.text:7831D347 AfxWinMain endp
Come vedete ci sono molte call del tipo call [eax+XXh], infatti con la chiamata a AfxGetApp (e AfxGetThread), ritorna un puntatore ad una struttura che contiene l'offset di tutte le funzioni usate da MFC.
Nel caso del programma che sto analizzando, edi (pApp) è 405498, ed a quest'indirizzo troviamo un valore, che non è altro l'offset a 40349C, e lì si trova proprio un' istanza statica, la virtual functions table, di CWinApp:
.rdata:004034A0 dd offset sub_401010
.rdata:004034A4 dd offset nullsub_1
.rdata:004034A8 dd offset nullsub_2
.rdata:004034AC dd offset nullsub_1
.rdata:004034B0 dd offset ?OnCmdMsg@CCmdTarget@@UAEHIHPAXPAUAFX_CMDHANDLERINFO@@@Z ; CCmdTarget::OnCmdMsg(uint,int,void *,AFX_CMDHANDLERINFO *)
.rdata:004034B4 dd offset ?OnFinalRelease@CCmdTarget@@UAEXXZ ; CCmdTarget::OnFinalRelease(void)
.rdata:004034B8 dd offset ?IsInvokeAllowed@CCmdTarget@@UAEHJ@Z ; CCmdTarget::IsInvokeAllowed(long)
.rdata:004034BC dd offset ?GetDispatchIID@CCmdTarget@@UAEHPAU_GUID@@@Z ; CCmdTarget::GetDispatchIID(_GUID *)
.rdata:004034C0 dd offset ?GetTypeInfoCount@CCmdTarget@@UAEIXZ ; CCmdTarget::GetTypeInfoCount(void)
.rdata:004034C4 dd offset ?GetTypeLibCache@CCmdTarget@@UAEPAVCTypeLibCache@@XZ ; CCmdTarget::GetTypeLibCache(void)
.rdata:004034C8 dd offset ?GetTypeLib@CCmdTarget@@UAEJKPAPAUITypeLib@@@Z ; CCmdTarget::GetTypeLib(ulong,ITypeLib * *)
.rdata:004034CC dd offset sub_401000
;.......................................................
Spero che a questo punto vi sorga una curiosità: da dove ricava quell'indirizzo? Analizzando i riferimenti con IDA...
.text:004023B0 push 0
.text:004023B2 mov ecx, offset dword_405498
.text:004023B7 call ??0CWinApp@@QAE@PB_W@Z ; CWinApp::CWinApp(wchar_t const *)
.text:004023BC push offset sub_4023F0 ; void (__cdecl *)()
.text:004023C1 mov dword_405498, offset off_40349C ;<-- ecco il nostro offset
.text:004023CB call _atexit
.text:004023D0 pop ecx
.text:004023D1 retn
.text:004023D1 sub_4023B0 endp
E l'offset di questa funzione è presenta in una struttura
.rdata:00403305 db 0
.rdata:00403306 db 0
.rdata:00403307 db 0
.rdata:00403308 dd offset _pre_cpp_init
.rdata:0040330C dd offset ??__E_afxInitAppState@@YAXXZ ; `dynamic initializer for '_afxInitAppState''(void)
.rdata:00403310 dd offset sub_4023B0
che viene passata alla funzione __initterm, chiamata prima del WinMain
.text:00401AB1 push offset unk_403304
.text:00401AB6 call _initterm
Dopo questo breve excursus, ritorniamo ad analizzare AfxWinMain:
vedendo la tabella si capisce che call dword ptr [eax+98h] (40349C + 98 = 00403534) non fa altro che chiamare
Mentre call dword ptr [eax+58h], che sarebbe pThread->InitInstance, invece chiama la funzione:
Questa call non fa altro che far apparire la Dialog del programma, ecco la parte principale di codice:
.text:00401030 push ebp
.text:00401031 mov ebp, esp
;..........................................................................
.text:0040109F call sub_401130
;--------------------------------------------------------------------------
;entrato nella call
.text:00401155 push 0 ; lpIconName
.text:00401157 push 66h ; ID della Dialog da caricare
.text:00401159 mov ecx, esi
.text:0040115B call ??0CDialog@@QAE@IPAVCWnd@@@Z ; CDialog::CDialog(uint,CWnd *)
.text:00401160 mov [esp+14h+var_4], 0
.text:00401168 mov dword ptr [esi], offset off_403744 ;virtual functions table offset che viene salvato
; in CDialog.DoModal -> CDialog__PreModal -> AfxHookWindowCreate
.text:0040116E call ?AfxGetModuleState@@YGPAVAFX_MODULE_STATE@@XZ ; AfxGetModuleState(void)
;esco dalla call
;---------------------------------------------------------------------------
.text:004010A4 lea edx, [esp+8+arg_4]
.text:004010A8 mov [esp+8+arg_88], 0
.text:004010B3 mov ecx, edx
.text:004010B5 mov [esi+20h], edx
.text:004010B8 call ?DoModal@CDialog@@UAEHXZ ; CDialog::DoModal(void)
.text:004010BD lea ecx, [esp+8+arg_4]
.text:004010C1 mov [esp+8+arg_88], 0FFFFFFFFh
.text:004010CC call ??1CDialog@@UAE@XZ ; CDialog::~CDialog(void)
;..........................................................................
.text:004010E3 mov esp, ebp
.text:004010E5 pop ebp
.text:004010E6 retn
Ricaviamo la MESSAGE_MAP
Ora dobbiamo capire dove è MESSAGE_MAP: essa viene ricavata in
{
// ....
const AFX_MSGMAP* pMessageMap;
pMessageMap = GetMessageMap();
// ....
if ((lpEntry = AfxFindMessageEntry(pMessageMap->lpEntries, message, 0, 0)) != NULL)
// ...
}
Ecco a voi il disasm
.text:78312E93 mov ecx, edi
.text:78312E95 call dword ptr [eax+30h] ; eax+30h = 00403774, sarebbe a dire GetMessageMap()
;.rdata:00403774 dd offset sub_4011E0
;...................................................................
.text:78312F1B push 0
.text:78312F1D push 0
.text:78312F1F jnb short loc_78312F67
.text:78312F21 push [ebp+arg_0] ;messagge
.text:78312F24 push dword ptr [esi+4] ; lpEntries (in questo caso 0040362C)
.text:78312F27 call AfxFindMessageEntry
La call in 78312E95 chiama:
.text:004011E0 mov eax, offset off_403628 ;eax = pMessageMap
.text:004011E5 retn
;----------------------------------------------------------------
;pMessageMap
.rdata:00403628 off_403628 dd offset ?GetThisMessageMap@CDialog@@KGPBUAFX_MSGMAP@@XZ
.rdata:00403628 ; CDialog::GetThisMessageMap(void)
.rdata:0040362C dd offset unk_403580 ;pMessageMap->lpEntries
Ed a 403580 c'è proprio MESSAGE_MAP per la dialog.
Quindi possiamo ricavare la MessageMap velocemente in questo modo:
- Cerchiamo prima di CDialog:DoModal un mov dword ptr [esi], offset off_XXXXXX (serve a caricare la virtual functions table).
- Aggiungiamo a quell'offset 0x30 per trovare la funzione GetMessageMap: andate nella funzione e troverete un mov eax, offset off_XXXXXX, dove eax è pMessageMap
- Aggiungete a pMessageMap 4, e troverete la MESSAGE_MAP della Dialog
Ora vi mostro come funziona: ecco a voi il resource della dialog che ho preso in esame:
CONTROL "About", 1007, BUTTON, //1007 = 0x3ef
CONTROL "Cancel", 1008, BUTTON, //1008 = 0x3f0
{
UINT nMessage; // windows message
UINT nCode; // control code or WM_NOTIFY code
UINT nID; // control ID (or 0 for windows messages)
UINT nLastID; // used for entries specifying a range of control id's
UINT_PTR nSig; // signature type (action) or pointer to message #
AFX_PMSG pfn; // routine to call (or special value)
};
.rdata:00403584 dd 0
.rdata:00403588 dd 0
.rdata:0040358C dd 0
.rdata:00403590 dd 1Eh
.rdata:00403594 dd offset sub_4012D0
.rdata:00403598 dd 0Fh
.rdata:0040359C dd 0
.rdata:004035A0 dd 0
.rdata:004035A4 dd 0
.rdata:004035A8 dd 13h
.rdata:004035AC dd offset sub_401370
.rdata:004035B0 dd 37h
.rdata:004035B4 dd 0
.rdata:004035B8 dd 0
.rdata:004035BC dd 0
.rdata:004035C0 dd 28h
.rdata:004035C4 dd offset sub_401450
.rdata:004035C8 dd 111h
.rdata:004035CC dd 0
.rdata:004035D0 dd 3EFh
.rdata:004035D4 dd 3EFh
.rdata:004035D8 dd 38h
.rdata:004035DC dd offset sub_401460
.rdata:004035E0 dd 111h
.rdata:004035E4 dd 0
.rdata:004035E8 dd 3F0h
.rdata:004035EC dd 3F0h
.rdata:004035F0 dd 38h
.rdata:004035F4 dd offset sub_4014F0
.rdata:004035F8 dd 111h
.rdata:004035FC dd 0
.rdata:00403600 dd 3EEh
.rdata:00403604 dd 3EEh
.rdata:00403608 dd 38h
.rdata:0040360C dd offset sub_401510
.rdata:00403610 dd 0
...
Come vedete per ogni evento è stata creata un struttura, nella quale vengono memorizzati gli ID della window, se presenti, con la funzione associata.
IDC Script
Per semplificare l'analisi della struttura ho fatto un piccolo IDC script per IDA
#include <idc.idc>
//non tutti i WM_ vengono identificati
static messageName(ptr, message) {
if(message == 1) // WM_CREATE
MakeComm(ptr, "WM_CREATE");
else if(message == 2) // WM_DESTROY
MakeComm(ptr, "WM_DESTROY");
else if(message == 5) // WM_SIZE
MakeComm(ptr, "WM_SIZE");
else if(message == 0x10) // WM_CLOSE
MakeComm(ptr, "WM_CLOSE");
else if(message == 0x18) // WM_SHOWWINDOW
MakeComm(ptr, "WM_SHOWWINDOW");
else if(message == 0x0100) // WM_KEYDOWN
MakeComm(ptr, "WM_KEYDOWN");
else if(message == 0x0101) // WM_KEYUP
MakeComm(ptr, "WM_KEYUP");
else if(message == 0x0102) // WM_CHAR
MakeComm(ptr, "WM_KEYCHAR");
else if(message == 0x0110) // WM_INITDIALOG
MakeComm(ptr, "WM_INITDIALOG");
else if(message == 0x0111) // WM_COMMAND
MakeComm(ptr, "WM_COMMAND");
else if(message == 0x0112) // WM_SYSCOMMAND
MakeComm(ptr, "WM_SYSCOMMAND");
else if(message == 0x0113) // WM_TIMER
MakeComm(ptr, "WM_TIMER");
else if(message == 0x0116) // WM_INITMENU
MakeComm(ptr, "WM_INITMENU");
else if(message == 0x0117) // WM_INITMENUPOPUP
MakeComm(ptr, "WM_INITMENUPOPUP");
else if(message == 0x0126) // WM_MENUCOMMAND
MakeComm(ptr, "WM_MENUCOMMAND");
}
static DefineStruct() {
auto idStruct;
idStruct = AddStrucEx(-1,"AFX_MSGMAP_ENTRY",0);
if(idStruct == 0) return 0;
if(AddStrucMember(idStruct, "nMessage", 0, FF_DWRD|FF_DATA, -1, 4) != 0) {
Warning("\n1\n");
DelStruc(idStruct);
return 0;
}
if(AddStrucMember(idStruct, "nCode", 4, FF_DWRD|FF_DATA, -1, 4) != 0) {
Warning("\n2\n");
DelStruc(idStruct);
return 0;
}
if(AddStrucMember(idStruct, "nID", 8, FF_DWRD|FF_DATA, -1, 4) != 0) {
Warning("\n3\n");
DelStruc(idStruct);
return 0;
}
if(AddStrucMember(idStruct, "nLastID", 12, FF_DWRD|FF_DATA, -1, 4) != 0) {
Warning("\n4\n");
DelStruc(idStruct);
return 0;
}
if(AddStrucMember(idStruct, "nSignature", 16, FF_DWRD|FF_DATA, -1, 4) != 0) {
Warning("\n5\n");
DelStruc(idStruct);
return 0;
}
if(AddStrucMember(idStruct, "pFunction", 20, FF_DWRD|FF_0OFF, -1, 4) != 0) {
Warning("\n6\n");
DelStruc(idStruct);
return 0;
}
return idStruct;
}
static GenerateMFCMap(addr) {
auto idStruct, ptr, message, isOk;
idStruct = GetStrucIdByName("AFX_MSGMAP_ENTRY");
if( idStruct == -1) {
idStruct = DefineStruct();
if(idStruct == 0) {
Warning("\nImpossible declare the structure\n");
return;
}
}
ptr = addr;
isOk = 1;
while( Dword(ptr) != 0) {
if(MakeStructEx(ptr, 24, "AFX_MSGMAP_ENTRY") == 0) {
isOk = 0;
break;
}
messageName(ptr,Dword(ptr));
ptr = ptr + 24;
}
if(isOk == 0) {
Warning("\nImpossible set the structure at %x\n", addr);
} else {
Message("Complete");
}
return;
}
Ecco lo script applicato alla MessageMap precedente
.rdata:00403580 ; DATA XREF: .rdata:0040362C�o
.rdata:00403598 AFX_MSGMAP_ENTRY <0Fh, 0, 0, 0, 13h, offset sub_401370>
.rdata:004035B0 AFX_MSGMAP_ENTRY <37h, 0, 0, 0, 28h, offset sub_401450>
.rdata:004035C8 AFX_MSGMAP_ENTRY <111h, 0, 3EFh, 3EFh, 38h, offset sub_401460> ; WM_COMMAND
.rdata:004035E0 AFX_MSGMAP_ENTRY <111h, 0, 3F0h, 3F0h, 38h, offset sub_4014F0> ; WM_COMMAND
.rdata:004035F8 AFX_MSGMAP_ENTRY <111h, 0, 3EEh, 3EEh, 38h, offset sub_401510> ; WM_COMMAND
.rdata:00403610 db 0
Catturiamo WM_COMMAND
La gestione del messaggio WM_COMMAND è affidata alla funzione BOOL CCmdTarget::OnCmdMsg(UINT nID, int nCode, void* pExtra, AFX_CMDHANDLERINFO* pHandlerInfo), ma più esattamente a _AfxDispatchCmdMsg.
Infatti se settiamo un bp sull'entry della funzione (7833B515 nel mio caso), potete notare che appena premete su un pulsante o su un menù, il debugger ferma l'esecuzione, e steppando potremo arrivare comodamente alla funzione che gestisce quel determinato evento, senza dover ricavare MESSAGE_MAP.
Note Finali
Ringrazio calorosamente la M$ per MFC (una cosa più semplice da reversare no??), e poi Nt, EvilCry, Que, emdel ed ocean che ho incontrato al MOCA 08 dove c'era anche John T. Draper aka Captain Crunch (mitiko!!!), e tutta la ciurmaglia di IRC.
Pnluck
Disclaimer
I documenti qui pubblicati sono da considerarsi pubblici e liberamente distribuibili, a patto che se ne citi la fonte di provenienza. Tutti i documenti presenti su queste pagine sono stati scritti esclusivamente a scopo di ricerca, nessuna di queste analisi è stata fatta per fini commerciali, o dietro alcun tipo di compenso. I documenti pubblicati presentano delle analisi puramente teoriche della struttura di un programma, in nessun caso il software è stato realmente disassemblato o modificato; ogni corrispondenza presente tra i documenti pubblicati e le istruzioni del software oggetto dell'analisi, è da ritenersi puramente casuale. Tutti i documenti vengono inviati in forma anonima ed automaticamente pubblicati, i diritti di tali opere appartengono esclusivamente al firmatario del documento (se presente), in nessun caso il gestore di questo sito, o del server su cui risiede, può essere ritenuto responsabile dei contenuti qui presenti, oltretutto il gestore del sito non è in grado di risalire all'identità del mittente dei documenti. Tutti i documenti ed i file di questo sito non presentano alcun tipo di garanzia, pertanto ne è sconsigliata a tutti la lettura o l'esecuzione, lo staff non si assume alcuna responsabilità per quanto riguarda l'uso improprio di tali documenti e/o file, è doveroso aggiungere che ogni riferimento a fatti cose o persone è da considerarsi PURAMENTE casuale. Tutti coloro che potrebbero ritenersi moralmente offesi dai contenuti di queste pagine, sono tenuti ad uscire immediatamente da questo sito.
Vogliamo inoltre ricordare che il Reverse Engineering è uno strumento tecnologico di grande potenza ed importanza, senza di esso non sarebbe possibile creare antivirus, scoprire funzioni malevoli e non dichiarate all'interno di un programma di pubblico utilizzo. Non sarebbe possibile scoprire, in assenza di un sistema sicuro per il controllo dell'integrità, se il "tal" programma è realmente quello che l'utente ha scelto di installare ed eseguire, né sarebbe possibile continuare lo sviluppo di quei programmi (o l'utilizzo di quelle periferiche) ritenuti obsoleti e non più supportati dalle fonti ufficiali.