Dump EXE with Ruby and C

Okuyacağınız yazı “Portable Executable” yani Windows çalıştırılabilir dosya formatının EXE başlık bilgilerinin Ruby ve C programlama dilleri ile okunmasını açıklamaktadır.Packer ve Malware Analiz üzerine çalışırken PEiD benzeri bir araç yazmak için uğraşırken konuyu bilmeyenler için açıklamak istedim.

Bir PE formatlı EXE dosya içerisinde çeşitli bilgiler barındırmaktadır. Örneğin, Kodun çalışmaya başlayacağı giriş noktası yani Entry Point , çalıştırılabilir dosyanın tipini belirleyen hexadecimal değerler, segment bilgileri gibi bir çok bilgi barındırabilir.Bir EXE dosyasının türünü belirleyen 4 byte’lık  hexadecimal  45 50  degeri PE dosya formatına denk gelmektedir. Fakat bir çok tip bulunmaktadır. Bunlardan bazıları ise MZ, NE, LE ve bizi ilgilendiren PE olarak sıralanabilir.

MZ bilgisini taşıyan EXE dosyalar eski DOS çalıştırılabilir dosyalarına ait bir bilgidir. PE dosya formatı içerisinde her bilginin saklandığı bir offset değeri mevcuttur ve eğer bir exe dosya içerisinden bilgi okumamız gerekiyorsa bu offset değerlerinde ki bilgileri  okumamiz gerekmektedir.

Çalıştırılabilir dosyalar iki başlıktan oluşmaktadır,  ve bunlar DOS başlıkları ve Windows başlıklarıdır. Windows başlıkları loader’ın ihtiyaç duyduğu bir çok bilgiyi barındırmaktadır. Bunlar Linker versiyonu, data segment bilgileri, resource segment bilgileri gibi çeşitli bilgilerdir. Aşağıda blok bilgileri ve lokasyonları listelenmektedir. Bu bilgileri EXE spesifikasyonlarında bulmanız mümkündür.


............

00h Specifies the signature word. The low byte contains
“N” (4Eh) and the high byte contains “E” (45h).
02h Specifies the linker version number.
03h Specifies the linker revision number.
04h Specifies the offset to the entry table (relative to
the beginning of the header).
06h Specifies the length of the entry table, in bytes.
08h Reserved.
0Ch Specifies flags that describe the contents of the
executable file. This value can be one or more of the
following bits:

Bit Meaning
0 The linker sets this bit if the executable-file
format is SINGLEDATA. An executable file with
this format contains one data segment. This bit
is set if the file is a dynamic-link library
(DLL).
1 The linker sets this bit if the executable-file
format is MULTIPLEDATA. An executable file with
this format contains multiple data segments. This
bit is set if the file is a Windows application.
If neither bit 0 nor bit 1 is set, the
executable-file format is NOAUTODATA. An
executable file with this format does not contain
an automatic data segment.
2 Reserved.
3 Reserved.
8 Reserved.
9 Reserved.
11 If this bit is set, the first segment in the
executable file contains code that loads the
application.
13 If this bit is set, the linker detects errors at
link time but still creates an executable file.
14 Reserved.
15 If this bit is set, the executable file is a
library module.
If bit 15 is set, the CS:IP registers point to an
initialization procedure called with the value in
the AX register equal to the module handle. The
initialization procedure must execute a far
return to the caller. If the procedure is
successful, the value in AX is nonzero.
Otherwise, the value in AX is zero.
The value in the DS register is set to the
library’s data segment if SINGLEDATA is set.
Otherwise, DS is set to the data segment of the
application that loads the library.
0Eh Specifies the automatic data segment number. (0Eh is
zero if the SINGLEDATA and MULTIPLEDATA bits are
cleared.)
10h Specifies the initial size, in bytes, of the local
heap. This value is zero if there is no local
allocation.
12h Specifies the initial size, in bytes, of the stack.
This value is zero if the SS register value does not
equal the DS register value.
14h Specifies the segment:offset value of CS:IP.
18h Specifies the segment:offset value of SS:SP.
The value specified in SS is an index to the module’s
segment table. The first entry in the segment table
corresponds to segment number 1.
If SS addresses the automatic data segment and SP is
zero, SP is set to the address obtained by adding the
size of the automatic data segment to the size of the
stack.
1Ch Specifies the number of entries in the segment table.
1Eh Specifies the number of entries in the
module-reference table.
20h Specifies the number of bytes in the nonresident-name
table.
22h Specifies a relative offset from the beginning of the
Windows header to the beginning of the segment table.
24h Specifies a relative offset from the beginning of the
Windows header to the beginning of the resource
table.
26h Specifies a relative offset from the beginning of the
Windows header to the beginning of the resident-name
table.
28h Specifies a relative offset from the beginning of the
Windows header to the beginning of the
module-reference table.
2Ah Specifies a relative offset from the beginning of the
Windows header to the beginning of the imported-name
table.
2Ch Specifies a relative offset from the beginning of the
file to the beginning of the nonresident-name table.
30h Specifies the number of movable entry points.
32h Specifies a shift count that is used to align the
logical sector. This count is log2 of the segment
sector size. It is typically 4, although the default
count is 9. (This value corresponds to the /alignment
[/a] linker switch. When the linker command line
contains /a:16, the shift count is 4. When the linker
command line contains /a:512, the shift count is 9.)
34h Specifies the number of resource segments.
36h Specifies the target operating system

……

Offset değerlerinin nasıl okunacağına dair iki kod aşağıda verilmiştir. fseek() fonksiyon 60 decimal yani 3ch offset degerine ayarlanıyor.fread() ile byte değerler okunuyor ve section değerine set ediliyor.

peFormath(char *fPath)
{
int section;
fseek(di, 60, SEEK_SET);
fread(&section, 1, sizeof(section), di);
fseek(di, section, SEEK_SET);

int i;
for(i=0;i>=1;i++){printf("%x\n", getc(di));}
}

Aşağıda ki kod ben C ile uğraşırken yan masada oturan sevgili Canberk tarafından yazılmıştır. Ruby ile yazılmış bu kod yine yazılmış kodun ana fonksiyonunun yani giriş noktasının adresini (Entry Point) vermektedir.


#!/usr/bin/env ruby
def hexdump (buf, x, printchs)
i = 0
charz = ""

buf.each_byte { |ch|
print “%02x ” % ch
charz << ch

i += 1
if i == x
print “\t #{charz.to_s}” if printchs
print “\r\n”
i = 0
charz = “”
end
}
end

def toAddr (buf)
bytez = []
addr = []
ret = “”
buf.each_byte { |by|
bytez << “%02x” % by
}
i = 0

bytez.each { |b|
addr[3-i] = b
i += 1
}

ret << “0x”
addr.each { |x| ret << “#{x}”}

ret
end

PEFile = ARGV[0]
mem_buf = File.open(PEFile, “rb”) { |io| io.read }
PEStart = 60
PEOffset = mem_buf[PEStart]
AddrEntryPointOffset = PEOffset + 0x28
BaseOfCodeOffset = PEOffset + 0x2c
baseOfCode = mem_buf[BaseOfCodeOffset, 4]
entryPoint = mem_buf[AddrEntryPointOffset, 4]

puts “reading headers of #{PEFile}”
puts “Offset of PE Header: %02x (%d)” % [PEOffset, PEOffset]
hexdump(mem_buf[PEOffset, 20], 10, false)
puts “——————————————————”
puts “Offset of AddressOfEntryPoint: #{AddrEntryPointOffset}”
puts “EntryPoint Addr: #{toAddr(entryPoint)}”
puts “Offset of BaseOfCode: #{BaseOfCodeOffset}”
puts “Base of Code: #{toAddr(baseOfCode)}”

Eski bir konu olmasına rağmen konu hakkında fikri olmayanların kafalarındaki soru işaretlerinin giderilebilmesi adına örneklendirmelerin faydalı olabileceğini düşünüyorum.