65.1 线性同余发生器

前面第20章的纯随机数生成器有一个缺陷:它不是线程安全的,因为它的内部状态变量可以被不同的线程同时读取或修改。

65.1.1 Win32

未初始化的TLS数据

一个全局变量如果添加了_declspec(thread)修饰符,那么它会被分配在TLS。

  1. #include <stdint.h>
  2. #include <windows.h>
  3. #include <winnt.h>
  4. // from the Numerical Recipes book
  5. #define RNG_a 1664525
  6. #define RNG_c 1013904223
  7. __declspec( thread ) uint32_t rand_state;
  8. void my_srand (uint32_t init)
  9. {
  10. rand_state=init;
  11. }
  12. int my_rand ()
  13. {
  14. rand_state=rand_state*RNG_a;
  15. rand_state=rand_state+RNG_c;
  16. return rand_state & 0x7fff;
  17. }
  18. int main()
  19. {
  20. my_srand(0x12345678);
  21. printf ("%d\n", my_rand());
  22. };

使用Hiew可以看到PE文件多了一个section:.tls。

Listing 65.2: Optimizing MSVC 2013 x86

  1. _TLS SEGMENT
  2. _rand_state DD 01H DUP (?)
  3. _TLS ENDS
  4. _DATA SEGMENT
  5. $SG84851 DB '%d', 0aH, 00H
  6. _DATA ENDS
  7. _TEXT SEGMENT
  8. _init$ = 8 ; size = 4
  9. _my_srand PROC
  10. ; FS:0=address of TIB
  11. mov eax, DWORD PTR fs:__tls_array ; displayed in IDA as FS:2Ch
  12. ; EAX=address of TLS of process
  13. mov ecx, DWORD PTR __tls_index
  14. mov ecx, DWORD PTR [eax+ecx*4]
  15. ; ECX=current TLS segment
  16. mov eax, DWORD PTR _init$[esp-4]
  17. mov DWORD PTR _rand_state[ecx], eax
  18. ret 0
  19. _my_srand ENDP
  20. _my_rand PROC
  21. ; FS:0=address of TIB
  22. mov eax, DWORD PTR fs:__tls_array ; displayed in IDA as FS:2Ch
  23. ; EAX=address of TLS of process
  24. mov ecx, DWORD PTR __tls_index
  25. mov ecx, DWORD PTR [eax+ecx*4]
  26. ; ECX=current TLS segment
  27. imul eax, DWORD PTR _rand_state[ecx], 1664525
  28. add eax, 1013904223 ; 3c6ef35fH
  29. mov DWORD PTR _rand_state[ecx], eax
  30. and eax, 32767 ; 00007fffH
  31. ret 0
  32. _my_rand ENDP
  33. _TEXT ENDS

rand_state现在处于TLS段,而且这个变量每个线程都拥有属于自己版本。它是这么访问的:从FS:2Ch加载TIB(Thread Information Block)的地址,然后添加一个额外的索引(如果需要的话),接着计算出在TLS段的地址。

最后可以通过ECX寄存器来访问rand_state变量,它指向每个线程特定的数据区域。

FS:这是每个逆向工程师都很熟悉的选择子了。它专门用于指向TIB,因此访问线程特定数据可以很快完成。

GS: 该选择子用于Win64,0x58的地址是TLS。

Listing 65.3: Optimizing MSVC 2013 x64

  1. _TLS SEGMENT
  2. rand_state DD 01H DUP (?)
  3. _TLS ENDS
  4. _DATA SEGMENT
  5. $SG85451 DB '%d', 0aH, 00H
  6. _DATA ENDS
  7. _TEXT SEGMENT
  8. init$ = 8
  9. my_srand PROC
  10. mov edx, DWORD PTR _tls_index
  11. mov rax, QWORD PTR gs:88 ; 58h
  12. mov r8d, OFFSET FLAT:rand_state
  13. mov rax, QWORD PTR [rax+rdx*8]
  14. mov DWORD PTR [r8+rax], ecx
  15. ret 0
  16. my_srand ENDP
  17. my_rand PROC
  18. mov rax, QWORD PTR gs:88 ; 58h
  19. mov ecx, DWORD PTR _tls_index
  20. mov edx, OFFSET FLAT:rand_state
  21. mov rcx, QWORD PTR [rax+rcx*8]
  22. imul eax, DWORD PTR [rcx+rdx], 1664525 ;0019660dH
  23. add eax, 1013904223 ; 3c6ef35fH
  24. mov DWORD PTR [rcx+rdx], eax
  25. and eax, 32767 ; 00007fffH
  26. ret 0
  27. my_rand ENDP
  28. _TEXT ENDS

初始化TLS数据

比方说,我们想为rand_state设置一些固定的值以避免程序员忘记初始化。

  1. #include <stdint.h>
  2. #include <windows.h>
  3. #include <winnt.h>
  4. // from the Numerical Recipes book
  5. #define RNG_a 1664525
  6. #define RNG_c 1013904223
  7. __declspec( thread ) uint32_t rand_state=1234;
  8. void my_srand (uint32_t init)
  9. {
  10. rand_state=init;
  11. }
  12. int my_rand ()
  13. {
  14. rand_state=rand_state*RNG_a;
  15. rand_state=rand_state+RNG_c;
  16. return rand_state & 0x7fff;
  17. }
  18. int main()
  19. {
  20. printf ("%d\n", my_rand());
  21. };

代码除了给rand_state设定初始值外与之前的并没有什么不同,但在IDA我们看到:

  1. .tls:00404000 ; Segment type: Pure data
  2. .tls:00404000 ; Segment permissions: Read/Write
  3. .tls:00404000 _tls segment para public 'DATA' use32
  4. .tls:00404000 assume cs:_tls
  5. .tls:00404000 ;org 404000h
  6. .tls:00404000 TlsStart db 0 ; DATA XREF: .rdata:TlsDirectory
  7. .tls:00404001 db 0
  8. .tls:00404002 db 0
  9. .tls:00404003 db 0
  10. .tls:00404004 dd 1234
  11. .tls:00404008 TlsEnd db 0 ; DATA XREF: .rdata:TlsEnd_pt
  12. ...

每次一个新的线程运行的时候,会分配新的TLS给它,然后包括1234所有数据将被拷贝过去。

这是一个典型的场景: - 线程A开始运行,然后分配给它一个TLS,并把1234拷贝到rand_state。 - 线程A里面多次调用my_rand()函数,rand_state已经不是1234。 - 线程B开始运行,然后分配给它一个TLS,并把1234拷贝到rand_state,这时候可以观察到两个线程使用同一个变量,但它们的值是不一样的。

TLS callbacks

如果我们想给TLS赋一个变量值呢?比方说:程序员忘记调用my_srand()函数来初始化PRNG,但是随机数生成器在开始的时候必须使用一个真正的随机数值而不是1234。这种情况下则可以使用TLS callbaks。

下面的代码的可移植性很差,原因你应该明白。我们定义了一个函数(tls_callback()),它在进程/线程开始执行前调用,该函数使用GetTickCount()函数的返回值来初始化PRNG。

  1. #include <stdint.h>
  2. #include <windows.h>
  3. #include <winnt.h>
  4. // from the Numerical Recipes book
  5. #define RNG_a 1664525
  6. #define RNG_c 1013904223
  7. __declspec( thread ) uint32_t rand_state;
  8. void my_srand (uint32_t init)
  9. {
  10. rand_state=init;
  11. }
  12. void NTAPI tls_callback(PVOID a, DWORD dwReason, PVOID b)
  13. {
  14. my_srand (GetTickCount());
  15. }
  16. #pragma data_seg(".CRT$XLB")
  17. PIMAGE_TLS_CALLBACK p_thread_callback = tls_callback;
  18. #pragma data_seg()
  19. int my_rand ()
  20. {
  21. rand_state=rand_state*RNG_a;
  22. rand_state=rand_state+RNG_c;
  23. return rand_state & 0x7fff;
  24. }
  25. int main()
  26. {
  27. // rand_state is already initialized at the moment (using GetTickCount())
  28. printf ("%d\n", my_rand());
  29. };

用IDA看一下:

Listing 65.4: Optimizing MSVC 2013

  1. .text:00401020 TlsCallback_0 proc near ; DATA XREF: .rdata:TlsCallbacks
  2. .text:00401020 call ds:GetTickCount
  3. .text:00401026 push eax
  4. .text:00401027 call my_srand
  5. .text:0040102C pop ecx
  6. .text:0040102D retn 0Ch
  7. .text:0040102D TlsCallback_0 endp
  8. ...
  9. .rdata:004020C0 TlsCallbacks dd offset TlsCallback_0 ; DATA XREF: .rdata:TlsCallbacks_ptr
  10. ...
  11. .rdata:00402118 TlsDirectory dd offset TlsStart
  12. .rdata:0040211C TlsEnd_ptr dd offset TlsEnd
  13. .rdata:00402120 TlsIndex_ptr dd offset TlsIndex
  14. .rdata:00402124 TlsCallbacks_ptr dd offset TlsCallbacks
  15. .rdata:00402128 TlsSizeOfZeroFill dd 0
  16. .rdata:0040212C TlsCharacteristics dd 300000h

TLS callbacks函数时常用于隐藏解包处理过程。为此有些人可能会困惑,为什么一些代码可以偷偷地在OEP(Original Entry Point)之前执行。

65.1.2 Linux

下面是GCC声明线程局部存储的方式:

  1. __thread uint32_t rand_state=1234;

这不是标准C/C++的修饰符,但是是GCC的一个扩展特性。

GS:该选择子同样用于访问TLS,但稍微有点区别:

Listing 65.5: Optimizing GCC 4.8.1 x86

  1. .text:08048460 my_srand proc near
  2. .text:08048460
  3. .text:08048460 arg_0 = dword ptr 4
  4. .text:08048460
  5. .text:08048460 mov eax, [esp+arg_0]
  6. .text:08048464 mov gs:0FFFFFFFCh, eax
  7. .text:0804846A retn
  8. .text:0804846A my_srand endp
  9. .text:08048470 my_rand proc near
  10. .text:08048470 imul eax, gs:0FFFFFFFCh, 19660Dh
  11. .text:0804847B add eax, 3C6EF35Fh
  12. .text:08048480 mov gs:0FFFFFFFCh, eax
  13. .text:08048486 and eax, 7FFFh
  14. .text:0804848B retn
  15. .text:0804848B my_rand endp

更多例子:ELF Handling For Thread-Local Storage #66章 系统调用(syscall-s)

众所周知,所有运行的进程在操作系统里面分为两类:一类拥有访问全部硬件设备的权限(内核空间)而另一类无法直接访问硬件设备(用户空间)。

操作系统内核和驱动程序通常是属于第一类的。

而应用程序通常是属于第二类的。

举个例子,Linux kernel运行于内核空间,而Glibc运行于用户空间。

这种分离对与操作系统的安全性是至关重要的:它最重要的一点是,不给任何进程有破坏到其它进程甚至是系统内核的机会。另一方面,一个错误的驱动或系统内核错误都会造成系统崩溃或者蓝屏。

保护模式下的x86处理器允许使用4个保护等级(ring)。但Linux和Windows两个操作系统都只使用了两个:ring0(内核空间)和ring3(用户空间)。

系统调用(syscall-s)是两个运行空间的连接点。可以说,这是提供给应用程序主要的API。

在Windows NT,系统调用表存在于SSDT。

通过系统调用实现shellcode在计算机病毒作者之间非常流行。因为很难确定所需函数在系统库里面的地址,但系统调用很容易确定。然而,由于系统调用属于比较底层的API,所以需要编写更多的代码。最后值得一提的是,在不同的操作系统版本里面,系统调用号是有可能不同的。