July 26, 2014

Inspection of SAR Instructions

SAR stands for Shift Arithmetic Right and the instruction performs arithmetic shift. The instruction preserves the sign of the value to be shifted and so the vacant bits are filled according to the sign-bit.

Compilers generate SAR instruction when right shift operator ">>" is used on a signed integer.

The use of SAR instruction can potentially lead to create a signedness bug if it's assumed the shift is unsigned.

Given the following simplified example.

char retItem(char* arr, int value)
{
    return arr[value>>24];
}

If value is positive the code is working as expected. However if value is negative the program can read out of the bounds of arr.

Other example would be to compare the signed value after the shift to an unsigned value leading to implicit conversion that may lead to trigger bug.

In my experiment, in several cases, it is seen that memory is being dereferenced involving SAR instruction. These places may be worthy to look for bugs, specially if the value to be shifted is a user input or is a controlled one.

If an unsigned jump is followed by a signed shift that could be a potential to look for bugs as well.

Regular expressions or scripts can be used to search for patterns of occurrences of SAR instructions. When it's not feasible to review all occurrences of SAR, a pintool may be used to highlight what SAR instructions have been executed, and only focus on those executed.

July 22, 2014

Examining Native Code by Looking for Patterns

Earlier this year a post was published of examining data format without using the program that reads the format. That post discusses patterns to look for, in order to identify certain constructs. This post focuses on static methods of examining code that can be either the complete code section of the file, memory dump, or just fragment. It also describes selected ideas what patterns to look for when examining a given code.

The reason one may look for patterns in code is to locate certain functionalities or to get high-level understanding of what the code does. Others may look for certain construct that may be the key part of the program in security point of view.

It's true to say one can expect this to be a rapid method compared to other methods such as line-by-line instruction analysis.

But, it's always good to read documentation, if possible at all, to get an overview of the expectations.

There are methods that more effective if performed on small region. Therefore to narrow the scope of the search wherein to look for pattern is something good to do at the beginning of analysis albeit it's not always feasible to do with enough certainty. Anyway, one can always widen the search region if required at a later stage.

Compilers tend to produce executable files with particular layout. Some have the library code at the beginning of the code section, while others have it at the end of the code section.

If there is no information about the compiler or no information about the layout there are other ways to locate the library in the code.

You may look for library function calls that can be visible in disassembler. Library code may have distinct color in disassembler.

Library/runtime code often have many implementations of functions to use the advantage of latest hardware. An example is MSVC. And so SSE instructions/functions may indicate the presence of library/runtime code.

Library code can be spotted by looking for strings can be associated with particular libraries.

Library/runtime code can be spotted by looking for constant values that can be associated with particular libraries such as cryptographic libraries that tend to have many constants.

To guess the compiler that was used to generate the code is possible by analyzing the library/runtime code.

In case the code is just a fragment of user code you may consider examining the instructions how they are encoded. Intel encodings are redundant and one instruction can have multiple encodings. This is something to make guess on what compiler was used.

If multiple encodings of an instruction is found in a binary the code that could be generated with a polymorphic encoder.

Also, code has other characteristics that may differ between compilers such as padding and stack allocation.

Imports and exports as well as strings can tell a lot. You may check where they are referenced in the code.

Debugging symbols can help awfully lot if the disassembler can handle that. Sometimes it's available sometimes it's not.

No matter what code you're looking at it most likely deals with input data. That case it may get the data from file, from network, via standard API calls. These are valuable areas to audit for security problems, and it's possible to follow how the data returned by these APIs. It may require to analyze caller functions as usually these APIs are wrapped around many calls before using the input.

Just like when reading the data the code may write data, or send data via standard API calls. These areas may be security-sensitive.

Programs have centralized, well-established functions. These functions, for example, read dword values, read data into structures and propagate any other internal storage. Discovery of these functions not considered hard, they are normally small, and have instructions of memory read and write. By looking where they referenced from we can find good attack surfaces.

Good to keep in mind that code sections can contain data besides code. But normally data is stored in data section. In the disassembler it's convenient to see how the data is referenced, and may decide if there is an attack surface nearby.

CRC and hash constants may indicate there is some data which is being CRC'd or hashed. You may figure out where is that data from and how can you perform security testing around.

When a library is using a parameter hardcoded it's often encoded as a part of the instruction rather than stored in data section of the executable. Example encoding looks like mov eax, <param> or mov al, <param>.

When a data format is parsed often a magic value is tested. Looking for instructions like cmp reg, <magic> or cmp dword ptr [addr], <magic> or similar instructions can help to locate attack surfaces.

Longer strings may be broken into immediate values and compared with multiple cmp instructions.

Looking for strcmp function calls is good idea to look for if you want to find code that test for data format as often strcmp functions are used for this purpose.

If the code is optimized for speed there are many ways to confirm. Normally the readability of code bad, for example when the code performs division or use the same memory address for multiple variables. If EBP register is used in arithmetic or other than to store stack base address that could indicate the code is optimized.

Perhaps there are circumstances when looking at the frequency of instructions, looking for undocumented instructions, or rare instruction, or instructions that not present can give us valuable clues that help the examination.

Intuitively going through the code and looking for undefined patterns can be good idea if the scientific ways have been exhausted.

July 16, 2014

251 Potential NULL Pointer Dereferences in Flash Player

251 potential NULL pointer dereference issues have been identified in Flash Player 14 by pattern matching approach. The file examined is NPSWF32_14_0_0_145.dll (17,029,808 bytes).

The issues are classified as CWE-690: Unchecked Return Value to NULL Pointer Dereference.

I don't copy&paste all the issues in this blog post but bringing up few examples.

First Example

0:012> uf 5438a1d0
NPSWF32_14_0_0_145!BrokerMainW+0xf6f6b:
5438a1d0 f6410810        test    byte ptr [ecx+8],10h
5438a1d4 8b4104          mov     eax,dword ptr [ecx+4]
5438a1d7 7411            je      NPSWF32_14_0_0_145!BrokerMainW+0xf6f85 (5438a1ea)

NPSWF32_14_0_0_145!BrokerMainW+0xf6f74:
5438a1d9 85c0            test    eax,eax
5438a1db 740b            je      NPSWF32_14_0_0_145!BrokerMainW+0xf6f83 (5438a1e8)

NPSWF32_14_0_0_145!BrokerMainW+0xf6f78:
5438a1dd 8b4c2404        mov     ecx,dword ptr [esp+4]
5438a1e1 8b448808        mov     eax,dword ptr [eax+ecx*4+8]
5438a1e5 c20400          ret     4

NPSWF32_14_0_0_145!BrokerMainW+0xf6f83:
5438a1e8 33c0            xor     eax,eax <--Set return value to NULL

NPSWF32_14_0_0_145!BrokerMainW+0xf6f85:
5438a1ea c20400          ret     4 <--Return with NULL
0:012> u 5438a47b L2
NPSWF32_14_0_0_145!BrokerMainW+0xf7216:
5438a47b e850fdffff      call    NPSWF32_14_0_0_145!BrokerMainW+0xf6f6b (5438a1d0)
5438a480 8a580c          mov     bl,byte ptr [eax+0Ch] <--Dereference NULL 

Second Example

0:012> uf 54362e60
NPSWF32_14_0_0_145!BrokerMainW+0xcfbfb:
54362e60 8b4128          mov     eax,dword ptr [ecx+28h]
54362e63 8b4c2404        mov     ecx,dword ptr [esp+4]
54362e67 3b4804          cmp     ecx,dword ptr [eax+4]
54362e6a 7205            jb      NPSWF32_14_0_0_145!BrokerMainW+0xcfc0c (54362e71)

NPSWF32_14_0_0_145!BrokerMainW+0xcfc07:
54362e6c 33c0            xor     eax,eax <--Set return value to NULL
54362e6e c20400          ret     4 <--Return with NULL

NPSWF32_14_0_0_145!BrokerMainW+0xcfc0c:
54362e71 56              push    esi
54362e72 8b748808        mov     esi,dword ptr [eax+ecx*4+8]
54362e76 56              push    esi
54362e77 e8e4b0faff      call    NPSWF32_14_0_0_145!BrokerMainW+0x7acfb (5430df60)
54362e7c 83c404          add     esp,4
54362e7f 85c0            test    eax,eax
54362e81 7407            je      NPSWF32_14_0_0_145!BrokerMainW+0xcfc25 (54362e8a)

NPSWF32_14_0_0_145!BrokerMainW+0xcfc1e:
54362e83 8b4010          mov     eax,dword ptr [eax+10h]
54362e86 5e              pop     esi
54362e87 c20400          ret     4

NPSWF32_14_0_0_145!BrokerMainW+0xcfc25:
54362e8a 8bc6            mov     eax,esi
54362e8c 83e0f8          and     eax,0FFFFFFF8h
54362e8f 5e              pop     esi
54362e90 c20400          ret     4
0:012> u NPSWF32_14_0_0_145+006b4eb2 L2
NPSWF32_14_0_0_145!BrokerMainW+0xd1c4d:
54364eb2 e8a9dfffff      call    NPSWF32_14_0_0_145!BrokerMainW+0xcfbfb (54362e60)
54364eb7 8b7004          mov     esi,dword ptr [eax+4] <--Dereference NULL

Third Example

0:012> uf 5429979a
NPSWF32_14_0_0_145!BrokerMainW+0x6535:
5429979a 0fb74108        movzx   eax,word ptr [ecx+8]
5429979e 48              dec     eax
5429979f 48              dec     eax
542997a0 740c            je      NPSWF32_14_0_0_145!BrokerMainW+0x6549 (542997ae)

NPSWF32_14_0_0_145!BrokerMainW+0x653d:
542997a2 83e815          sub     eax,15h
542997a5 7403            je      NPSWF32_14_0_0_145!BrokerMainW+0x6545 (542997aa)

NPSWF32_14_0_0_145!BrokerMainW+0x6542:
542997a7 33c0            xor     eax,eax <--Set return value to NULL
542997a9 c3              ret <--Return with NULL

NPSWF32_14_0_0_145!BrokerMainW+0x6545:
542997aa 8d4110          lea     eax,[ecx+10h]
542997ad c3              ret

NPSWF32_14_0_0_145!BrokerMainW+0x6549:
542997ae 8d410c          lea     eax,[ecx+0Ch]
542997b1 c3              ret
0:012> u NPSWF32_14_0_0_145+005f3423 L2
NPSWF32_14_0_0_145!BrokerMainW+0x101be:
542a3423 e87263ffff      call    NPSWF32_14_0_0_145!BrokerMainW+0x6535 (5429979a)
542a3428 8038fe          cmp     byte ptr [eax],0FEh <--Dereference NULL

You can find a list of 251 potential NULL pointer dereferences in Flash Player here.

July 14, 2014

Issues with Flash Player & Firefox in Non-default Configurations

Few months ago I encountered a bug when a fuzzed flash file is being rendered by Flash Player in Firefox. This bug can be reached only in the non-default configuration described below so very unlikely you are affected by this bug.

To trigger the bug the flash player module has to be loaded into Firefox's virtual address space. And this can be achieved if Flash Player protected mode is disabled and Firefox plugin container process is disabled too.

The bug involves to dereference arbitrary memory address via a CALL instruction in the vtable dispatcher. Here you can see the bug in the exception state.

0:048> g
Implementation limit exceeded: attempting to allocate too-large object
error: out of memory
(170fc.16998): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=00000001 ebx=00000000 ecx=0034f670 edx=00000000 esi=1600f2c8 edi=0000001c
eip=5996bd5f esp=0034f638 ebp=0034f668 iopl=0         nv up ei pl nz na po nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010202
*** ERROR: Symbol file could not be found.  Defaulted to export symbols for C:\Windows\SysWOW64\Macromed\Flash\NPSWF32_14_0_0_145.dll - 
NPSWF32_14_0_0_145!unuse_netscape_plugin_Plugin+0x5c2:
5996bd5f 8b461c          mov     eax,dword ptr [esi+1Ch] ds:002b:1600f2e4=????????
0:000> u eip L10
NPSWF32_14_0_0_145!unuse_netscape_plugin_Plugin+0x5c2:
5996bd5f 8b461c          mov     eax,dword ptr [esi+1Ch] <--Read unmapped address
5996bd62 a801            test    al,1
5996bd64 7420            je      NPSWF32_14_0_0_145!unuse_netscape_plugin_Plugin+0x5e9 (5996bd86)
5996bd66 33d2            xor     edx,edx
5996bd68 39550c          cmp     dword ptr [ebp+0Ch],edx
5996bd6b 7519            jne     NPSWF32_14_0_0_145!unuse_netscape_plugin_Plugin+0x5e9 (5996bd86)
5996bd6d 8b4e04          mov     ecx,dword ptr [esi+4]
5996bd70 83e0fe          and     eax,0FFFFFFFEh
5996bd73 89461c          mov     dword ptr [esi+1Ch],eax
5996bd76 8b06            mov     eax,dword ptr [esi] <--Read unmapped address
5996bd78 51              push    ecx
5996bd79 8bce            mov     ecx,esi
5996bd7b 895604          mov     dword ptr [esi+4],edx
5996bd7e 895618          mov     dword ptr [esi+18h],edx
5996bd81 ff500c          call    dword ptr [eax+0Ch] <--Dereference arbitrary memory content
5996bd84 eb06            jmp     NPSWF32_14_0_0_145!unuse_netscape_plugin_Plugin+0x5ef (5996bd8c)

I had reported this bug to Adobe and they opened a case PSIRT-2707 on 14/April/2014 but so far Adobe didn't confirm whether or not it was able to reproduce the bug or the exception state reported.

Again, the bug doesn't affect the default configuration, and so very unlikely you're affected by this. However, users using Firefox with plugin-container disabled as well as Flash Player plugin with protected mode disabled are affected by this issue.

The original report is about Flash Player 13_0_0_182 and Firefox 28.0 but the testcase fails with Flash Player 14_0_0_145 and Firefox 30.0 (latest available till today).

These are the steps to reproduce the bug.
  • Edit mms.cfg to have ProtectedMode=0 to disable protected mode in Flash Player
  • Start cmd.exe and type "set MOZ_DISABLE_OOP_PLUGINS=1" to disable plugin-container in Firefox
These settings above required to get Flash Player plugin loaded in firefox.exe's address space.
  • Start Firefox from command prompt opened previously
  • Open fuzzed.swf in Firefox (drag n drop should work)
  • Attach firefox.exe process to Windbg when you notice that Firefox is hanging
  • Exception should occur in few second. If you see the out-of-memory error in the debugger log without exception you may restart the browser and try again.
The fuzzed flash file has the following changes compared to the template file. The value of the first item in the integer pool has been changed to a large value. TagLength of DoAbc tag and FileSize of the main header have been therefore updated to maintain the integrity of the flash file.

Drop me an email if you think you need the testcase.

May 13, 2014

Security Implications of IsBad*Ptr Calls in Binaries

IsBad*Ptr [1] functions are to test whether the memory range specified in the argument list is accessible. Despite the fact they have been banned, they are still being referenced in many binaries shipped with popular applications.

In this post I'm describing the inner working of IsBad*Ptr, the steps the attacker may follow to abuse them, and mention few examples of binaries that have a reference to these banned functions.

Inner Working

When IsBad*Ptr is executed it first registers an exception handler. Then, it attempts to access to the memory specified in the argument list.

For example, IsBadReadPtr has the following instruction to read memory. ECX is the memory address specified in the argument list.

mov al,byte ptr [ecx]

If the instruction raises an exception, the execution is transferred to the exception-handler code. And IsBad*Ptr returns TRUE, meaning, it is a "bad" pointer because the data pointed by is inaccessible.

If the instruction executes without an exception being raised IsBad*Ptr returns FALSE.

Steps of Attacking

The attack against IsBad*Ptr looks like this.
  1. The attacker attempts to supply an invalid pointer parameter to IsBad*Ptr that returns TRUE.
  2. The attacker refines step #1 in a way that the supplied invalid pointer becomes valid due to a forced allocation for the location pointed by the invalid pointer. The attacker refines step #1 in a way that the supplied invalid pointer will point to valid memory location allocated by heap spray. And so, IsBad*Ptr returns FALSE leading to enter in an inconsistent state; that may or may not be an exploitable state.
  3. If the attacker can perform step #2 with IsBadWritePtr, when the call returns, it's expected to reach code that writes the location pointed by the pointer -- and that has attacker controlled data. And so, he reaches a presumably exploitable condition.
Referencing IsBad*Ptr can be easily checked during binary analysis and it is worthy to do.

Examples

This code snippet below can be found in msvbvm60.dll in Windows folder.

.text:72A0FEE5         push    38h                     ; ucb
.text:72A0FEE7         push    edi                     ; attacker supplies pointer was invalid before
.text:72A0FEE8         call    ds:IsBadReadPtr         ; and now it's valid because he's filled memory up
.text:72A0FEEE         test    eax, eax
.text:72A0FEF0         jnz     loc_72A0FF80            ; fall through
.text:72A0FEF6         mov     eax, [edi+4]
.text:72A0FEF9         mov     eax, [eax+4]
.text:72A0FEFC         mov     esi, [eax+8]            ; ESI is attacker controlled
.text:72A0FEFF         and     [ebp+arg_0], 0
.text:72A0FF03         mov     ax, [edi+2]
.text:72A0FF07         test    esi, esi
.text:72A0FF09         jz      short loc_72A0FF42      ; fall through
.text:72A0FF0B         movzx   ebx, ax
.text:72A0FF0E         mov     eax, [esi]              ; EAX is attacker controlled
.text:72A0FF10         push    esi
.text:72A0FF11         call    dword ptr [eax+0Ch]     ; EIP is attacker controlled


This one below can be found in dxtrans.dll in Windows folder.

.text:35C6142C         push    4                       ; ucb
.text:35C6142E         push    esi                     ; attacker supplies pointer was invalid before
.text:35C6142F         call    ds:__imp__IsBadWritePtr@8 ; and now it's valid because he's filled memory up
.text:35C61435         test    eax, eax
.text:35C61437         jz      short loc_35C61440      ; jump is taken
.text:35C61439         mov     eax, 80004003h
.text:35C6143E         jmp     short loc_35C6144A
.text:35C61440 loc_35C61440:                           ; CODE XREF: CDXBaseSurface::GetAppData(ulong *)+14
.text:35C61440         mov     eax, [ebp+this]
.text:35C61443         mov     eax, [eax+24h]
.text:35C61446         mov     [esi], eax              ; ESI is attacker controlled

The next code snippet is taken from v2.0.50727\mscorwks.dll in Windows folder. IsBadReadPtr is used to test the pointer that is passed to MultiByteToWideChar.

.text:7A0D17FB         push    eax                     ; ucb
.text:7A0D17FC         push    ebx                     ; attacker supplies pointer was invalid before
.text:7A0D17FD         call    ds:__imp__IsBadReadPtr@8 ; and now it's valid because he's filled memory up
.text:7A0D1803         test    eax, eax
.text:7A0D1805         jz      short loc_7A0D17A7      ; jump is taken
[...]
.text:7A0D17A7         cmp     edi, esi
.text:7A0D17A9         jle     short loc_7A0D17BF
.text:7A0D17AB         push    esi                     ; cchWideChar
.text:7A0D17AC         push    esi                     ; lpWideCharStr
.text:7A0D17AD         push    edi                     ; cbMultiByte
.text:7A0D17AE         push    ebx                     ; lpMultiByteStr - attacker's data
.text:7A0D17AF         push    1                       ; dwFlags
.text:7A0D17B1         push    esi                     ; CodePage
.text:7A0D17B2         call    ?WszMultiByteToWideChar@@YGHIKPBDHPAGH@Z ; WszMultiByteToWideChar(uint,ulong,char const *,int,ushort *,int)

I was collecting files with IsBad*Ptr in them, and have found plenty others including but not exclusively MSCOMCTL.OCX, EXCEL.EXE, Lenovo's, Corel's, Nokia's, AVerMedia's products...

UPDATE 13/May/2014 To add IsBad*Ptr to the program doesn't automatically mean to create bugs. However if IsBad*Ptr is present we have reason to believe that the function is expecting a pointer that might be invalid in certain circumstances. In that case IsBad*Ptr may be used to attack the program. And that's why it's important to conduct the audit according to this.

UPDATE 16/May/2014 The term "valid pointer" had an ambiguous meaning in the part Steps of Attacking. This is now reworded. Reference.

[1] IsBad*Ptr functions are IsBadReadPtr, IsBadCodePtr, IsBadWritePtr, and IsBadStringPtr. All of them are exports of kernel32.dll.

April 28, 2014

Order of Memory Reads of Intel's String Instructions

Neither the Intel Manual nor Kip R. Irvine's assembly book discusses the behavior I'm describing about x86 string instructions in this post.

Given the following instruction that compares the byte at ESI to the byte at EDI.

cmps    byte ptr [esi],byte ptr es:[edi]

To perform comparison the instruction must read the bytes first. The question is whether byte at ESI or byte at EDI is read first?

Intel Manual says:
Compares the byte, word, doubleword, or quadword specified with the first source operand with the byte, word, doubleword, or quadword specified with the second source operand
Kip R. Irvine's book titled Assembly Language for Intel-Based Computers (5th edition) says:
The CMPSB, CMPSW, and CMPSD instructions each compare a memory operand pointed to by ESI to a memory operand pointed to by EDI
Both of the descriptions explain what the instructions do but none of them says how. So I needed to do some experiments in Windbg to find the answer to the question.

The first experiment was not a good one. Initially, I thought I'd put processor breakpoint (aka memory breakpoint) at ESI and another one at EDI. I also thought to execute CMPS and let the debugger to break-in on either of the processor breakpoints. And here it goes why it was a bad idea. The execution of CMPS has to complete for debugger break-in. And, by the time the CMPS completes it hits both of the breakpoints.

The other experiment I came up with is like this. Set both ESI and EDI to point two distinct memory addresses that are unmapped. The assumption is when CMPS is executed it raises an exception when trying to read memory. By looking at the exception record we can tell the address the instruction tries to read from. Given that, we can tell if that value was assigned to ESI, or to EDI, and so we can tell whether byte at ESI or byte at EDI is read first.

Here is how I did the experiment in Windbg.

I opened an executable test file in Windbg. I assembled CMPS to be placed in the memory at EIP.

0:000> a
011113be cmpsb
cmpsb

I changed ESI to point to invalid memory. And I did the same with EDI.

0:000> resi=51515151
0:000> redi=d1d1d1d1

Here is the disassembly of CMPS. Also, you know from the highlighted text that both ESI and EDI point to unmapped memory addresses.

0:000> r
eax=cccccccc ebx=7efde000 ecx=00000000 edx=00000001 esi=51515151 edi=d1d1d1d1
eip=011113be esp=0022fb70 ebp=0022fc3c iopl=0         nv up ei pl nz na po nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000202
test!wmain+0x1e:
011113be a6              cmps    byte ptr [esi],byte ptr es:[edi] ds:002b:51515151=?? es:002b:d1d1d1d1=??

I executed the process leading to an expected access violation triggered by CMPS instruction.

0:000> g
(5468.1eec8): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=cccccccc ebx=7efde000 ecx=00000000 edx=00000001 esi=51515151 edi=d1d1d1d1
eip=011113be esp=0022fb70 ebp=0022fc3c iopl=0         nv up ei pl nz na po nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010202
test!wmain+0x1e:
011113be a6              cmps    byte ptr [esi],byte ptr es:[edi] ds:002b:51515151=?? es:002b:d1d1d1d1=?? 

I got the details of the exception like below.

0:000> .exr -1
ExceptionAddress: 011113be (test!wmain+0x0000001e)
   ExceptionCode: c0000005 (Access violation)
  ExceptionFlags: 00000000
NumberParameters: 2
   Parameter[0]: 00000000
   Parameter[1]: d1d1d1d1
Attempt to read from address d1d1d1d1

As you can see the access violation was occurred due to an attempt to read from d1d1d1d1 that is the value of EDI. Therefore, to answer the question at the beginning of the article, the byte at EDI is read first.

To give more to the fun, you may try how emulators handle this - that is the order of memory reads of CMPS instructions.

If you like this you may click to read more debugging posts.

UPDATE 28/April/2014 Of course I don't encourage people to rely on undocumented behavior when developing software. Stephen Canon says Intel may optimize the microcode for operands from time to time, and so we don't have a good reason to believe this behavior to be stable.

UPDATE 29/April/2014 Here is a Windows C++ program to test this behavior on your architecture: cmps-probe.cpp

  This blog is written and maintained by Attila Suszter. Read in Feed Reader. Advert is experimentally shown.