Hook print spooler on server

c++ / delphi package - dll injection and api hooking
marmotta
Posts: 20
Joined: Wed Jun 25, 2008 4:19 pm

Post by marmotta »

Yes, you're right, I must be very careful... and this is the reason for which when I tried to include Printers or Classes in my uses clause the app run with a blue screen of death :sorry:.
Anyway, I tried another way... oh, to tell you the truth, many many different ways. At the end this is what I was able to realize.
common.pas

Code: Select all

unit common;

interface
uses Windows;

type
  TJobInfoMG = record
    IsValid:  boolean;
    JobId:    cardinal;

    PrinterName:    array[0..MAX_PATH] of Char;
    MachineName:    array[0..MAX_PATH] of Char;
    UserName:       array[0..MAX_PATH] of Char;
    Document:       array[0..MAX_PATH] of Char;
    NotifyName:     array[0..MAX_PATH] of Char;
    Datatype:       array[0..MAX_PATH] of Char;
    PrintProcessor: array[0..MAX_PATH] of Char;
    Parameters:     array[0..MAX_PATH] of Char;
    DriverName:     array[0..MAX_PATH] of Char;
    StatusDescr:    array[0..MAX_PATH] of Char;

    Status:       DWORD;
    Priority:     DWORD;
    Position:     DWORD;
    StartTime:    DWORD;
    UntilTime:    DWORD;
    TotalPages:   DWORD;
    Size:         DWORD;
    Submitted:    TSystemTime;  { Time the job was spooled }
    Time:         DWORD;        { How many seconds the job has been printing }
    PagesPrinted: DWORD;

    devFields:  DWORD;
    devCopies:  SHORT;
    devColor:   SHORT;
    devDuplex:  SHORT;
    devCollate: SHORT;

    Collate:  boolean;
    Color:    boolean;
    Duplex:   boolean;
    Copies:   integer;
  end;

  TPrintNotification = record
    Process:  array[0..MAX_PATH] of Char;
    Api:      array[0..MAX_PATH] of Char;
    Params:   array[0..MAX_PATH] of Char;

    jobinfo:  TJobInfoMG;
  end;

implementation

end.
hookPrint.dll

Code: Select all

library hookPrint;
//{$DEFINE EXTLOG}

uses
  Windows,
  SysUtils,
  WinSpool,
  madCodeHook,
  madStrings,
  madRemote,
  madTools,
  common in '..\common.pas';

const
  DLL_SpoolSs =   'spoolss.dll';

type
  TPrinters = array[0..1023] of PRINTER_INFO_1;
  PPrinters = ^TPrinters;
  TJobs = array[0..1023] of JOB_INFO_2;
  PJobs = ^TJobs;

var
  prnt: TPrintNotification;

  nextSetJob: function(hPrinter: THandle; JobId,Level: DWORD; pJob: Pointer; Command: DWORD): boolean; stdcall;

// ***************************************************************

procedure InitVars;
begin
  prnt.jobinfo.IsValid := false;
end;

procedure NotifyApplication(api: string; params: string);
var
  arrChW : array [0..MAX_PATH] of WideChar;
  session: dword;
begin
  GetModuleFileNameW(0, arrChW, MAX_PATH);
  {$IFDEF UNICODE}
  lstrcpy(prnt.Process, arrChW);
  {$ELSE}
  WideToAnsi(arrChW, prnt.Process);
  {$ENDIF}
  lstrcpy(prnt.Api, PChar(api));
  lstrcpy(prnt.Params, PChar(params));

  if AmSystemProcess and (GetCurrentSessionId = 0) then session := GetInputSessionId
  else session := GetCurrentSessionId;
  SendIpcMessage(PAnsiChar('PrintMonitor' + IntToStrEx(session)), @prnt, sizeOf(prnt));
end;

procedure Error(title, msg: string; const icon: cardinal = 0);
begin
  {$IFDEF EXTLOG}
  MessageBox(
    GetDesktopWindow,
    PChar(msg),
    PChar(title),
    MB_OK+icon);
  {$ELSE}
    NotifyApplication('ERROR '+title,msg);
  {$ENDIF}
end;

// ***************************************************************

function JobToJobInfoMG(job: JOB_INFO_2): TJobInfoMG;
begin
  try
    with Result do begin
      JobId := job.JobId;

      lstrcpy(PrinterName,job.pPrinterName);
      lstrcpy(MachineName,job.pMachineName);
      lstrcpy(UserName,job.pUserName);
      lstrcpy(Document,job.pDocument);
      lstrcpy(NotifyName,job.pNotifyName);
      lstrcpy(Datatype,job.pDatatype);
      lstrcpy(PrintProcessor,job.pPrintProcessor);
      lstrcpy(Parameters,job.pParameters);
      lstrcpy(DriverName,job.pDriverName);
      lstrcpy(StatusDescr,job.pStatus);

      devFields := job.pDevMode^.dmFields;
      devCopies := job.pDevMode^.dmCopies;
      devColor := job.pDevMode^.dmColor;
      devDuplex := job.pDevMode^.dmDuplex;
      devCollate := job.pDevMode^.dmCollate;

      Status := job.Status;
      Priority := job.Priority;
      Position := job.Position;
      StartTime := job.StartTime;
      UntilTime := job.UntilTime;
      TotalPages := job.TotalPages;
      Size := job.Size;
      Submitted := job.Submitted;
      Time := job.Time;
      PagesPrinted := job.PagesPrinted;

      Copies := devCopies;
      if ((devFields and DM_COLOR) = DM_COLOR) then
        Color := (devColor = DMCOLOR_COLOR);
      if ((devFields and DM_DUPLEX) = DM_DUPLEX) then
        Duplex := (devDuplex <> DMDUP_HORIZONTAL );
      if ((devFields and DM_COLLATE) = DM_COLLATE) then
        Collate := (devCollate = DMCOLLATE_TRUE);

      IsValid := true;
    end;
  except
    on E: Exception do Error('JobToJobInfoMG',E.Message,MB_ICONERROR);
  end;
end;

procedure ShowJob(sender: string; job: TJobInfoMG); overload;
{$IFDEF EXTLOG}var s: string;{$ENDIF}
begin
  {$IFDEF EXTLOG}
  with job do begin
    if (IsValid = false) then exit;
    try
      s := s+format('Printer name: %s'#13#10,[PrinterName]);
      s := s+format('Machine name: %s'#13#10,[MachineName]);
      s := s+format('User name: %s'#13#10,[UserName]);
      s := s+format('Document: %s'#13#10,[Document]);
      s := s+format('NotifyName: %s'#13#10,[NotifyName]);
      s := s+format('DataType: %s'#13#10,[DataType]);
      s := s+format('PrintProcessor: %s'#13#10,[PrintProcessor]);
      s := s+format('Parameters: %s'#13#10,[Parameters]);
      s := s+format('DriverName: %s'#13#10,[DriverName]);
      s := s+format('StatusDescr: %s'#13#10,[StatusDescr]);

      s := s+format('Status: $%8.8x'#13#10,[Status]);
      s := s+format('Priority: %d'#13#10,[Priority]);
      s := s+format('Position: %d'#13#10,[Position]);
      s := s+format('StartTime: %d'#13#10,[StartTime]);
      s := s+format('UntilTime: %d'#13#10,[UntilTime]);
      s := s+format('TotalPages: %d'#13#10,[TotalPages]);
      s := s+format('Size: %d'#13#10,[Size]);
      s := s+format('Submitted: %d:%d:%d.%d'#13#10,
        [Submitted.wHour,Submitted.wMinute,Submitted.wSecond,Submitted.wMilliseconds]);
      s := s+format('Time: %d'#13#10,[Time]);
      s := s+format('PagesPrinted: %d'#13#10,[PagesPrinted]);
      Error('ShowJob 02 from '+sender,s,MB_ICONINFORMATION);
    except
      on E: Exception do begin
        Error('ShowJob 02 from '+sender,E.Message,MB_ICONERROR);
        exit;
      end;
    end;
  end;
  {$ENDIF}
end;

procedure ShowJob(sender: string; job: JOB_INFO_2); overload;
begin
  ShowJob(sender,JobToJobInfoMG(job));
end;

function GetJob(id: cardinal): TJobInfoMG;
const TITLE = 'GetJob';
var
  a,pcount: integer;
  dwPrinters,dwNeed,dwJobs: cardinal;
  printers: PPrinters;
  jobs: PJobs;
  job: JOB_INFO_2;
  pd: TPrinterDefaults;
  hPrinter: THandle;
begin
  Result.IsValid := false;
  with pd do begin
    DesiredAccess := PRINTER_ACCESS_USE;
    pDatatype := nil;
    pDevMode := nil;
  end;

  EnumPrinters(PRINTER_ENUM_LOCAL,nil,1,nil,0,dwNeed,dwPrinters);
  printers := AllocMem(dwNeed);
  EnumPrinters(PRINTER_ENUM_LOCAL,nil,1,printers,dwNeed,dwNeed,dwPrinters);

  for pcount := 0 to dwPrinters - 1 do begin
    if OpenPrinter(printers[pcount].pName,hPrinter,@pd) then begin
      dwNeed := 0; dwJobs := 0;
      EnumJobs(hPrinter,0,sizeof(TJobs),2,nil,0,dwNeed,dwJobs);
      jobs := AllocMem(dwNeed);
      EnumJobs(hPrinter,0,sizeof(TJobs),2,jobs,dwNeed,dwNeed,dwJobs);

      for a := 0 to dwJobs - 1 do begin
        job := jobs[a];
        if job.JobId = id then begin
          Result := JobToJobInfoMG(job);
          prnt.jobinfo := Result;
        end;
      end;
      ClosePrinter(hPrinter);
    end else begin
      {$IFDEF EXTLOG}
      Error(TITLE,format('Can''t EnumJobs for printer %s',[printers[pcount].pName]));
      {$ENDIF}
    end;
  end;
end;

// ***************************************************************

function hookSetJob(hPrinter: THandle; JobId,Level: DWORD; pJob: Pointer; Command: DWORD): boolean; stdcall;
var job: TJobInfoMG;
begin
  if Command = JOB_CONTROL_CANCEL then begin
    job := GetJob(JobId);
    {$IFNDEF EXTLOG}
    ShowJob('SetJob',job);
    {$ENDIF}
    NotifyApplication('SetJob', 'JOB_CONTROL_CANCEL');
  end else
    NotifyApplication('SetJob', format('JobId: $%2.2x - Cmd: %x',[JobId,Command]));
  Result := nextSetJob(hPrinter,JobId,Level,pJob,Command);
end;

begin
  InitVars;

  CollectHooks;
  HookAPI(DLL_SpoolSs, 'SetJobW', @hookSetJob, @nextSetJob);
  FlushHooks;
end.
I thought every job must be deleteed after it has finished its work, so I searched for DeleteJob unsuccesfully; so I hooked SetJob and I saw that every job, after completing print, is recalled in SetJob with JOB_CONTROL_CANCEL command. Hooking that call I go through all printers installed and search for that JobId, getting the JOB_INFO_2 structure.
Well, that's all...

I repeat that this is only a test, not final release.
So it MUST be tested on servers and, finally, put in a service.

Can someone help me to complete these tasks?
Thanks a lot.
madshi
Site Admin
Posts: 10857
Joined: Sun Mar 21, 2004 5:25 pm

Post by madshi »

Yeah, avoid "using" any such fancy VCL units. Instead copy that part of the code which you need into your own hook dll source code. That's much safer.
marmotta
Posts: 20
Joined: Wed Jun 25, 2008 4:19 pm

Post by marmotta »

I know, probably it's my fault but... help me !!!
I copied my EXE and DLL to the server and to another pc too to try that my software is really working... but nothing happens :(.
Is this behaviour related to driver mchinjdrv.sys? Or what else?
How can I deploy my app to another pc and try it?
Thanks
madshi
Site Admin
Posts: 10857
Joined: Sun Mar 21, 2004 5:25 pm

Post by madshi »

Well, first step is to check whether your dll got injected successfully. You can check that by using the ProcessExplorer tool.
marmotta
Posts: 20
Joined: Wed Jun 25, 2008 4:19 pm

Post by marmotta »

madshi wrote:Well, first step is to check whether your dll got injected successfully. You can check that by using the ProcessExplorer tool.
I usually use Process Explorer... but how can I see if my DLL got injected? :oops:
OK, found. Yes, DLL is correctly injected system wide: every process uses it when my EXE is running...
marmotta
Posts: 20
Joined: Wed Jun 25, 2008 4:19 pm

Post by marmotta »

To have a try: I'm hooking SetJobW and when I print even on a locally installed PDFCreator printer my EXE shows the hook, good.
But when I try the same EXE+DLL on other PCs (Windows Server 2003 or Windows Vista) where madCodeHook is not installed... well, even printing locally doesn't let my EXE shows anything... and I'm quite sure that function is called... you know, PDFCreator is the same on every pc. I'm confused...
madshi
Site Admin
Posts: 10857
Joined: Sun Mar 21, 2004 5:25 pm

Post by madshi »

Try creating a dummy file somewhere instead of calling SendIpcMessage. Maybe IPC doesn't work or something?
marmotta
Posts: 20
Joined: Wed Jun 25, 2008 4:19 pm

Post by marmotta »

madshi wrote:Try creating a dummy file somewhere instead of calling SendIpcMessage. Maybe IPC doesn't work or something?
IPC doesn't work: writing events on a file it seems really good!!!
So, what could cause this problem? Some port has to be opened in firewall?
madshi
Site Admin
Posts: 10857
Joined: Sun Mar 21, 2004 5:25 pm

Post by madshi »

Make sure that the IPC queue names match. Maybe they don't match because of different session numbers or something like that. Also try the latest beta:

http://madshi.net/madCollectionBeta.exe
marmotta
Posts: 20
Joined: Wed Jun 25, 2008 4:19 pm

Post by marmotta »

madshi wrote:Make sure that the IPC queue names match. Maybe they don't match because of different session numbers or something like that.
Session number, that was the point. You're always right, thanks Mathias!!
madshi wrote: Also try the latest beta:
http://madshi.net/madCollectionBeta.exe
I'll do that, thanks again
marmotta
Posts: 20
Joined: Wed Jun 25, 2008 4:19 pm

Post by marmotta »

It's working !!! :D
I'm trying my code on a Windows 2003 Server and it's getting all print jobs sent to shared printers. I hope it can be useful to others.

It's necessary to change the hooking function a little bit.

Code: Select all

function hookSetJob(hPrinter: THandle; JobId,Level: DWORD; pJob: Pointer; Command: DWORD): boolean; stdcall;
var 
  job: TJobInfoMG;
  {$IFDEF EXTLOG}
  cmd: string;
  {$ENDIF}
begin
  if ((Command = JOB_CONTROL_CANCEL) or
    (Command = JOB_CONTROL_SENT_TO_PRINTER)) then begin
    job := GetJob(JobId);
    {$IFDEF EXTLOG}
    ShowJob('SetJob',job);
    {$ENDIF}
    NotifyApplication('SetJob', '');
  end else begin
    {$IFDEF EXTLOG}
    case Command of
      JOB_CONTROL_PAUSE: cmd := 'PAUSE';
      JOB_CONTROL_RESUME: cmd := 'RESUME';
      JOB_CONTROL_RESTART: cmd := 'RESTART';
      JOB_CONTROL_DELETE: cmd := 'DELETE';
      JOB_CONTROL_LAST_PAGE_EJECTED: cmd := 'LAST PAGE EJECTED';
    end;
    NotifyApplication('SetJob', format('JobId: $%2.2x - Cmd: %s',[JobId,cmd]));
    {$ENDIF}
  end;
  Result := nextSetJob(hPrinter,JobId,Level,pJob,Command);
end;
Today I'll test it on a Windows Vista machine and, if possible, on a Windows 7 one too.
cyberproject, can you help me? Thanks.

Now we have to:
  • 1. Test it on different Windows versions
    2. Test if returned infos are OK with different programs (Word, Excel, Adobe Reader and many others) using collation, duplex, color/bw printing and multiple copies
    3. Let DLL read settings from registry (for example to know whics log file use, if some printer should be discarded and whatever we want)
    4. Let main EXE use external saved driver, so antivirus software doesn't complain anymore (Sophos stops my EXE ecerytime I compile it)
    5. Let main EXE become a Windows service
    6. Last, and this is to create a really useful application, create a console to set app params and watch printed jobs, having some report too
Finally: when I print, usually MachineName is reported as IP... it could be useful convert it in a readable netbios name.[/list]
marmotta
Posts: 20
Joined: Wed Jun 25, 2008 4:19 pm

Post by marmotta »

madshi, a question: I extracted injection driver and, in my EXE, use SetMadCHookOption(USE_EXTERNAL_DRIVER_FILE,'mchInjDrv.sys'); but when I first run my recompiled EXE, Sophos tells me that a virus in inside blocking it; so I must authorize it and everything is fine.
I undesrtood that this happens when EXE starts and tries to install (automatically, right?) injection driver. But before calling InjectLibrary Sophos block my app... why? Using external driver doesn't fix situation, because app is blocked before driver is installed !!! So, is there a solution?

And more, my app doesn't work on Vista 64: I have to wait new release, right?

Thanks
madshi
Site Admin
Posts: 10857
Joined: Sun Mar 21, 2004 5:25 pm

Post by madshi »

Please complain to Sophos, this seems like a typical false alarm. You can also try signing your exe with a code signing certificate, if you have one. That often helps avoiding false alarms...

Yes, for x64 you'll have to wait for madCodeHook 3.0.
Post Reply