DiskInf 1.0
Leer el número serial de Fábrica y modelo del HDD
Fecha: 08/Oct/2001 (07/Oct/2001)
Autor: Miguelacho (
[email protected] )
Hola Guille, aquí te mando un ejemplo sencillo pero muy interesante, en especial para quienes quieran crear un sistema de seguridad para sus proyectos, se trata de una aplicación que permite leer el numero serial del disco duro, pero el numero REAL, el de fábrica. No el del Volumen, además de esto, lee el modelo y marca del disco, y el número de revisión, la idea de este código es brindar a los programadores la posibilidad de capturar estas informaciones, para crear un sistema de procesamiento bien sea de una licencia única, o la protección de un sistema para que solo pueda ser ejecutado en una máquina específica. Mucho te agradecería a ti y a los colegas que usen este código, me informen de las posibilidades que se puedan introducir, en especial porque lamentablemente, solo corre bajo Windows 95, 98, 98 SE, y Me, no he logrado que corra en NT, ni en 2000, y no lo he probado en XP, aunque estoy trabajando con un colega quien está haciendo modificaciones, así como introduciendo nuevas funciones de seguridad, posiblemente él o ambos, complementemos esta información.
Este es el código fuente de la DLL:
(La misma está escrita en Delphi, y es propiedad de Lucy o MAC ([email protected]), la verdad nunca pude contactarme con esta persona, los códigos los saqué de aquí: http://skyscraper.fortunecity.com/virtuosity/452/)
DSIdeInf.DPR: {$A-} (* ¡¡¡ IMPORTANT: DON'T ALIGN REGISTER FIELDS !!! *) // -------------------------------------------------------- LIBRARY DSIdeInf; USES Windows, SysUtils; // ------------- DATA - handling of the VxD ---------------- (* VxD Functions *) CONST cVxDfunction_IdesDinfo = 1; (* Output Bbuffer for the VxD (rt_IdeDinfo record) *) TYPE at_DRawInfo = array [0..255] of word; rt_IdeDInfo = record IdeExists: array [1..4] of byte; DiskExists: array [1..8] of byte; DisksRawInfo: array [1..8] of at_DRawInfo; end; {rIdeDInfo} pt_IdeDInfo = ^rt_IdeDInfo; // pointer for dynamic allocation VAR VxDHandle: THandle; POutBufVxD: pt_IdeDInfo; lpBytesReturned: DWord; // ---------- DATA - Handling of IdeDinfo (DLL) --------- CONST bit00 = $00000001; bit01 = $00000002; bit02 = $00000004; bit03 = $00000008; bit04 = $00000010; bit05 = $00000020; bit06 = $00000040; bit07 = $00000080; bit08 = $00000100; bit09 = $00000200; bit10 = $00000400; bit11 = $00000800; bit12 = $00001000; bit13 = $00002000; bit14 = $00004000; bit15 = $00008000; (* IdeDinfo "data fields" *) TYPE rt_DiskInfo = record DiskExists: boolean; ATAdevice: boolean; RemovableDevice: boolean; TotLogCyl: word; TotLogHeads: word; TotLogSPT: word; SerialNumber: string[20]; FirmwareRevision: string[8]; ModelNumber: string[40]; CurLogCyl: word; CurLogHeads: word; CurLogSPT: word; end; VAR vp_Valid: boolean; vp_IdeExists: array [1..4] of boolean; vp_DisksInfo: array [1..8] of rt_DiskInfo; // ----------- CODE - VxD function handling -------------- PROCEDURE IdeDInfo; Var Ide, MoS, Disk: byte; fi, fj: byte; {FOR counters} Begin (* 1. Make an output buffer for the VxD *) POutBufVxD:= AllocMem(SizeOf(rt_IdeDInfo)); (* 2. Run VxD function *) DeviceIoControl (VxDHandle, cVxDfunction_IdesDInfo, NIL, 0, {In} POutBufVxD, SizeOf(POutBufVxD^), lpBytesReturned, {Out} NIL); (* 3. Translate and store data *) // Ide - Disk exists for Ide:= 1 to 4 do begin vp_IdeExists[Ide]:= Odd(POutBufVxD^.IdeExists[Ide]); for MoS:= 1 to 2 do begin Disk:= (Ide-1)*2 + MoS; vp_DisksInfo[Disk].DiskExists:= Odd(POutBufVxD^.DiskExists[Disk]) end; // for MoS end; // for Ide for Ide:= 1 to 4 do begin if Not vp_IdeExists[Ide] then continue; (* Ide Exists - Go on *) for MoS:= 1 to 2 do begin Disk:= (Ide-1)*2 + MoS; if Not vp_DisksInfo[Disk].DiskExists then continue; (* Disk Exists - Go on *) { --- ATAdevice --- } if (POutBufVxD^.DisksRawInfo[Disk][0] And bit15) = 0 then vp_DisksInfo[Disk].ATAdevice:= True; { --- Removable Device --- } if LongBool(POutBufVxD^.DisksRawInfo[Disk][0] And bit07) then vp_DisksInfo[Disk].RemovableDevice:= True; { --- Total number of Logical Cylinders --- } vp_DisksInfo[Disk].TotLogCyl:= POutBufVxD^.DisksRawInfo[Disk][1]; { --- Total number of Logical Heads --- } vp_DisksInfo[Disk].TotLogHeads:= POutBufVxD^.DisksRawInfo[Disk][3]; { --- Total Logical Sectors per Logical Track --- } vp_DisksInfo[Disk].TotLogSPT:= POutBufVxD^.DisksRawInfo[Disk][6]; { -- Serial Number --- } for fi:=10 to 19 do begin fj:= 1+ (fi-10)*2; vp_DisksInfo[Disk].SerialNumber[fj]:= Chr( Hi(POutBufVxD^.DisksRawInfo[Disk][fi]) ); vp_DisksInfo[Disk].SerialNumber[fj+1]:= Chr( Lo(POutBufVxD^.DisksRawInfo[Disk][fi]) ); end; // for fi // Let's adjust its length for fi:=1 to 20 do if vp_DisksInfo[Disk].SerialNumber[fi] = Chr(0) then break else vp_DisksInfo[Disk].SerialNumber[0]:= Chr(fi); { -- Firmware Revision --- } for fi:=23 to 26 do begin fj:= 1+ (fi-23)*2; vp_DisksInfo[Disk].FirmwareRevision[fj]:= Chr( Hi(POutBufVxD^.DisksRawInfo[Disk][fi]) ); vp_DisksInfo[Disk].FirmwareRevision[fj+1]:= Chr( Lo(POutBufVxD^.DisksRawInfo[Disk][fi]) ); end; // for fi // Let's adjust its length for fi:=1 to 8 do if vp_DisksInfo[Disk].FirmwareRevision[fi] = Chr(0) then break else vp_DisksInfo[Disk].FirmwareRevision[0]:= Chr(fi); { -- Model Number --- } for fi:=27 to 46 do begin fj:= 1+ (fi-27)*2; vp_DisksInfo[Disk].ModelNumber[fj]:= Chr( Hi(POutBufVxD^.DisksRawInfo[Disk][fi]) ); vp_DisksInfo[Disk].ModelNumber[fj+1]:= Chr( Lo(POutBufVxD^.DisksRawInfo[Disk][fi]) ); end; // for fi // Let's adjust its length for fi:=1 to 40 do if vp_DisksInfo[Disk].ModelNumber[fi] = Chr(0) then break else vp_DisksInfo[Disk].ModelNumber[0]:= Chr(fi); { --- Current Logical Cylinders --- } vp_DisksInfo[Disk].CurLogCyl:= POutBufVxD^.DisksRawInfo[Disk][54]; { --- Current Logical Heads --- } vp_DisksInfo[Disk].CurLogHeads:= POutBufVxD^.DisksRawInfo[Disk][55]; { --- Current Logical Sectors per Logical Track --- } vp_DisksInfo[Disk].CurLogSPT:= POutBufVxD^.DisksRawInfo[Disk][56]; end; // NEXT MoS end; // NEXT Ide (* 4. Free the VxD output buffer memory *) ReallocMem (POutBufVxD, 0); End; {IdeDInfo} // --------- CODE - DSIdeInf exported functions/procedures ----- FUNCTION Valid : byte; Stdcall; Begin If vp_Valid then Valid:= 1 else Valid:= 0; End; {Valid} FUNCTION IdeExists (a_Ide: byte) : byte; Stdcall; Begin if (a_Ide < 1) or (a_Ide > 4) then // If Invalid Ide Argument begin IdeExists:= 0; Exit; end; If vp_IdeExists[a_Ide] then IdeExists:= 1 else IdeExists:= 0; End; {IdeExists} { Not exported Function. Handles data as boolean type. Exported value is byte type} FUNCTION bool_DiskExists (a_Disk: byte) : boolean; Begin if (a_Disk < 1) or (a_Disk > 8) then bool_DiskExists:= False // If Invalid Disk Argument else bool_DiskExists:= vp_DisksInfo[a_Disk].DiskExists; End; {bool_DiskExists} FUNCTION DiskExists(a_Disk: byte) : byte; Stdcall; Begin if bool_DiskExists (a_Disk) then DiskExists:= 1 else DiskExists:= 0; End; {DiskExists} FUNCTION ATAdevice(a_Disk: byte) : byte; Stdcall; Begin if NOT bool_DiskExists(a_Disk) then begin ATAdevice:= 0; Exit; end; if vp_DisksInfo[a_Disk].ATAdevice then ATAdevice:= 1 else ATAdevice:= 0; End; {ATAdevice} FUNCTION RemovableDevice (a_Disk: byte): byte; Stdcall; Begin if NOT bool_DiskExists (a_Disk) then begin RemovableDevice:= 0; Exit; end; if vp_DisksInfo[a_Disk].RemovableDevice then RemovableDevice:= 1 else RemovableDevice:= 0; End; {RemovableDevice} FUNCTION TotLogCyl (a_Disk: byte): word; Stdcall; Begin if bool_DiskExists(a_Disk) then TotLogCyl:= vp_DisksInfo[a_Disk].TotLogCyl else TotLogCyl:= 0; End; {TotLogCyl} FUNCTION TotLogHeads (a_Disk: byte): word; Stdcall; Begin if bool_DiskExists(a_Disk) then TotLogHeads:= vp_DisksInfo[a_Disk].TotLogHeads else TotLogHeads:= 0; End; {TotLogHeads} FUNCTION TotLogSPT (a_Disk: byte): word; Stdcall; Begin if bool_DiskExists(a_Disk) then TotLogSPT:= vp_DisksInfo[a_Disk].TotLogSPT else TotLogSPT:= 0; End; {TotLogSPT} PROCEDURE SerialNumber(a_Disk: byte; aP_SerialNumber: PChar); Stdcall; Begin if bool_DiskExists(a_Disk) then StrPCopy (aP_SerialNumber, vp_DisksInfo[a_Disk].SerialNumber) else aP_SerialNumber:= ''; End; {SerialNumber} PROCEDURE FirmwareRevision(a_Disk: byte; aP_FirmwareRevision: PChar); Stdcall; Begin if bool_DiskExists(a_Disk) then StrPCopy (aP_FirmwareRevision, vp_DisksInfo[a_Disk].FirmwareRevision) else aP_FirmwareRevision:= ''; End; {FirmwareRevision} PROCEDURE ModelNumber(a_Disk: byte; aP_ModelNumber: Pchar); Stdcall; Begin if bool_DiskExists(a_Disk) then StrPCopy (aP_ModelNumber, vp_DisksInfo[a_Disk].ModelNumber) else aP_ModelNumber:= ''; End; {ModelNumber} FUNCTION CurLogCyl (a_Disk: byte): word; Stdcall; Begin if bool_DiskExists(a_Disk) then CurLogCyl:= vp_DisksInfo[a_Disk].CurLogCyl else CurLogCyl:= 0; End; {CurLogCyl} FUNCTION CurLogHeads (a_Disk: byte): word; Stdcall; Begin if bool_DiskExists(a_Disk) then CurLogHeads:= vp_DisksInfo[a_Disk].CurLogHeads else CurLogHeads:= 0; End; {CurLogHeads} FUNCTION CurLogSPT (a_Disk: byte): word; Stdcall; Begin if bool_DiskExists(a_Disk) then CurLogSPT:= vp_DisksInfo[a_Disk].CurLogSPT else CurLogSPT:= 0; End; {CurLogSPT} // ----------------------------------------------------------- EXPORTS Valid, IdeExists, DiskExists, ATAdevice, RemovableDevice, TotLogCyl, TotLogHeads, TotLogSPT, SerialNumber, FirmwareRevision, ModelNumber, CurLogCyl, CurLogHeads, CurLogSPT; // --------------- CODE - DLL Initialization ---------------- BEGIN (* 1. Try to load the VxD *) VxDHandle:= CreateFile (PChar('\\.\DSIdeInf.VXD'), 0, 0, NIL, 0, FILE_FLAG_DELETE_ON_CLOSE, 0); (* 2. If loading was succesful, run its function, interpret and store the info, and then unload the VxD *) If VxDHandle <> INVALID_HANDLE_VALUE then begin vp_Valid:= True; IdeDInfo; // Call VxD function, interpret and store info (* Unload VxD *) CloseHandle (VxDHandle); end; // If valid END. DSIdeinf Import Unit: UNIT DSIdeInf_DLL_Import_Unit; INTERFACE FUNCTION Valid : byte; Stdcall; external 'DSIdeInf.DLL'; FUNCTION IdeExists (a_Ide: byte) : byte; Stdcall; external 'DSIdeInf.DLL'; FUNCTION DiskExists(a_Disk: byte) : byte; Stdcall; external 'DSIdeInf.DLL'; FUNCTION ATAdevice(a_Disk: byte) : byte; Stdcall; external 'DSIdeInf.DLL'; FUNCTION RemovableDevice (a_Disk: byte): byte; Stdcall; external 'DSIdeInf.DLL'; FUNCTION TotLogCyl (a_Disk: byte): word; Stdcall; external 'DSIdeInf.DLL'; FUNCTION TotLogHeads (a_Disk: byte): word; Stdcall; external 'DSIdeInf.DLL'; FUNCTION TotLogSPT (a_Disk: byte): word; Stdcall; external 'DSIdeInf.DLL'; PROCEDURE SerialNumber(a_Disk: byte; aP_SerialNumber: PChar); Stdcall; external 'DSIdeInf.DLL'; PROCEDURE FirmwareRevision(a_Disk: byte; aP_FirmwareRevision: PChar); Stdcall; external 'DSIdeInf.DLL'; PROCEDURE ModelNumber(a_Disk: byte; aP_ModelNumber: PChar); Stdcall; external 'DSIdeInf.DLL'; FUNCTION CurLogCyl (a_Disk: byte): word; Stdcall; external 'DSIdeInf.DLL'; FUNCTION CurLogHeads (a_Disk: byte): word; Stdcall; external 'DSIdeInf.DLL'; FUNCTION CurLogSPT (a_Disk: byte): word; Stdcall; external 'DSIdeInf.DLL'; IMPLEMENTATION END.
Y este es el fuente del archivo VXD que está en ASSEMBLER: DSIdeInf.ASM:
TITLE DSIdeInf.asm - VxD retrieves information from IDEs .386P .NOLIST Include Vmm.inc Include VWin32.inc Include Shell.inc .LIST ; ***** EQUATES ***** ; --- Bits --- cBit00 EQU 0000000000000001b cBit02 EQU 0000000000000100b cBit07 EQU 0000000010000000b ; --- Tests --- cERR EQU cBit00 cBusy EQU cBit07 ; --- Area offsets in buffer --- cDisk0_Exists = 4 cDisk0_RawInfo =12 ; --- GDI commands --- cATA_GDIcmd EQU 0ECh ; GDI command for ATA cATAPI_GDIcmd EQU 0A1h ; GDI command for ATAPI ; **** MACROS ***** ; wPort0 has to be initialized according to IDE WaitWhileBusy MACRO LOCAL LoopWhileBusy MOV DX, [wPort0] ADD DX, 7 ; DX = Port0 +7 LoopWhileBusy: IN AL, DX TEST AX, cBusy JNZ LoopWhileBusy ENDM ; (WaitWhileBusy) ; wPort0 has to be initialized according to IDE ; bDevSelCmd has to be initialized according to MoS SelectDevice MACRO MOV DX, [wPort0] ADD DX, 6 ; DX = Port0 +6 MOV AL, [bDevSelCmd] ; AL = DevSelCmd OUT DX, AL ENDM ; (SelectDevice) ; wPort0 has to be initialized according to IDE ; Parameter 'GDIcmd' is GDI command (ATA or ATAPI) SendGDIcmd MACRO GDIcmd MOV DX, [wPort0] ADD DX, 7 ; DX = Port0 +7 MOV AL, GDIcmd OUT DX, AL ENDM ; (SendGDIcmd) ; ------------------------------- ; --- Device Descriptor Block --- ; ------------------------------- DECLARE_VIRTUAL_DEVICE \ DSIDEINF, 1, 0, DSIDEINF_Control,\ UNDEFINED_DEVICE_ID, UNDEFINED_INIT_ORDER ; ---------------------------------------- ; --- Prepare Device Control Procedure --- ; ---------------------------------------- Begin_control_dispatch DSIDEINF Control_Dispatch w32_DeviceIoControl, On_DeviceIoControl End_control_dispatch DSIDEINF ; ************ ; ** CODE ** ; ************ VxD_LOCKED_CODE_SEG ; === DEVICE CONTROL PROCEDURE === BeginProc On_DeviceIoControl ASSUME ESI:PTR DIOCParams .if [esi].dwIoControlCode==DIOC_Open xor eax,eax .elseif [esi].dwIoControlCode==1 CALL IdesInfo xor eax, eax .endif RET EndProc On_DeviceIoControl ; Meaning of registers: ; ESI: pointer to output buffer ; EDI: pointer to section of 1 disk info, in output buffer ; EBX: IDE (0..3) ; ECX: MasterOrSlave (0..1) ; IDE controllers: ; Range: 0..3 ; Address of IdeExists(n) byte in output buffer ; = ESI + Ide = ESI + EBX ; DISCs: ; Range: 0..7 ; Disk = Ide*2 + MoS = EBX*2 + ECX ; Address of DiskExists(n) byte in output buffer ; = ESI + cDisk0_Exists + Disk ; Address of Disc raw info first byte in output buffer ; = ESI + cDisk0_RawInfo + Disk*512 BeginProc IdesInfo ASSUME ESI:PTR DIOCParams ; --- Preserve registers and flags --- PUSHAD PUSHFD ; --- Initialize ESI as pointer to output buffer --- MOV ESI, [ESI.lpvOutBuffer] ; -------------------------------------- ; --- Process to get the information --- ; -------------------------------------- ; Initialize FOR_Ide counter MOV EBX, 3 ; EBX = Ide no 3 FOR_Ide: ; FOR EBX=3 DOWNTO 0 ; --- Initialize wPort0 for this IDE --- MOV DX, [wPorts_table + EBX*2] MOV wPort0, DX ; --- Check if IDE exists --- ;MOV DX, [wPort0] ADD DX, 7 ; DX = Port0 +7 IN AL, DX ;Test0FFh CMP AL, 0FFh JNE Test07Fh JMP NEXT_Ide ; If AL = 0FFh, IDE not present ; -> NEXT_Ide Test07Fh: CMP AL, 07Fh JNE IdeExists JMP NEXT_Ide ; If AL = 07Fh, IDE not present ; -> NEXT_Ide IdeExists: ; --- Set value at the IdeExists area in output buffer --- MOV BYTE PTR [ESI + EBX], 1 ; Ide_Exists = True ; --- Prepare MoS (Master or Slave) loop --- ; Initialize FOR_MoS counter MOV ECX, 1 ; ECX = Slave FOR_MoS: ; FOR MoS=1 DOWNTO 0 ; --- Get bDevSelCmd for this disc --- MOV DL, [bDevSelCmd_table + ECX] ; DL = DevSelCmd MOV bDevSelCmd, DL ; --- Check if Disc exists --- WaitWhileBusy SelectDevice WaitWhileBusy CMP AL, 0 JNZ DiskExists JMP NEXT_MoS ; If AL = 0, Disc not present ; -> NEXT_MoS DiskExists: ; Compute Disk (= Ide*2 + MoS = EBX*2 + ECX) MOV EAX, EBX ; EAX = Ide SHL EAX, 1 ; EAX = Ide*2 ADD EAX, ECX ; EAX = Disk ; --- Set value at the DiscExists area in output buffer --- MOV BYTE PTR [ESI + cDisk0_Exists + EAX], 1 ; Disk_Exists = True ; --- Prepare EDI as pointer ; to the area where info is to be stored -- ; Compute Disk*512 (knowing that 512 = 2 ^9) XCHG AH, AL ; Was: AL = Disk , Now: AH = Disk ; -> EAX = Disk* 2^8 SHL EAX, 1 ; EAX = Disk* 2^9 MOV EDI, ESI ; EDI = @Output buffer ADD EDI, cDisk0_RawInfo ; EDI = @beginning of raw info area in output buffer ADD EDI, EAX ; EDI = @raw info section for this Disc ; ----------------- Retrieve --------- WaitWhileBusy SelectDevice SendGDIcmd cATA_GDIcmd WaitWhileBusy ; check error status MOV DX, [wPort0] ADD DX, 7 ; DX = Port0 +7 IN AL, DX TEST AL, cBit00 JZ RetrieveInfo ; ERR=1 -> try ATAPI GDI command WaitWhileBusy SelectDevice SendGDIcmd cATAPI_GDIcmd WaitWhileBusy RetrieveInfo: PUSH ECX ; keep MoS value MOV ECX, 256 MOV DX, [wPort0] CLD REP INSW ; Retrieve (finally!) POP ECX ; restore MoS value NEXT_Mos: ;LOOP FOR_MoS CMP ECX, 0 JE NEXT_Ide DEC ECX JMP FOR_MoS NEXT_Ide: ;LOOP FOR_Ide CMP EBX, 0 JE Exit_IdeLoop DEC EBX JMP FOR_Ide Exit_IdeLoop: ; --- Restore flags and registers --- POPFD POPAD RET EndProc IdesInfo VxD_LOCKED_CODE_ENDS ; ************ ; ** DATA ** ; ************ VxD_LOCKED_DATA_SEG ; --- Tables --- wPorts_table WORD 1F0h, 170h, 1E8h, 168h ; IDE 1 - 4 bDevSelCmd_table BYTE 0A0h, 0B0h ; Master - Slave ; --- Variables --- wPort0 WORD ? ; Port 0 bDevSelCmd BYTE ? VxD_LOCKED_DATA_ENDS END
DSIdeInf.DEF:
VXD DSIDEINF DYNAMIC SEGMENTS _LTEXT CLASS 'LCODE' PRELOAD NONDISCARDABLE _LDATA CLASS 'LCODE' PRELOAD NONDISCARDABLE EXPORTS DSIDEINF_DDB @1
Y Finalmente, el Código en VB:
Private Declare Function Valid Lib "DSIdeInf.DLL" _ () As Byte Private Declare Function IdeExists Lib "DSIdeInf.DLL" _ (ByVal a_Ide As Byte) As Byte Private Declare Function DiskExists Lib "DSIdeInf.DLL" _ (ByVal a_Disk As Byte) As Byte Private Declare Function ATAdevice Lib "DSIdeInf.DLL" _ (ByVal a_Disk As Byte) As Byte Private Declare Function RemovableDevice Lib "DSIdeInf.DLL" _ (ByVal a_Disk As Byte) As Byte Private Declare Sub SerialNumber Lib "DSIdeInf.DLL" _ (ByVal a_Disk As Byte, ByVal aP_SerialNumber As String) Private Declare Sub FirmwareRevision Lib "DSIdeInf.DLL" _ (ByVal a_Disk As Byte, ByVal aP_FirmwareRevision As String) Private Declare Sub ModelNumber Lib "DSIdeInf.DLL" _ (ByVal a_Disk As Byte, ByVal aP_ModelNumber As String) Private Sub btn_End_Click() End End Sub Private Sub Form_Load() Dim Ide, Disk As Byte Dim sSerialNumber As String * 21 Dim sFirmwareRevision As String * 9 Dim sModelNumber As String * 41 Dim sCurLogCyl As String If Valid = 0 Then Mensaje = MsgBox("No se puede cargar el Archivo DSIdeInf.VXD, este archivo es necesario para la ejecución del programa, verifique que existe en la misma carpeta de el ejecutable. El programa se cerrará", vbExclamation + vbOKOnly + vbDefaultButton1 + vbApplicationModal, "No se Encuentra el archivo VXD") End End If Disk = 1 ' == Serial Number === Call SerialNumber(Disk, sSerialNumber) Text1(0).Text = sSerialNumber ' == Firmware Revision === Call FirmwareRevision(Disk, sFirmwareRevision) Text1(1).Text = sFirmwareRevision ' == Model === Call ModelNumber(Disk, sModelNumber) Text1(2).Text = sModelNumber End Sub
Fichero con el código en Delphi, Assambler y VB (DiskInf1.zip - 42.1 KB)