Copy Link
Add to Bookmark
Report

SLAM4.025: Finding the DOS kernel entry point by Trigger/SLAM

eZine's profile picture
Published in 
Slam
 · 3 Mar 2022

Finding the DOS kernel entry point
by Trigger [SLAM] '97

INTRODUCTION

A good virus should have structures to avoid detection by AV. It still is very easy to patch TBDRIVER in memory with the good old EB05->EB00 trick. I think it is fair to say that this wasn't a stupidity-on-purpose (so Frans could catch a whole bunch of viruses by checking for the patching), 'cause I think it already works too long. Frans really *is* stoopid. And of course let's not forget good old VSAFE, which stands a good chance for winning the "supremacy in stupidity"-award.
Well, at least *they* have the brains to disable the calls. :)

A by far better way to avoid detection by resident monitors is to find the DOS kernel entry point done by eg. tunnelling. (For extensive coverage on other forms of tunnelling I recommend the excellent tutorials by Methyl.) The advantage of having the kernel entry point is that you can call the kernel *directly* so you avoid being checked by ANY resident monitor that checks DOS API calls, the so called behaviour blockers.


HOW TO FIND THE ENTRY POINT

So the question is, how do we find this entry point. There are a lot of possible ways; here I will present you a new one.

Let's start with some DOS basics. I believe everyone with a mind in his head lets DOS try to load itself (i.e. the kernel / API) "high" by using the DOS=HIGH option in CONFIG.SYS. I believe even MEMMAKER makes sure it's included. Now what this option actually does is use the (mostly unused) HMA area (which ranges from FFFF:0010 to FFFF:FFFF) to load the kernel in, which saves some conventional memory. This HMA area is normally not accessible because it's not within the 1 MB memory limit (it's actually eXtended Memory which can be accessed in real mode). As you have seen, it CAN be addressed (FFFF:0010 up to FFFF:FFFF) so the computer people invented a thing called the A20 line. I won't tell you what it exactly is, you can read it in the XMS 3.0 specification (FTPsearch for XMS30.TXT), but it isn't really important. The only thing you really need to know about it is that you have to enable it to get access to the HMA. Of course, DOS knows this too, so it makes sure that this A20 line is enabled before it makes a call to the kernel in HMA. You can check this on your own computer. Set a BPINT 21, and trace into it. (From now on I'm assuming you have DOS=HIGH configured!) At one point you will get to some code which should look like this:

 00CB:0FB2  90          NOP 
00CB:0FB3 90 NOP
00CB:0FB4 E8CE00 CALL 1085
00CB:0FB7 2EFF2E820F JMP FAR CS:[0F82]


Of course, many of you guys already recognize this: the famous double NOP construct. What it does: it CALLs a function that makes sure the A20 line is enabled, so that the HMA is accessible. Then it performs a far jump to the kernel located in HMA, which should look something like:

 FF3B:41E9  FA          CLI 
FF3B:41EA 80FC?? CMP AH,?? ;6C for DOS6-, 73 for DOS7+
FF3B:41ED 77D2 JA 41C1
FF3B:41EF 80FC33 CMP AH,33
FF3B:41F2 7218 JB 420C
FF3B:41F4 74A2 JZ 4198
(...)


So these two structures are interesting. Most tunnellers "tunnel" up to the double NOP construct, which is clever, because they don't have to keep that A20 line in mind anymore. The second structure (the actual kernel entry point) is also interesting. Why? Assuming DOS=HIGH, it's ALWAYS in the HMA! And the beginning of the DOS kernel entry point has been the same for ages (well, ages... :)). So we could search for this piece of code in the HMA!

What will we search for? The string FA80FC??77 seems pretty stable: It includes the CLI, it includes the CMP AH without the compare value (so we trap all DOS versions (at least up to 7), and it includes the conditional jump without the relative offset (for compatibility). Where do we search? In HMA! So set a segment register to FFFF (-1), and start looking! Found it?


BUT WAIT!

Now, we have the kernel at, say, FFFF:35A9. Save the location, set a bogus function to test it (say AH=2/DL=7, which will produce a short beep). Call the location. Does it beep? NO!! Well maybe it does, but it won't stop! Big Crash (tm)!! What went wrong?!

Let's first discuss again some basics: I hope you all know that due to the silly 20bit addressing mode, the physical offset of a memory location is calculated with:

Physical Offset = (Segment*10h) + Offset

Let's take the location 1234:5678 as an example. To calculate the physical offset, we multiply the segment by 10h, and add the offset, which gives us:

(1234 * 10) + 5678 = 12340 + 5678 = 179B8 as a physical offset.

Now the nice thing about this, is that if we add ONE to CS, and sub 10 from IP, we get the EXACT same physical offset:

(1235 * 10) + 5668 = 12350 + 5658 = 179B8 !

NOTE: Exactly the same goes when we SUB one from CS, and ADD 10 to IP, it stays the same physical offset!


SO WHAT?!?

Let's get back to the program. What does this have to do with our crashing? Behold.. The attentive reader may have noticed that (on my computer) when calling the DOS kernel through Int21, we didn't end up in the FFFF segment, but in the FF3B segment! Something fishy going on here? No, not really.. :)

We're just overlooking a big problem. We just saw that two exact same memory locations can be addressed using different segment:offset pairs. (In our example we saw that 1234:5678 gives the same memory location as 1233:5688, and 1235:5668 gives also the same location.)

So we could also call the kernel using different segment:offset pairs, 'cause either way, we're addressing the same location, right? Wrong, we just tried it, and the computer crashed. Why? Watch this:

FF3B:4217 2E8E1E273F MOV DS, CS:[3527] 

FFFF:35D7 2E8E1E273F MOV DS, CS:[3527]


This is one instruction inside the DOS kernel, addressed using different segment:offset pairs, but they're both addressing EXACTLY the same physical offset. (I hope my digression was clear enough for you to understand WHY :)) Now this is the moment you should see something weird: the first instruction loads DS with [FF3B:3527], and the second loads DS with [FFFF:3527] which are two DIFFERENT locations! So what does this mean?

You can't just call routines (like a kernel) using different segment:offset pairs, because of hardcoded memory locations! (in this example the 3527 was the hardcoded location). So in spite of the nice CS+1->IP+10 trick, we can't just assume the "official" kernel entry location is in segment FFFF. (Note that they ARE the same physical offsets, so FF3B is in HMA too!) On my computer the "official" location is in segment FF3B, but there's no need for that. So we'll have to find out in another way.


UHM.. HOW?

The ideal solution would be to call Int21, and as soon as it RETurns, check where 'it' came from, which would be the correct kernel segment. But, unfortunately, the x86 doesn't have some kind of history table to check where 'it' has been, so we can forget that.

Another solution, which does work is using the CALL instruction: when executing a call, the CPU pushes CS if it's a far call, then it pushes the IP of the instruction to be executed after the call, and then jumps to the call. So what would happen if we would PATCH the first few bytes of the DOS kernel with a FAR call to our code?! When executed, it would PUSH THE CS:IP OF THE KERNEL, and jump to our code!! Yes, read that again, it jumps to our code with the correct segment of the kernel on the stack, ready for us to read!

In short, Int21 is called, the Enable_A20_line call is made, it jumps to the kernel in HMA, which is overwritten by our far CALL, which gets executed. It would jump to our code, with the correct kernel segment on the stack. What would our code look like? Something like this:

       call_by_patched_kernel: 
pop ax
pop bx
mov cs:[kernelIP],ax
mov cs:[kernelCS],bx
(...)


Whoomp, there it is! We have the "official" segment:offset pair of the kernel entry point. If we restore the original first bytes of the kernel, noone will notice the difference, and we have our kernel entry point!


A FEW SIDENOTES

A few practical problems discussed here:

  • Before you start scanning the HMA for the CLI construct, make sure the A20 line is enabled! How can we do this? Just call an honest Int21 function with the INT instruction, and make sure it's a call that no resident monitor will suspect. Something like GetTime or GetVersion..
  • Before patching the kernel, clear (CLI) all interrupts! We definitely DO NOT want the patched Int21 to be called without us knowing! Your system will crash! (STI after restoring the 5 bytes)
  • Note the "Patched_Int21_Calls_Here"-routine. We could push the CS and IP back on the stack, so control would be returned to the kernel, but this isn't advisable. We fucked the code up. If we do 3 POPs (Remember: INT xx = PUSHF, CALL [0:4*xx]) we can just continue our program. The 3 values we get are useless, so we can just use ADD SP,6.
  • After catching the PUSHed CS:IP, correct the IP. It then contains the IP of the instruction to be executed after the call. Since a far call eats 5 bytes (9A offs seg), just subtract it with 5.
  • As said, a normal INT is just a far call PRECEDED by a PUSHF! So calling the kernel entry point should include a PUSHF!
  • Don't forget to restore the first 5 bytes of the kernel. :)

IN SHORT

  1. Check for (or just assume) DOS=HIGH
  2. Make sure the A20 line is enabled (call i21 with the INT instruction)
  3. Scan the the HMA for: FA 80 FC 73/6C (6C=dos6-) 77 (D2)
  4. Clear interrupts (CLI)
  5. Replace the first 5 bytes with: 9A (far call) <offset> <seg>
  6. Call a bogus Int21 (with the INT instruction!)
  7. Return the original 5 first bytes of the kernel

COMPATIBILITY

Patching the kernel under Win95 is not really a clever thing to do. This is because every modification you make to the kernel in a DosBox is permanent. What? Yes, permanent. Instead of saving a fresh copy on startup and copying it in every new DosBox, it *ALWAYS* uses the same copy.

To illustrate: start Win95 and launch two DosBoxes after each other. Search (in HMA) for the copyright string in the kernel, and d(ump) it. You will see a string something like "DOS Version 7". Make a harmless change in one DosBox, e.g. change "DOS" into "D0S". Now dump the memory location again. It's changed. No miracle. Now jump to the other box, and dump the same memory location: "D0S Version 7". That's right, the modification is counting in every DosBox around. Now the danger in this is that when you or your virus modifies the kernel in one DosBox, other boxes will still blindly rely on it still properly functioning as DOS kernel.

Furthermore, the HMA is the ONLY structure of which the same copy gets used over and over again. So the MCB and UMB chain are properly cleared for each DosBox. If you would leave a far call in the kernel to your virus and you'd close the box and open a new one, the kernel would perform a call to your virus which WAS in some MCB. But in this new box, that chain is cleared, so your CPU will end up executing random code. Not Good...

In short, fiddling around with the kernel in Win95 is not a good idea unless you really know what you're doing. Workaround: call i2F/AH=1600, if it returns anything other than 1600 in AX (Version, Win95 will most probably return version 4), don't patch the kernel.

Almost the same thing goes for most multitasking environments. Most of these multitasking environments utilize DPMI (Dos Protected Mode Interface), which normally isn't used in plain DOS; it *can* be used, but it really isn't used that much, so most users won't have it loaded. There's a nice Int2F call to check for DPMI availability, namely Int2F/AX=1686h. It returns zero in AX if DPMI is enabled, otherwise AX is non-zero. If you want to be sure your virus is multitasking-environment-aware, check for DPMI and use another tunnelling technique instead. I know, this is not really a solution, but better be compatible than crashing someone's system.

Anyway, here's an example program. It first calls i21/AH=2 to enable the A20 line, then it does its work, after which it will call i21/AH=2 with DL=ASCII 251 (˚), but this time by calling the fetched kernel entry point.

(Note: if you're not running SoftIce, DL will not be clear when the first i21/AH=2 call is made, so you'll get a sort of random character display)

Model Tiny 
Radix 16
.286

code segment
assume cs:code, ds:code
org 100h

main:
mov ax,1686h ;DPMI detect
int 2fh
or ax,ax
jz cantfind ;quit if DPMI

mov ax,1600h ;windows check
int 2fh
cmp ax,1600h
jnz cantfind ;quit if under windows

mov ah,2
int 21h ;bogus call to enable A20 line
cld ;always include this...
push -1 ;seg FFFF
pop ds
xor di,di
loopje:
inc di
cmp di,0ff00h ;near end of segment?
ja cantfind
cmp [di],80fah
jnz loopje
cmp byte ptr [di+2],0fch
jnz loopje
cmp byte ptr [di+4],77h
jnz loopje
mov bl,byte ptr [di+3] ;save 6C or 73 in BL
push ds
pop es
cli ;disable interrupts
mov al,9ah ;]
stosb ;]
mov ax, offset get21handler ;] patch kernel with our call
stosw ;]
mov ax,cs ;]
stosw ;]
int 21h ;get kernel entry CS:IP in ES:DI
get21handler:
pop di ;kernel entry IP
pop es ;kernel entry CS
sub di,5 ;correct the IP to the kernel ENTRY point
add sp,+6 ;add out the flags, CS and IP pushed by our INT.
push cs
pop ds
mov [KernelIP],di ;]save "official" CS:IP pair of kernel
mov [KernelCS],es ;]
mov ax,80fah
stosw
mov al,0fch
stosb
xchg ax,bx
mov ah,77
stosw
sti ;and the road is clear...

mov ah,2
mov dl,"˚"
pushf ;important!
db 9ah ;far call
KernelIP dw ?
KernelCS dw ?

cantfind:
int 20h

code ends
end main

← previous
next →
loading
sending ...
New to Neperos ? Sign Up for free
download Neperos App from Google Play
install Neperos as PWA

Let's discover also

Recent Articles

Recent Comments

Neperos cookies
This website uses cookies to store your preferences and improve the service. Cookies authorization will allow me and / or my partners to process personal data such as browsing behaviour.

By pressing OK you agree to the Terms of Service and acknowledge the Privacy Policy

By pressing REJECT you will be able to continue to use Neperos (like read articles or write comments) but some important cookies will not be set. This may affect certain features and functions of the platform.
OK
REJECT