Brief history of self-modified code
Self-modified code has a wide range of uses:
1. It was very difficult to use SMC (self-protection code) to protect applications 10 to 20 years ago, even if it was used to put compiled code into the memory.
2. the emergence of 95/NT In The Middle Of 1990s confused programmers about how to protect applications in the new operating system. I don't know how to port the protection measures to this new version. it is no longer possible to freely access the memory, hardware, and general operating systems. All the skills learned previously had to be abandoned. At first people thought that SMC could not be written except for VxD, this was all due to the fact that documents were not keeping up with each other.
Then we find that we want to continue using SMC in our program, we can adopt the following two methods:
- Use writememoryprocess exported from kernell32
- Put the code in the stack and modify it
Windows Memory Organization
Creating SMS in wnidws is not as straightforward as I think. First, you must face some special methods, and then use the Guide Windows provides to you.
As you may know, wondows allocates 4 GB of virtual memory to the process. this memory address is used in windows. One is the CS segment register, and the other is the DS, SS, and ES registers. they use the same memory base address (equal to 0) and are also limited to 4 GB
Only one segment contains code and data, that is, the process stack. You can use near call or jump to control the local code on the stack, that is, you do not need to use the SS register to access the stack, in addition, the CS register value is not equal to DS, es, and SS registers,
MoV dest
CS: [SRC]
MoV dest
DS: [SRC]
And
MoV dest
SS: [SRC]
The command points to the same local address.
The data contained in the Memory Page, code and stack have different attributes. In fact, the code page allows reading and execution. The data page allows reading and writing, and the stack allows both reading, writing, and execution.
Although they are bound with some security attributes, we will continue later.
Use writeprocessmemory
The simplest way to change the bytes in a process is to use writememoryprocess (of course, when some security signs are not set)
The first thing for the process in the memory we want to modify is to open it with OpenProcess and set process_vm_operation and process_vm_write attributes.
Here are some simple examples of SMC. The inline assembly language needs to be used in C ++,
Example 1: Use writepreocessmemory to create SMC
Int writeme (void * ADDR, int WB)
{
Handle H = OpenProcess (process_vm_operation | process_vm_write,
True, getcurrentprocessid ());
Return writeprocessmemory (H, ADDR, & WB, 1, null );
}
Int main (INT argc, char * argv [])
{
_ ASM {
Push 0x74; JMP --> JZ
Push offset here
Call writeme
Add ESP, 8
Here: JMP short here
}
Printf ("Holy sh ^ & osix, it worked! # JMP short $-2 was changed to JZ $-2n ");
Return 0;
}
As you can see, the program uses the JZ command to implement the jump. In this way, the program can continue, and the program tells us that jump is successfully used.
However, writememoryprocess has some disadvantages. First, it will be discovered by experienced Decryptors in the entry table. It is likely to set a breakpoint from this call, then, find the desired code in one step. writeprocessmemory is generally used by the compiler to compile the memory or to unpack executable files, but it must be ensured that the Code is not used by the decrypted.
Another disadvantage of writememoryprocess is that it cannot create new pages in memory. It can only work on existing pages.
Place the code on the stack and execute
It is not impossible to execute code on the stack, but will this cause thread security problems? If a patch is installed that does not allow code to be executed on the stack, the programmer will be unskillful. but in turn, the possible result is that most of your programs will not run, and Linux has such a patch.
Do you remember the shortcomings of writememoryprocess mentioned above? Okay, there are two reasons for using the stack to execute code. One is that an attacker cannot modify the code by sending instructions on unknown memory blocks, and he has to analyze and protect the code, this is hard to succeed. the second reason is that the executable code on the stack actually exists at any time. The application can allocate enough memory to the stack and release it when it is not needed. Generally, the system allocates 1 MB of memory to the stack. If the allocated memory is insufficient, you can modify it in the configuration file of the program.
Why is code relocation poor?
You must understand Windows 9x, Windows NT, and Windows 2000. stack location is different. in order for your program to be available in other machines after it is transplanted, you must relocate it to meet simple rules.
Fortunately, all short jumps in 8086 are associated with near calls. that is to say, it does not use linear addresses, but it is different between the next instruction and the target address. This makes code relocation easier, but it also has some constraints.
For example, what will happen to the following functions?
Void osixdemo ()
{
Printf ("Hi from osixn ");
}
The function is copied to the stack. Is the control passed to it? Because the address of printf has been changed, this may cause errors.
In the Assembly, we can use registers to easily lock it, and it is very easy to call the printf function for relocation, for example:
Lea eax,
Printfncall eax.
It is an absolute linear address, not an associated one. It is placed in the eax register. Now, no matter where it is called, the control can be passed to printf.
To do these things, your compiler must support linear assembly,
Example 2: how to copy the code to the stack and execute it
Void demo (INT (* _ printf) (const char *,...))
{
_ Printf ("Hello, osix! N ");
Return;
}
Int main (INT argc, char * argv [])
{
Char buff [1000];
INT (* _ printf) (const char *,...);
INT (* _ main) (INT, char **);
Void (* _ demo) (INT (*) (const char *,...));
_ Printf = printf;
// Add two values.
_ Main = Main;
_ Demo = demo;
//
Int func_len = (unsigned INT) _ main-(unsigned INT) _ demo;
For (int A = 0; A <func_len; A ++)
Buff [a] = (char *) _ demo) [a];
_ Demo = (void (*) (INT (*) (const char *,...) & buff [0];
_ Demo (_ printf );
Return 0;
}
If someone tells you that the advanced language cannot execute code on the stack, do not trust it.
Some Optimizations
First, you need to consider which compiler to use. If you want to use SMC or execute code on the stack, you must study the usage wizard of the compiler. many people fail for the first time because the compiler is not "optimized ".
How can this happen? In pure high-level languages, such as C and Pascal, Code that has been condemned to be unable to copy functions to stacks and other places can be obtained by a programmer as a pointer to a function, but without its standards, our programmers call it "magic number", because only the compiler knows.
Fortunately, almost all compilers use the same logic to generate code, so that the program can assume the compiled code, and the programmer can also assume.
Let's go back and look at the routine 2. assume that the pointer to the function is the same as the first address of the function, and the subject is behind the first address. Most compilers use this "common sense compiling ", many large compilers use this rule (VC ++, Borland, and so on ). so if you don't use some unknown compilers, don't worry about this.
Note VC ++. In debug mode, the compiler inserts an "adapter" and assigns the function to other places to condemn ms, but don't worry, you only need to select "link incrementally" in the compiler options. if your compiler does not have such an option, or something similar, you either do not use SMC or use another compiler.
Deciding the function length is another problem, but this requires skill. in C ++, the sizeof structure does not return the length of the function itself, but the size of the pointer to the function. but according to the rule: the compiler allocates memory according to the order in which the source code appears. therefore, the length of the function body is equal to the distance between the pointer pointing to the function and the pointer pointing to the function. easy!
There is another thing to do to optimize the compiler: Delete the variables they think are no longer in use. returns to see the routine 2. some things are written to the buff cache. but without reading anything from that place, most compilers cannot be passed to the cache for control. so they deleted the code being copied. this is why the control is passed to the cache that has not been initially implemented. the program crashes. If this problem occurs, cancel the "Global Optimization" option.
If your program is still not working, don't give up because the compiler may insert regular calls at the end of each function to monitor the stack. microshoft 'vc ++ does this. it places the _ chkesp call in the debug project. don't try to find it in the document. I don't think about it. This call is related and cannot be blocked. However, in the release version, VC ++ does not check the stack status. therefore, this problem will not occur.
Use SMC in your apps
Now you can ask yourself (or ask me) "What are the advantages of code execution on the stack? "The answer is: the code of the function on the stack can be changed smartly.
Even the stupid encryption code can embarrass the decryption experts. Of course, it will be easier to debug, but it is still difficult.
This simple encryption algorithm will process every line of code in an inconsistent or continuous manner, and re-execution will generate the target code we need.
Here is an example to read the content of our demo function, encrypt it, and write the result to a file.
Example 3: How to encrypt the demo Function
Void _ Bild ()
{
File * F;
Char buff [1000];
Void (* _ demo) (INT (*) (const char *,...));
Void (* _ Bild )();
_ Demo = demo;
_ Bild = _ Bild;
Int func_len = (unsigned INT) _ BILD-(unsigned INT) _ demo;
F = fopen ("demo32.bin", "WB ");
For (int A = 0; A <func_len; A ++)
Fputc (INT) buff [a]) ^ 0x77, F );
Fclose (f );
}
After the encryption is complete, the content is put into the string. Now the demo function can be removed from the initialization code. Then, when we need it, it can be encrypted, it can be copied to the local cache and called.
Here is an example of how we can implement it:
Example 4: encryption program
Int main (INT argc, char * argv [])
{
Char buff [1000];
INT (* _ printf) (const char *,...);
Void (* _ demo) (INT (*) (const char *,...));
Char code [] = "x22xfcx9bxf4x9bx67xb1x32x87
X3fxb1x32x86x12xb1x32x85x1bxb1
X32x84x1bxb1x32x83x18xb1x32x82
X5bxb1x32x81x57xb1x32x80x20xb1
X32x8fx18xb1x32x8ex05xb1x32x8d
X1bxb1x32x8cx13xb1x32x8bx56xb1
X32x8ax7dxb1x32x89x77xfax32x87
X27x88x22x7fxf4xb3x73xfcx92x2a
Xb4 ";
_ Printf = printf;
Int code_size = strlen (& code [0]);
Strcpy (& buff [0], & code [0]);
For (int A = 0; A <code_size; A ++)
Buff [a] = buff [a] ^ 0x77;
_ Demo = (void (*) (INT (*) (const char *,...) & buff [0];
_ Demo (_ printf );
Return 0;
}
Note that the printf function shows a congratulation. At first glance, you may not be able to notice what is useless, but if you find the string "Hello, osix! "Location, it should not be in the code segment (Borland put it for a reason)-So check the data segment and you will find it should be there.
Now, even if attackers view the source code, they will be confused. I use this method to hide all the information (a string of numbers, the generator of my program, and so on ).
If you want to use this method to check the serial number, the method should be organized so that it can be used during decompression. I will talk about this in the next example.
Remember, when implementing SMC, you need to know the exact location of the byte you want to change. Therefore, you need to replace the advanced language with assembly.
Note that the mov Command needs to change the exact byte by passing an absolute linear address. we can find this information during the program running. call $ + 5pop regmov [Reg + relative_address]. The XX status has been obtained. now that you can work, insert the following statement, execute the call command, and the return address (or the absolute address of the command) pops up from the stack ). this is used as the base address of the stack function code.
The following is an example of code sequence:
Example 5: generate a serial number and run it on the stack.
Myfunc:
Push ESI; saving the ESI register on the stack
MoV ESI, [esp + 8]; ESI = & username [0]
Push EBX; saving other registers on the stack
Push ECx
Push edX
XOR eax, eax; zeroing working registers
XOR edX, EDX
Repeatstring:; byte-by-byte string-processing loop
Lodsb; reading the next byte into Al
Test Al, Al; has the end of the string been reached?
JZ short exit
; The value of the counter that processes 1 byte of the string
; Must be choosen so that all bits are intermixed, but parity
; (Oddness) is provided for the result of transformations
; Saved med by the XOR operation.
MoV ECx, 21 h
Repeatchar:
XOR edX, eax; repeatedly replacing XOR with ADC
Ror eax, 3
Rol edX, 5
Call $ + 5; EBX = EIP
Pop EBX ;/
XOR byte PTR [ebx-0Dh], 26 h;
; This instruction provides for the loop.
; The XOR instruction is replaced with ADC.
Loop repeatchar
JMP short repeatstring
Exit:
Xchg eax, EDX; the result of work (Ser. Num) in eax
Pop edX; restoring the registers
Pop ECx
Pop EBX
Pop ESI
Retn; returning from the function
This algorithm is a bit weird --
Because a function is constantly called and passing the same parameters to it may produce or be the same, or completely different results! This is the length of the user name. When the function ends, if it is odd, XOR is replaced by ADC, and if it is an even number, nothing will happen.
Okay, it's all about it. I hope you know at least one thing. It took me two hours to get back.
Alas! It took me three hours to translate it !!!