Dll in binary

Дата публикации 4 дек 2004

Dll in binary — Архив WASM.RU

It was shown in the article «Win32 Appy by Hand» how to «assemble» the simplest Win32 application with the only MessageBox using debug.exe. This time let’s create the simplest dll in a similar manner; this is the continuation and further elaboration of the previous work and the necessary foundation for my future articles, because I am going to describe how to create COM components in binary and it is quite impossible to manage without dll’s there.

I hope that a reader has thoroughly studied the last topic and now can easily create PE headers, import tables and code and data sections on the fly :smile3: It is supposed, too, that because of Svin’s work followed by epidemic enthusiasm with Intel’s manuals everybody will be able to understand hex’es or even binary codes. So we will concentrate on more useful things – namely, what is the difference between dll’s and usual exe-files. There are two main points here: first, exported functions appear hence export tables, too, and second, we will have to investigate relocations because it is not guaranteed that our dll will be loaded to the preferred base address of 10000000h :(

This time let’s create 4 sections in our PE-file: for code, data, import with export and relocations. We will name these sections .code, .data, .rdata and .reloc respectively. The sections will be located in memory at offsets 1000h, 2000h, 3000h and 4000h, and in a file at offsets 200h, 400h, 600h and 800h correspondingly. The content will be the same as in the last article, i.e. we will export the only function and the only work it does will be dispalying of MessageBox. Thus there are only two strings of data; create the file data.txt:

Код (Text):
  1. n data.bin
  2. r cx
  3. 200
  4. f 0 l 200 0
  5. e 0 "Dll"
  6. e 10 "Exported function "
  7. m 0 l 200 100
  8. w
  9. q

I hope it is clear that these are commands for debug.exe written in the file. This time we decided to automate the work somewhat :smile3:

Using debug.exe is not mandatory though. If you wish you can use any hex-editor e.g. HIEW or QView or something else. Then you can build dll as a single file not divided into separate auxiliary files as in our case. In any case the .data section must look like this in memory:

Код (Text):
  1. 2000: 44 6C 6C 00 00 00 00 00 00 00 00 00 00 00 00 00 | Dll.............
  2. 2010: 45 78 70 6F 72 74 65 64 20 66 75 6E 63 74 69 6F | Exported functio
  3. 2020: 6E 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | n...............

So the title string is located in the beginning of the data section at offset 2000h and the memory address will be at offset 10002000h correspondingly. The values for the message string will be 2010h and 10002010h accordingly. Do not forget that the .data section begins in the dll file at offset 400h.

And now the code! We will have to import MessageBoxA from User32.dll again; this time IAT will be located in its own section – .rdata – in the very beginning as usual, i.e. at offset 3000h; the memory address will be 10003000h. There are no other imported functions there. The .code section will look like this:

Код (Text):
  1. 1000: 6A 00 68 00 20 00 10 68 10 20 00 10 6A 00 FF 15
  2. 1010: 00 30 00 10 C3 00 00 00 00 00 00 00 00 00 00 00

To use debug.exe fill in the file code.txt:

Код (Text):
  1. n code.bin
  2. r cx
  3. 200
  4. f 0 l 200 0
  5. a 0
  6. ; parameters of MessageBox
  7. db 6a 0
  8. db 68  0 20 0 10
  9. db 68 10 20 0 10
  10. db 6a 0
  11. ; call MessageBox
  12. db ff 15 0 30 0 10
  13. ; return
  14. db c3
  15.  
  16. m 0 l 200 100
  17. w
  18. q

Do not remove the empty string after ‘db c3’! Otherwise debug will complain and you will get nonsense instead of a cool binary block :smile3:

So we did it – the half of the work is already done! It’s time to proceed to the new subject now. Let’s look at our code and mark unsafe places:

Код (Text):
  1. 1000: 6a 00 68 <B>00</B> | <B>20 00 10</B> 68 | <B>10 20 00 10</B> | 6a 00 ff 15
  2. 1010: <B>00 30 00 10</B> | c3

I marked out in bold the memory addresses that are parts of instructions. The system easily can load our dll in a quite different location – then we will get our addresses broken and the code will push garbage in the stack and send it to unknown place instead of our imported function. It is the three 32-bit values that must be fixed up; we must specify them in fix-up table for that purpose.

Each fixup is represented with only two bytes (16-bit values), the 4 high bits being the fixup type. The fixup type nearly always is 3 for Win32, meaning the system must fix up the 32-bit address at the given offset. The offset is represented with the remaining 12 bits of the fixup. As you can see this is enough for offsets within the single memory page (4 Kb) only. So the fixups for the given memory page are combined in fixup block. The first 4 bytes at the beginning of the fixup block contain the offset of the memory page relative to the base load address and the next 4 bytes contain the size of the fixup block (including the first 8 bytes). The remaining of the fixup block is a collection of fixups for the memory page (see picture).

When loading a so called delta is calculated as the difference between the preferred base address (indicated in the PE-header), and the base where the image is actually loaded. If the dll is loaded at its preferred base, the delta would be 0, and thus the fixups would not have to be applied. Otherwise the delta is added to each 32-bit value that has the corresponding fixup. And that’s all.

In our case there must be 3 fixups; their offsets are 3, 8 and 10h relative to the page beginning. Including the fixup type (3) we will get values 3003h, 3008h, and 3010h. A fixup block must be aligned on DWORD boundary, so we add one more «empty» fixup at the end (it has even its own type – of course, 0). Finally we get: page RVA is the offset of the relocated page, that is, the code page – 1000h; the block size is 8 bytes (block header) + 3 fixups, 2 bytes each + 1 «empty» fuxup (2 bytes) – 10h in sum. The relocation section is ready! It looks like this:

Код (Text):
  1. 4000: 00 10 00 00 10 00 00 00 03 30 08 30 10 30 00 00

Fill in the file reloc.txt:

Код (Text):
  1. n reloc.bin
  2. r cx
  3. 200
  4. f 0 l 200 0
  5. a 0
  6. ; Page RVA
  7. dw 1000 0
  8. ; Block size
  9. dw 10 0
  10. ; fixups
  11. dw 3003
  12. dw 3008
  13. dw 3010
  14.  
  15. m 0 l 200 100
  16. w
  17. q

Now only export remains. Since my readers are great specialists on import no word will be said about it more :smile3: If anybody does not understand how to deal with import refer to my previous article.

The main table combining all the other information is the export directory table. It has the following fields:

Offset Size Description
0 4 Reserved (0)
4 4 Time and date the export data was created.
8 2 Major version number.
0Ah 2 Minor version number.
0Ch 4 Offset (RVA) of the ASCII string containing the name of the DLL.
10h 4 Starting ordinal number for exports.
14h 4 Number of entries in the export address table.
18h 4 Number of entries in the name pointer table.
1Ch 4 Offset (RVA) of the export address table.
20h 4 Offset (RVA) of the export name pointer table.
24h 4 Offset (RVA) of the ordinal table.

See the following picture for interrelation of various tables:

As you can see export data contains mainly various offsets and indices. When exporting using ordinals only we need only two tables: the main export directory table and the export address table. The latter is a simple array of 32-bit offsets of the exported symbols, that is the implementations of the functions we export relative to the base load address. Thus each exported function has its own index to this array; adding the ordinal base to that index we get the function’s ordinal.

If functions are exported by names, there must be three additional tables: the ordinal table, the export name pointer table and the export name table. It is clear with the export name table: it contains the names to be exported. The number of names can be less than the number of entries in the export address table. The export name pointer table contains offsets (RVAs) into the export name table. In fact the export name table is simply a list of strings containing names for exported functions; and the export name pointer table contains offsets to those strings. But the pointers to the strings must be ordered lexically to allow binary searches between function names. Note: it is the name pointers that must be ordered not the names (that is, strings) itself. The export ordinal table is another array, but it contains 16-bit (not 32-bit!) indices into the export address table. The ordinal table and the name pointer table are joint in that the index of each of these arrays corresponds to the same function. So if we have a function name (a string in the name table) and a pointer to that name in the name pointer table, the location of that pointer in the name pointer table is determined by the lexical order of the name. And we will find the index into the export address table at the same relative position (that is with the same index as in the name pointer table) in the ordinal table. After we get index into the export address table we can get the function address itself in the address table.

Well, it is time to proceed to the last section – ‘.rdata’. First we will try on a layout (see picture)

Here is the .rdata section itself:

Код (Text):
  1. 3000: 20 30 00 00 00 00 00 00 20 30 00 00 00 00 00 00    0...... 0......
  2. 3010: 55 73 65 72 33 32 2E 64 6C 6C 00 00 00 00 00 00   User32.dll......
  3. 3020: 00 00 4D 65 73 73 61 67 65 42 6F 78 41 00 00 00   ..MessageBoxA...
  4. 3030: 08 30 00 00 00 00 00 00 00 00 00 00 10 30 00 00   .0...........0..
  5. 3040: 00 30 00 00 00 00 00 00 00 00 00 00 00 00 00 00   .0..............
  6. 3050: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00   ................
  7. 3060: 00 00 00 00 00 00 00 00 00 00 00 00 94 30 00 00   .............0..
  8. 3070: 01 00 00 00 01 00 00 00 01 00 00 00 88 30 00 00   .............0..
  9. 3080: 90 30 00 00 8C 30 00 00 00 10 00 00 00 00 00 00   .0...0..........
  10. 3090: A0 30 00 00 44 6C 6C 2E 64 6C 6C 00 00 00 00 00   .0..Dll.dll.....
  11. 30A0: 46 75 6E 63 74 69 6F 6E 31 00 00 00 00 00 00 00   Function1.......

And now the file rdata.txt:

Код (Text):
  1. n rdata.bin
  2. r cx
  3. 200
  4. f 0 l 200 0
  5. a 0
  6. ; Import
  7. ; IAT
  8. dw 3020 0 0 0
  9. ; Lookup table
  10. dw 3020 0 0 0
  11. ; Dll name
  12. db "User32.dll" 0
  13.  
  14. a 20
  15. ; Imported function with hint
  16. db 0 0 "MessageBoxA" 0
  17.  
  18. a 30
  19. ; Import directory table:
  20. ; lookup table RVA
  21. dw 3008 0
  22. ; 2 empty fields
  23. dw 0 0 0 0
  24. ; Dll name RVA
  25. dw 3010 0
  26. ; RVA of IAT
  27. dw 3000 0
  28.  
  29. a 60
  30. ; Export
  31. ; Export directory table:
  32. ; 3 empty fields
  33. dw 0 0 0 0 0 0
  34. ; Dll name RVA
  35. dw 3094 0
  36. ; Ordinal base
  37. dw 1 0
  38. ; Address table entries
  39. dw 1 0
  40. ; Number of name pointers
  41. dw 1 0
  42. ; Export address table RVA
  43. dw 3088 0
  44. ; Name pointer RVA
  45. dw 3090 0
  46. ; Ordinal table RVA
  47. dw 308C 0
  48. ; Export address table
  49. dw 1000 0
  50. ; Ordinal table
  51. dw 0 0
  52. ; Name pointer table
  53. dw 30a0 0
  54. ; Dll name
  55. db "Dll.dll" 0
  56.  
  57. a a0
  58. ; Exported function name
  59. db "Function1" 0
  60.  
  61. m 0 l 200 100
  62. w
  63. q

Do not forget to keep the empty strings! Now we will have to only slightly modify our PE-header (compared to our first article). Here it is in full:

Код (Text):
  1. 0000: 4D 5A 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  2. 0010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  3. 0020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  4. 0030: 00 00 00 00 00 00 00 00 00 00 00 00 40 00 00 00
  5. 0040: 50 45 00 00 4C 01 04 00 00 00 00 00 00 00 00 00
  6. 0050: 00 00 00 00 E0 00 0E 21 0B 01 00 00 00 00 00 00
  7. 0060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  8. 0070: 00 00 00 00 00 00 00 10 00 10 00 00 00 02 00 00
  9. 0080: 04 00 00 00 00 00 00 00 04 00 00 00 00 00 00 00
  10. 0090: 00 50 00 00 00 02 00 00 00 00 00 00 02 00 00 00
  11. 00A0: 00 00 10 00 00 10 00 00 00 00 10 00 00 10 00 00
  12. 00B0: 00 00 00 00 10 00 00 00 60 30 00 00 4A 00 00 00
  13. 00C0: 30 30 00 00 28 00 00 00 00 00 00 00 00 00 00 00
  14. 00D0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  15. 00E0: 00 40 00 00 10 00 00 00 00 00 00 00 00 00 00 00
  16. 00F0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  17. 0100: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  18. 0110: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  19. 0120: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  20. 0130: 00 00 00 00 00 00 00 00 2E 63 6F 64 65 00 00 00
  21. 0140: 00 02 00 00 00 10 00 00 00 02 00 00 00 02 00 00
  22. 0150: 00 00 00 00 00 00 00 00 00 00 00 00 20 00 00 60
  23. 0160: 2E 64 61 74 61 00 00 00 00 02 00 00 00 20 00 00
  24. 0170: 00 02 00 00 00 04 00 00 00 00 00 00 00 00 00 00
  25. 0180: 00 00 00 00 40 00 00 C0 2E 72 64 61 74 61 00 00
  26. 0190: 00 02 00 00 00 30 00 00 00 02 00 00 00 06 00 00
  27. 01A0: 00 00 00 00 00 00 00 00 00 00 00 00 40 00 00 40
  28. 01B0: 2E 72 65 6C 6F 63 00 00 00 02 00 00 00 40 00 00
  29. 01C0: 00 02 00 00 00 08 00 00 00 00 00 00 00 00 00 00
  30. 01D0: 00 00 00 00 40 00 00 42 00 00 00 00 00 00 00 00
  31. 01E0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  32. 01F0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

And here is how to create it using debug.exe (file header.txt):

Код (Text):
  1. n Header.bin
  2. r cx
  3. 200
  4. f 0 l 200 0
  5. e 0 'MZ'
  6. e 3C 40
  7. e 40 'PE'
  8. e 44 4C 01
  9. a 46
  10. ; Number of sections
  11. db 04
  12.  
  13. a 54
  14. ; Size of the optional header
  15. db e0 00
  16. ; Flags: set dll flag and flag that permits to
  17. ; load image at address different from the preferred
  18. ; base address indicated in PE-header
  19. db 0E 21
  20. ; Magic number
  21. db 0B 01
  22.  
  23. a 74
  24. ; Image base
  25. db 00 00 00 10
  26. ; Section alignment
  27. db 00 10 00 00
  28. ; File alignment
  29. db 00 02 00 00
  30. ; Major Windows version
  31. db 04
  32.  
  33. a 88
  34. ; Major subsystem version
  35. db 04
  36.  
  37. a 90
  38. ; Size of image
  39. db 00 50 00 00
  40. ; Size of headers
  41. db 00 02
  42.  
  43. a 9C
  44. ; Subsystem
  45. db 02 00
  46.  
  47. a A0
  48. ; Size of stack to reserve
  49. db 00 00 10 00
  50. ; Size of stack to commit
  51. db 00 10 00 00
  52. ; Size of heap to reserve
  53. db 00 00 10 00
  54. ; Size of heap to commit
  55. db 00 10 00 00
  56.  
  57. a B4
  58. ; Number of data directory entries
  59. db 10 00 00 00
  60. ;
  61. ; Data directories:
  62. ; Export directory table RVA
  63. dw 3060 0
  64. ; Export directory table size
  65. dw 4a 0
  66. ; Import directory table RVA
  67. dw 3030 0
  68. ; Import directory table size
  69. dw 28 0
  70. ; skip 24 bytes (3 entries)
  71. dw 0 0 0 0 0 0 0 0 0 0 0 0
  72. ; Base relocation table RVA
  73. dw 4000 0
  74. ; Base relocation table size
  75. dw 10
  76.  
  77. a 138
  78. ; Start of section table
  79. ;
  80. ; name of the first section
  81. db '.code' 0 0 0
  82. ; size of the section in memory
  83. dw 200 0
  84. ; section RVA
  85. dw 1000 0
  86. ; size of the section on disk
  87. dw 200 0
  88. ; offset of section data in file
  89. dw 200 0
  90. ; skip 12 bytes
  91. dw 0 0 0 0 0 0
  92. ; first section flags
  93. db 20 00 00 60
  94. ; second section
  95. db '.data' 0 0 0
  96. dw 200 0
  97. dw 2000 0
  98. dw 200 0
  99. dw 400 0
  100. dw 0 0 0 0 0 0
  101. db 40 0 0 c0
  102. ; third section
  103. db '.rdata' 0 0
  104. dw 200 0
  105. dw 3000 0
  106. dw 200 0
  107. dw 600 0
  108. dw 0 0 0 0 0 0
  109. db 40 0 0 40
  110. ; fourth section
  111. db '.reloc' 0 0
  112. dw 200 0
  113. dw 4000 0
  114. dw 200 0
  115. dw 800 0
  116. dw 0 0 0 0 0 0
  117. db 40 0 0 42
  118.  
  119. m 0 l 200 100
  120. w
  121. q

That’s all! So we have five fragments (4 sections and 1 header) and 5 corresponding files if you used debug.exe. The next table summarizes our data:

RVA in
memory
Section File Offset
in file
0 (Header) header.bin 0
1000 .code code.bin 200
2000 .data data.bin 400
3000 .rdata rdata.bin 600
4000 .reloc reloc.bin 800

If you have used hex-editor, your binary file is ready. If you have used debug.exe you will have to combine the five binary files into one dll. Put all together in the file make.bat:

Код (Text):
  1. @echo off
  2. debug < header.txt > report.lst
  3. debug < code.txt >> report.lst
  4. debug < data.txt >> report.lst
  5. debug < rdata.txt >> report.lst
  6. debug < reloc.txt >> report.lst
  7. copy /b header.bin+code.bin+data.bin+rdata.bin+reloc.bin dll.dll

Launch this file and – hurrah! – we get out dll. Necessarily look at created report file – report.lst – to check for errors that may be reported by debug. Carefully look for errors. You perfectly know: cut&paste technique does not protect against the most stupid errors!

Well; but we will need some exe-application to test our dll, won’t we? I have absolutely no doubt that now your level will permit you to easily create such a test program using debug.exe :smile3:

Well, well... I see some confused faces. You have famously worked for a while today (if you have read this up to here) so I decided to grant you a «lazy» test application in MASM32 as a bonus. Here is it:

Код (Text):
  1. .386
  2. .model flat,stdcall
  3. option casemap:none
  4.  
  5. include \masm32\include\windows.inc
  6. include \masm32\include\kernel32.inc
  7. include \masm32\include\user32.inc
  8. includelib \masm32\lib\kernel32.lib
  9. includelib \masm32\lib\user32.lib
  10.  
  11. .data
  12.  
  13. hM1 dword 0
  14. hM2 dword 0
  15. app db "Test dll",0
  16. dll db "dll.dll",0
  17. dll2    db "dll2.dll",0
  18. fname   db "Function1",0
  19. err1    db "LoadLibrary (dll1) failed",0
  20. err1_1  db "LoadLibrary (dll2) failed",0
  21. err2    db "GetProcAddress (first) failed",0
  22. err2_1  db "GetProcAddress (second) failed",0
  23.  
  24. .code
  25. start:
  26. invoke LoadLibrary,offset dll
  27. .if eax==0
  28.     invoke MessageBox,0,offset err1,offset app,MB_ICONERROR
  29.     ret
  30. .endif
  31. mov hM1,eax
  32. invoke LoadLibrary,offset dll2
  33. .if eax==0
  34.     invoke MessageBox,0,offset err1_1,offset app,MB_ICONERROR
  35.     ret
  36. .endif
  37. mov hM2,eax
  38. invoke GetProcAddress,hM1,offset fname
  39. .if eax==0
  40.     invoke MessageBox,0,offset err2,offset app,MB_ICONERROR
  41.     ret
  42. .endif
  43. call eax
  44. invoke GetProcAddress,hM2,offset fname
  45. .if eax==0
  46.     invoke MessageBox,0,offset err2_1,offset app,MB_ICONERROR
  47.     ret
  48. .endif
  49. call eax
  50. invoke FreeLibrary,hM1
  51. invoke FreeLibrary,hM2
  52. ret
  53. end start

Copy created dll.dll with new name ‘dll2.dll’ in the same folder. The point is that we need to check for correctness of our fixups and for that system can work with them. We need at least two dll’s with the same preferred base load address for that purpose. The most lazy approach one could imagine is to use the second renamed instance of the same image. But you can experiment with them of course, at least to change text strings for output messages and create another dll with different module and function names.

See you later!

© Roustem

0 1.738
archive

archive
New Member

Регистрация:
27 фев 2017
Публикаций:
532