Archive for the '三句话不离本行' Category

批处理的“函数调用”

2010-7-18 00:39 | by 2ndboy

  今天看 Visual Studio 2008 里环境设置批处理的时候学到一招,就是在批处理里面可以模拟出函数调用的效果,可以传参数也可以从“函数”里返回值,示例如下:

  1. @echo off
  2. call :foo 2ndboy
  3. call :foo World
  4.  
  5. goto :EOF
  6.  
  7. :foo
  8. echo Hello %1 !
  9. exit /b

说白了就是 call 现在直接支持调用本批处理里面的一个标签,而不是我们以前熟知的调用另外一个批处理。但是其实内部的原理还是把批处理自身当作另外一个批处理来调用(创建新的上下文),只不过是直接调用到了标签这个级别罢了。所以在被调用标签的最后的 exit /b 就相当于是函数调用里面的 return,跟 return 一样,exit 也可以把特定的值返回给调用方,比如用 exit /b 0 返回 0。这样一来调用方还可以根据返回值做不同的处理(if %ERRORLEVEL%==0 echo ok)。

exit /b 还可以用 goto :EOF 来代替,两者都可以从被调用标签内部返回到 call 命令的下一行继续执行,区别是 goto :EOF 不带返回值。

最近在断续实战 Hudson 的时候补了不少批处理命令的课,发现 Windows 命令行已经增强了很多,远非 DOS 时代我熟悉的那些有限功能。当然,离 *nix shell 或者 PowerShell 还是有差距的,不过它对环境的依赖也最少,有时候信手拈来用用还是不错滴。

又一个鄙视 IE 的理由

2010-4-8 20:35 | by 2ndboy

  以前曾经在 IE 6 上遇到过这种问题,用 Chrome 和 FireFox 都可以正常浏览的网站,有几幅图片在 IE 里始终显示叉叉。抓包分析后看上去 web server 那边一切正常,当时感觉这肯定是 IE 的毛病,不过没有深究。今天同事在 IE 8 上遇到了类似的问题,一起分析了下,结果又发现了一个鄙视 IE 的理由。

  我们的程序里内嵌了 IE 控件,但是如果频繁多次去访问 http://www.sina.com 的话,从第二次开始铁定是访问失败的。抓包后发现,凡是出现这种情况的时候,IE 发出去的 HTTP 请求报文都是不完整的,这个不完整不是说请求报文里少了什么字段,而是说假设把整个请求包报文看作一个 200 bytes 的 buffer 的话,IE 在一个单独的 TCP 包里只发出了比方说 100 bytes。其实这在协议上是允许的,遇到这种情况时,web server 要先把这部分请求报文存起来,等将来把整个请求报文收全了再做处理。

  估计是新浪为了防止 DoS 攻击吧,如果 client 发过去一个不完整的 request message,新浪 server 那边会马上回应一个 RST(Reset)过来,从而导致 HTTP 连接被重置,IE 不能显示网页。其实这也不能怪新浪,如果有人用程序在短时内恶意的发来海量不完整 HTTP request,server 又照单全收的缓存了这些半拉子请求的话,系统肯定会很快失去响应。看了一下,新浪用的 web server 是 nginx 0.8.35,果然。

  从抓包的结果看,Chrome 遇到这种连接被 server 端 reset 的情况时会重试 3 次,3 次都失败后才会放弃,所以网页/图片不能显示的情况就极少出现。从 Google 刚发布 Chrome 的时候我就开始从 Maxthon 转向 Chrome,现在除了如网银这类非用 IE 不可的情况外,我已经很少开 IE/Maxthon 了,这次的事情再次证明转向正确:)

–== 俺就一分割线 ==–

明天凌晨,Apple 就要发布 iPhone OS 4.0 了,希望盼望已久的多任务支持变成现实!明早起来再看新闻,希望不会失望:)

聊聊 API hooking

2009-12-14 22:30 | by 2ndboy

  今天接着说说 API hook。API hook 大致上有两种手段:修改 IAT 和修改 API 指令。那种用一个自己写的同名 DLL 来替换目标 DLL 的手段就不提啦,可操作性太差。今天聊一下直接修改 API 指令这种方法。

  直接修改 API 指令,简单的说就是在目标 API 代码的开始处修改一些指令,让执行绪跳转到自己的代码里面。这里需要面对的问题有 3 个:如何找到 API 的代码地址,如何修改代码段,如何构造执行绪转移指令。

  前两个问题比较简单,无非是相关 API 的调用,用 GetProcAddress() 可以取到目标 API 的代码所在地址;用 VirtualProtect() 可以修改代码所在地址的访问权限,有了写权限之后就可以对目标 API 的代码做手脚了。

  至于如何构造执行绪转移指令,其实说白了就是在目标 API 代码的开始处放置一条无条件跳转指令 jmp。对于 32bits 系统,jmp 的机器码是 0xE9(这里不讨论短跳转 0xEB 的原因是通常我们自己代码跟目标 API 代码之间的偏移都大于 128),加上 32bits 的相对地址,一条无条件跳转指令的长度为 5 个字节。也就是说我们要至少覆盖目标 API 开始处的 5 个字节,但是直接覆盖别人的代码是肯定会出问题地,所以我们在覆盖目标 API 开始处指令之前要把这些指令搬到其它地方去,后面还有用。

  对于 jmp 指令后面那个相对地址,不是简单的拿目标地址减去当前 jmp 指令的地址就行,由于 CPU 要执行过 jmp 指令,拿到跳转地址再进行跳转,所以计算相对地址的时候要用“目的跳转地址 – ( jmp 所在指令地址 + 5 )”。目前为止,把对目标 API 的调用劫持到我们自己的函数这个目的已经达到了。但是我们还是得在需要的时候调用原始 API 来完成 caller 想要完成的功能,不过别忘了原始 API 已经被我们破坏掉了,所以调用原始 API 还要靠我们之前保存的 API 代码起始处的那些指令。

  假设我们刚好从目标 API 处复制了 5 个字节的完整指令(注意一定要是完整指令)放在内存 A 处,那么在 A + 5 处再构造一条跳转指令跳回到 API + 5 处就等于是把断成两截的 API 代码又连接了起来。这样一来,在需要调用原本的 API 功能时我们要调用的其实是 A。

以下是一些示例代码:

  1. typedef HRESULT (WINAPI *_DirectDrawCreate)( GUID FAR *lpGUID, LPDIRECTDRAW FAR *lplpDD, IUnknown FAR *pUnkOuter );
  2.  _DirectDrawCreate pOldDirectDrawCreate = NULL;
  3.  
  4.  HRESULT WINAPI
  5.  MyDirectDrawCreate( GUID FAR *lpGUID, LPDIRECTDRAW FAR *lplpDD, IUnknown FAR *pUnkOuter )
  6.  {
  7.      MessageBox( NULL, "MyDirectDrawCreate", "MyDirectDrawCreate", 0 );
  8.      return( pOldDirectDrawCreate( lpGUID, lplpDD, pUnkOuter) );
  9.  }
  10.  
  11.  int main( int argc, char *argv[] )
  12.  {
  13.      HMODULE hDll = LoadLibrary( "DDraw.dll" );
  14.      if( hDll )
  15.      {
  16.          pOldDirectDrawCreate = (_DirectDrawCreate)GetProcAddress( hDll, "DirectDrawCreate" );
  17.          HookAPI( (PVOID *)(&pOldDirectDrawCreate), MyDirectDrawCreate );
  18.      }
  19.  
  20.      LPDIRECTDRAW pDDraw = NULL;
  21.      HRESULT hr = DirectDrawCreate( NULL, &pDDraw, NULL );
  22.      if( DD_OK == hr )
  23.          hr = pDDraw->SetCooperativeLevel( NULL, DDSCL_NORMAL );
  24.  
  25.      return( 0 );
  26.  }

以上代码成功执行后,当我们调用 DirectDrawCreate() 的时候会被之前设置的 hook 函数截获,显示一个对话框。注意 HookAPI() 的第一个参数是指向指针的指针,所以我们开始时拿到的“原始” API 地址其实被换掉了(见上面描述,原始 API 代码已经被我们断成两截啦)。

下面是代码里用到的两个关键数据结构:

  1. #pragma pack (1)
  2.  struct HookTarget
  3.  {
  4.      char jmp;
  5.      int  address;
  6.  
  7.      HookTarget()
  8.      {
  9.          jmp = char( 0xE9 );
  10.      }
  11.  };
  12.  
  13.  struct HookThunk
  14.  {
  15.      char instructions[16];
  16.      HookTarget HookProc;
  17.  
  18.      HookThunk()
  19.      {
  20.          memset( instructions, 0x90, sizeof( instructions ) );
  21.      }
  22.  };
  23.  #pragma pack ()

HookTarget 是用来写覆盖在目标 API 起始处的无条件跳转指令的,HookThunk 是用来把断成两截的 API 代码连起来的。0×90 是 nop 的机器码,就算我们只从目标 API 那里复制了 5 个字节,后面的 nop 也会安全的让执行绪运行到 jmp 指令处。

下面是 HookAPI() 的代码:

  1. bool
  2.  HookAPI( PVOID *ppProc, LPCVOID pMyProc )
  3.  {
  4.      do
  5.      {
  6.          DWORD dwOldProtect = 0;
  7.          if( !RemoveReadOnly( *ppProc, 16, &dwOldProtect ) )
  8.              break;
  9.  
  10.          HookThunk *pThunk = new HookThunk;
  11.          int cbInstructions = CheckInstructions( (LPBYTE)*ppProc );
  12.          memcpy( pThunk, *ppProc, cbInstructions );
  13.          pThunk->HookProc.address = ( (int)*ppProc + cbInstructions )
  14.              - ( (int)pThunk + sizeof( HookThunk ) );
  15.  
  16.          DWORD dwTemp = 0;
  17.          if( !VirtualProtect( pThunk, sizeof( HookThunk ), dwOldProtect, &dwTemp ) )
  18.              break;
  19.  
  20.          HookTarget *pOriginalProc = (HookTarget *)*ppProc;
  21.          pOriginalProc->jmp     = char( 0xE9 );
  22.          pOriginalProc->address = (int)MyDirectDrawCreate - ( (int)pOriginalProc + 5 );
  23.  
  24.          if( !VirtualProtect( *ppProc, 16, dwOldProtect, &dwTemp ) )
  25.              break;
  26.  
  27.          *ppProc = pThunk;
  28.          return( true );
  29.      }
  30.      while( false );
  31.  
  32.      return( false );
  33.  }

就不详细解释了,看完了上面那一大坨原理分析,HookAPI() 这个函数很容易看懂。上面代码里面的 RemoveReadOnly() 就是对 VirtualProtect() 调用的包装而已,对于我们这个示例的目标 API——DirectDrawCreate(),CheckInstructions() 简单的返回 5 就可以了(如果程序执行异常,最好自己用调试器看一下指令边界在哪里)。

  注意以上的示例代码只是用来做测试之用,没有做完备的错误处理。而且在实际环境中目标 API 代码的起始指令边界不一定正好是 5 个字节,如果边界是 4 个字节和 6 个字节处,由于我们至少要覆盖 5 个字节,所以要多复制一些(复制 6 个字节)。另外代码中使用的指令集和地址都只适用于 32bits x86 系统,在其它 CPU 和 OS 上要做相应的修改才能正常运行。最后,在实际的环境中,还要写卸载 hook 的代码;)

Apache mod_rewrite 经验一则

2009-12-14 13:12 | by 2ndboy

  周末呼吸道发炎,今早起床发现还没好,所以在家休养。照理说这么个小毛病是不影响上班地,但是之前有过好几次类似的经验——呼吸道发炎,还去上班,结果在空气污浊的办公司待了一天,随后就严重起来啦!顺便在这儿鄙视一下公司办公室的空气,由于层高太低,电脑众多,通风不畅,基本上下午 2 点一过就头昏脑胀~~再说下去要坏了心情啦:D

  周末研究了一下 Apache 的 rewrite,这个东西有 RE 基础的人上手还是比较快的。不过期间遇到的小问题折腾了好久,我一开始试验的时候规则是放在 httpd.conf 里面的,后来移到 .htaccess 里面就没了效果,开始以为是没有开启对 .htaccess 的支持,后来经过大量折腾,才明白是规则写在 httpd.conf 和写在 .htaccess 里面,收到的 URL 不一样,比如访客用 http://www.foo.com/ 来访问,在 httpd.conf 里面拿到的 URL 串是“/”,而 .htaccess 里拿到的居然是“”,即便 .htaccess 所在目录的内容已经被配置成虚拟主机也一样,所以在 httpd.conf 里面的如下规则:

  1. RewriteRule /$ index.php?rewrite=1 [L]

放到 .htaccess 里面以后要改成:

  1. RewriteRule ^$ index.php?rewrite=1 [L]

  这就是有经验跟没经验的区别,浪费了时间,走了弯路换回来的经验,值得小记一下:)

C++ 类的内存布局

2009-12-6 00:28 | by 2ndboy

  最近工作上的项目需要 hook COM 组件,顺便把 C++ 类实例的内存布局重温和巩固了一下,特别是有 virtual function 的 class。近年来记忆力衰退:D,所以顺便记一笔吧。

  用下面两个有 virtual function 和继承关系的 class 来做例子:

  1. class base
  2.  {
  3.  public:
  4.      virtual int   func1( int   foo ) { return( foo % 3 ); }
  5.      virtual float func2( float foo ) { return( foo / 1.2f ); }
  6.  };
  7.  
  8.  class derived : public base
  9.  {
  10.  public:
  11.      derived() { m = 0xf0f0f0f0; }
  12.  
  13.      virtual int func1( int  foo ) { return( foo + m ); }
  14.      bool func3( bool bar ) { return( !bar ); }
  15.  
  16.  private:
  17.      int m;
  18.  };

C++ 标准里并没有对虚函数的实现做说明,但是几乎所有的编译器都使用虚表来实现 virtual function。下面的这段小程序围绕上面两个类做了一些事情:

  1. typedef struct
  2.  {
  3.      int *pVtbl;
  4.  } class_with_vtbl;
  5.  
  6.  int main( int argc, char *argv[] )
  7.  {
  8.      base    *pBase    = new base;
  9.      derived *pDerived = new derived;
  10.  
  11.      printf( "pBase    = %x\n", pBase );
  12.      printf( "pDerived = %x\n", pDerived );
  13.  
  14.      printf( "sizeof( base )    = %x\n", sizeof( base ) );
  15.      printf( "sizeof( derived ) = %x\n", sizeof( derived ) );
  16.  
  17.      printf( "[pBase]    = %x\n", ((int *)pBase)[0] );
  18.      printf( "[pDerived] = %x\n", ((int *)pDerived)[0] );
  19.  
  20.      printf( "[pBase    + 4] = %x\n", ((int *)pBase)[1] );
  21.      printf( "[pDerived + 4] = %x\n", ((int *)pDerived)[1] );
  22.  
  23.      printf( "pBase->pVtbl[0] = %x\n", ((class_with_vtbl *)pBase)->pVtbl[0] );
  24.      printf( "pBase->pVtbl[1] = %x\n", ((class_with_vtbl *)pBase)->pVtbl[1] );
  25.  
  26.      printf( "pDerived->pVtbl[0] = %x\n", ((class_with_vtbl *)pDerived)->pVtbl[0] );
  27.      printf( "pDerived->pVtbl[1] = %x\n", ((class_with_vtbl *)pDerived)->pVtbl[1] );
  28.  
  29.      printf( "pBase->func1 = %x\n", pBase->func1 );
  30.      printf( "pBase->func2 = %x\n", pBase->func2 );
  31.  
  32.      printf( "pDerived->func1 = %x\n", pDerived->func1 );
  33.      printf( "pDerived->func2 = %x\n", pDerived->func2 );
  34.      printf( "pDerived->func3 = %x\n", pDerived->func3 );
  35.  
  36.      pDerived->func1( 0 );
  37.  
  38.      typedef int (base::* func1_type)( int );
  39.      func1_type pFunc1 = pBase->func1;
  40.      (pBase->*pFunc1)( 0 );
  41.  
  42.      return 0;
  43.  }

程序在 VC6 下编译,某次的执行结果如下:

  1. pBase    = 431a20
  2. pDerived = 4319e0
  3. sizeof( base )    = 4
  4. sizeof( derived ) = 8
  5. [pBase]    = 42720c
  6. [pDerived] = 4271b4
  7. [pBase    + 4] = fdfdfdfd
  8. [pDerived + 4] = f0f0f0f0
  9. pBase->pVtbl[0] = 401023
  10. pBase->pVtbl[1] = 401005
  11. pDerived->pVtbl[0] = 40100a
  12. pDerived->pVtbl[1] = 401005
  13. pBase->func1 = 401028
  14. pBase->func2 = 40102d
  15. pDerived->func1 = 401028
  16. pDerived->func2 = 40102d
  17. pDerived->func3 = 401037

接下来通过对程序运行结果的解读来分析一下带有虚函数的类实例在内存中的布局(程序用 VC 6 编译)。结果中的前两行是两个类实例的内存地址。

接下来的第 3、4 行显示的是两个类实例占用的内存大小,base 没有成员变量,但是占用了 4 个 byte,而它是有 2 个虚函数的,所以这 4 个字节是一个指向虚表的指针;derived 有一个 int 型的成员变量,它同样也有虚函数,所以占用 8 个字节。

第 5、6 行是两个类实例所在内存的第一个 dword 的内容,没什么特别;但是看看第 7、8 行(显示两个类实例所在内存的第二个 dword 内容),第 8 行表明了虚表总是位于任何成员变量之前的,也就是说虚表指针总是在类实例内存布局中的第一项。

在上面程序里,我们用一个名叫 class_with_vtbl 的结构来强转类实例指针,从而可以方便的展示虚表的内容。9~12 行就是 base 和 derived 的虚表内容,可以看到,它们虚表的第二项内容相同,这是因为 derived 没有改写基类的 func2。

接下来 13~16 行的结果看上去比较令人费解:
1) pBase->func1 跟 pBase->pVtbl[0] 的内容不符,这么说来,虚表里的内容不是指向虚函数的吗?其实如果说这是一个 surprise 的话,你在调试时在 watch 窗口里输入 base::func1 看到的东西会更让你惊讶,这样看到的值居然是 0×401350!
2) 如果说 base::func2() 和 derived::func2() 的地址相同还可以理解的话,则么居然 base::func1() 和 derived::func1() 的值竟然也一样!

下面先来解决第 2 个疑问,代码和实际数据说明一切,在调试器里看一下 401028 及其周边的内容:

  1. @ILT+0(?func2@base@@UAEMM@Z):
  2. 00401005   jmp         base::func2 (00401250)
  3. @ILT+5(?func1@derived@@UAEHH@Z):
  4. 0040100A   jmp         derived::func1 (00401290)
  5. @ILT+10(??0base@@QAE@XZ):
  6. 0040100F   jmp         base::base (00401310)
  7. @ILT+15(??0derived@@QAE@XZ):
  8. 00401014   jmp         derived::derived (004011f0)
  9. @ILT+20(_main):
  10. 00401019   jmp         main (00410130)
  11. @ILT+25(?func3@derived@@UAE_N_N@Z):
  12. 0040101E   jmp         derived::func3 (004012d0)
  13. @ILT+30(?func1@base@@UAEHH@Z):
  14. 00401023   jmp         base::func1 (00401350)
  15. 00401028   jmp         `vcall' (00401240)
  16. 0040102D   jmp         `vcall' (00401280)
  17. 00401032   jmp         `vcall' (00401340)
  18. 00401037   jmp         derived::func3 (004012d0)

可以看到 401028 地址处有一条 jmp 指令,它其实是个 vcall,可以理解为专门用来调用虚函数(vfunction)的 call。401028 处的 vcall 要跳转到 401240 处,所以我们来看一下该地址处的代码:

  1. `vcall':
  2. 00401240   mov         eax,dword ptr [ecx]
  3. 00401242   jmp         dword ptr [eax]

在做 vcall 之前,caller 要把类实例指针放在 ecx 里面,这个 vcall 事实上是调用所有对象第一个虚函数的通用代码,它把虚表的第一项取出来放在 eax 里,接着就跳转到 eax 所含的那个地址去继续执行了。看看 401280 处(也是 pBase->func2 和 pBase->func2 共同指向的内容 )另外一个 vcall 的实现会更清楚一些:

  1. `vcall':
  2. 00401280   mov         eax,dword ptr [ecx]
  3. 00401282   jmp         dword ptr [eax+4]

由于是第二个虚函数,所以在把虚表入口地址放入 eax 之后,程序是取出了虚表的第二项([eax+4]),接着才跳转过去的。

这就解释了刚才的第 2 个疑问,其实在程序里用 pBase->func1 拿到的并不是 func1 的真实入口地址,而只是一个通用的 vcall thunk 地址。

下面来解决第 1 个疑问,通过上面的描述,现在可以知道 pBase->func1 指向的其实是 vcall thunk,那么 pBase->pVtbl[0] 指向的又是什么?还是来看代码,其实上面已经列出来了:

  1. @ILT+30(?func1@base@@UAEHH@Z):
  2. 00401023   jmp         base::func1 (00401350)

居然也是一条 jmp 指令,不过注意一下它 jump 到的地址——401350,这个值跟我们在 VC watch 窗口里看到 base::func1 的值是一样的,其实这个才是真正 base::func1 的地址!问题解决。

这里稍总结一下:
a. 用代码取 pBase->func1 取到的地址其实指向的是 vcall thunk,vcall thunk 最终也是借助虚表里的内容找到真正虚函数地址的
b. 虚表表项也并非直接指向相应的虚函数地址,而是指向一个跳转指令(这貌似有点兜圈子),这个跳转指令才真正的把执行绪引到真实函数体
c. 用 VC 调试器 watch 到的地址就直接是真正的函数入口地址

接下来的篇幅,我们来实际看看用虚表和 vcall 两种方法调用虚函数的过程。看上面实例代码的最后两坨:

  1. pDerived->func1( 0 );

这句代码编译后变成:

  1. 0041031D   mov         esi,esp
  2. 0041031F   push        0
  3. 00410321   mov         edx,dword ptr [ebp-14h]
  4. 00410324   mov         eax,dword ptr [edx]
  5. 00410326   mov         ecx,dword ptr [ebp-14h]
  6. 00410329   call        dword ptr [eax]

[ebp-14h] 处的 pDerived 对象指针被放入 edx,接着虚表里的第一项被放入 eax,call [eax] 导致代码流程跑到虚表项所指的那个地址继续执行(我们已经知道了,那个地方又是一个 jmp)。可以看到在调用虚函数之前,代码把对象指针(this 指针)存入 ecx,这是 __thiscall 调用习惯(其它调用习惯中,this 指针通过堆栈传递)。

接下来看看 vcall:

  1. typedef int (base::* func1_type)( int );
  2.  func1_type pFunc1 = pBase->func1;
  3.  (pBase->*pFunc1)( 0 );

成员函数类型无法强转成其它普通指针类型,所以要定义一个“::*”类型的指针来存放它,调用的时候也要用 ->*(或 .*,如果左边是非指针类型的对象)。编译后的汇编代码如下:

  1. 00410332   mov         dword ptr [ebp-18h],offset @ILT+35(`vcall') (00401028)
  2. 00410339   mov         ecx,dword ptr [ebp-18h]
  3. 0041033C   mov         dword ptr [ebp-18h],ecx
  4. 0041033F   mov         esi,esp
  5. 00410341   push        0
  6. 00410343   mov         ecx,dword ptr [ebp-10h]
  7. 00410346   call        dword ptr [ebp-18h]

调用类实例第一个虚函数的 vcall thunk 地址被放入 [ebp-18h],接着 pBase 的指针(this 指针)被放入 ecx,call [ebp-18h] 导致程序跳转到 vcall thunk 处执行,最终导致 pBase 的第一个虚函数被调用。