flat assembler
Message board for the users of flat assembler.

Index > Windows > PE Header questions.

Author
Thread Post new topic Reply to topic
Overflowz



Joined: 03 Sep 2010
Posts: 1046
Overflowz 19 Mar 2011, 11:39
Hello everyone. I got problem on headers.. I don't know how to use them.. for example, I'm trying to move IMAGE_DOS_HEADER's e_lfanew value in EDI but it fails and I don't know why. Here's example and tell me what I'm doing wrong! Thank you.
Code:
section '.data' data readable writeable
struct IMAGE_DOS_HEADER
                   e_magic dw ?
                   e_cblp dw ?
                   e_cp dw ?
                   e_crlc dw ?
                   e_cparhdr dw ?
                   e_minalloc dw ?
                   e_maxalloc dw ?
                   e_ss dw ?
                   e_sp dw ?
                   e_csum dw ?
                   e_ip dw ?
                   e_cs dw ?
                   e_lfarlc dw ?
                   e_ovno dw ?
                   e_res dw 4
                   e_oemid dw ?
                   e_oeminfo dw ?
                   e_res2 dw 10
                   e_lfanew dd ?
ends
idosh IMAGE_DOS_HEADER

section '.code' code readable executable

mov edi,[idosh.lfanew]

...    


after this, EDI doesn't have 4D 5A value or vice versa 5A 4D. How this structure can read data from executable ? I mean, it must be filled right ? It's going fill automatically or what ? I don't understand..
Post 19 Mar 2011, 11:39
View user's profile Send private message Reply with quote
mindcooler



Joined: 01 Dec 2009
Posts: 423
Location: Västerås, Sweden
mindcooler 19 Mar 2011, 11:53
What do you expect to load from your uninitialized struct?

If you wanted to load from your own runtime image, perhaps you meant something like this:

Code:
mov edi,[$400000+IMAGE_DOS_HEADER.e_lfanew]    
Post 19 Mar 2011, 11:53
View user's profile Send private message Visit poster's website MSN Messenger ICQ Number Reply with quote
Overflowz



Joined: 03 Sep 2010
Posts: 1046
Overflowz 19 Mar 2011, 12:02
how can I initialize it ? I'm trying to locate PE Header's RVA in current process but I don't know how.. I found this structure which are often used for such things..
Post 19 Mar 2011, 12:02
View user's profile Send private message Reply with quote
Overflowz



Joined: 03 Sep 2010
Posts: 1046
Overflowz 19 Mar 2011, 12:02
A bit problem here.. When I'm doing that, it show's me different location. I've opened 2 binaries in hex editor and PE headers has different image bases. How can I find PE header's RVA ?
Post 19 Mar 2011, 12:02
View user's profile Send private message Reply with quote
typedef



Joined: 25 Jul 2010
Posts: 2909
Location: 0x77760000
typedef 20 Mar 2011, 03:21
You'll need to open the exe you want to fill it with

Code:
idosh IMAGE_DOS_HEADER

CreateFile(...) ;If reading from disk
mov [hFile],eax

ReadFile,[hFile],idosh, sizeof.IMAGE_DOS_HEADER,...

that will fill up the structure.

Or if you know how, you can make a Device driver that contains your executable code and you can do IRP request to the device using CreateFile('\\.\yourDeviceDriver'), ReadFile,[hDevice],idosh,sizeof.IMAGE_DOS_HEADER,....


and then map that into memory and execute using CreateThread or setting next EIP pointer to it.

    
Post 20 Mar 2011, 03:21
View user's profile Send private message Reply with quote
Overflowz



Joined: 03 Sep 2010
Posts: 1046
Overflowz 20 Mar 2011, 11:52
typedef
I don't know how to write drivers.. Sad But I have still problem here. Watch this code, it's not working normally.
Code:
struct IMAGE_DOS_HEADER
                   e_magic dw ?
                   e_cblp dw ?
                   e_cp dw ?
                   e_crlc dw ?
                   e_cparhdr dw ?
                   e_minalloc dw ?
                   e_maxalloc dw ?
                   e_ss dw ?
                   e_sp dw ?
                   e_csum dw ?
                   e_ip dw ?
                   e_cs dw ?
                   e_lfarlc dw ?
                   e_ovno dw ?
                   e_res dw ?
                   e_oemid dw ?
                   e_oeminfo dw ?
                   e_res2 dw ?
                   e_lfanew dd ?
ends
idosh IMAGE_DOS_HEADER

........

invoke CreateFile,fname,GENERIC_READ,0,0,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0
mov [hFile],eax
invoke ReadFile,[hFile],idosh,sizeof.IMAGE_DOS_HEADER,rbytes,0 
mov edi,[$400000+IMAGE_DOS_HEADER.e_lfanew] ;When I try idosh.e_lfanew, just watch in debugger what then happening. I can't explain. instead of mov edi,... it shows DB 8A or something..    
Post 20 Mar 2011, 11:52
View user's profile Send private message Reply with quote
idle



Joined: 06 Jan 2011
Posts: 440
Location: Ukraine
idle 20 Mar 2011, 14:23
can you tell the aim?
you have said "getting header's rva"
there are lots of headers
Post 20 Mar 2011, 14:23
View user's profile Send private message Reply with quote
typedef



Joined: 25 Jul 2010
Posts: 2909
Location: 0x77760000
typedef 20 Mar 2011, 16:23
Code:
PUSH EBX EDI


PUSH 0
PUSH FILE_ATTRIBUTE_NORMAL
PUSH OPEN_EXISTING
PUSH 0
PUSH 0
PUSH GENERIC_READ
PUSH fname
call    [CreateFile]

mov [hFile],eax

PUSH 0
PUSH rbytes
PUSH sizeof.IMAGE_DOS_HEADER
PUSH idosh
PUSH [hFile]
CALL  [ReadFile]

MOV EBX,idosh
MOV EDI,[EBX+IMAGE_DOS_HEADER.e_lfanew]


POP EDI EBX
    


try that..... Am at church right now Very Happy
Post 20 Mar 2011, 16:23
View user's profile Send private message Reply with quote
typedef



Joined: 25 Jul 2010
Posts: 2909
Location: 0x77760000
typedef 20 Mar 2011, 16:39
PS: e_lfanew points to another structure in this case. so you need to get that structure in order to get the entry point

Here, I found this.... I know you don't know C++; I'll translate it into ASM as soon as I get home

http://www.rohitab.com/discuss/topic/33534-how-to-change-entry-point-of-pe-file/

The entry point of the executable is located at IMAGE_NT_HEADERS->OptionalHeader.AddressOfEntryPoint which you can change to suit your needs.The steps in getting this information is :
1 - You would read the file to an IMAGE_DOS_HEADER structure,then go on to get the NT header of the module.The offset for this structure is IMAGE_DOS_HEADER->e_lfanew.
2 - read and understand the MS PE file format which you can get the documentation for with a simple google search.


Code:
long SetFileEntryPoint(char *szFilename,long newEntryPoint)
{
    IMAGE_DOS_HEADER *pHead;
    pHead = new IMAGE_DOS_HEADER;
       FILE *file = fopen(szFilename,"r+b");
     if(!file)
           return -1;
  fread(pHead,sizeof(IMAGE_DOS_HEADER),1,file);
       if(pHead->e_magic != IMAGE_DOS_SIGNATURE)
                return -1;
  long peHeader = pHead->e_lfanew;
 long peOptHeader = peHeader + 4 + sizeof(IMAGE_FILE_HEADER);
        fseek(file,peOptHeader,SEEK_SET);
   IMAGE_OPTIONAL_HEADER *pOpt;
        pOpt = new IMAGE_OPTIONAL_HEADER;
   fread(pOpt,sizeof(IMAGE_OPTIONAL_HEADER),1,file);
   pOpt->AddressOfEntryPoint = newEntryPoint;
       fseek(file,peOptHeader,SEEK_SET);
   fwrite(pOpt,sizeof(IMAGE_OPTIONAL_HEADER),1,file);
  fclose(file);
       delete pOpt;
        delete pHead;
       return 1;
}
    



NOTE:I take no credit in this work
Post 20 Mar 2011, 16:39
View user's profile Send private message Reply with quote
Overflowz



Joined: 03 Sep 2010
Posts: 1046
Overflowz 20 Mar 2011, 19:18
idle
I'm trying to understand how to modify program's header's in memory. For example, I want to open file, then store it in memory, get program's AddressOfEntrypoint(I Guess), then change it and save to disk. Or something like that.
typedef
Same problem.. I'd better wait for translate.
Regards
Post 20 Mar 2011, 19:18
View user's profile Send private message Reply with quote
typedef



Joined: 25 Jul 2010
Posts: 2909
Location: 0x77760000
typedef 20 Mar 2011, 23:25
@Overflows, sorry dude I am kind of busy writing my System Driver right now, so i just wrote it quickly......You can figure out some stuff if you think the results are not working. Use a debugger to look for changes.

I'm sorry if it doesn't work...maybe some one can help you out. I am a little busy right now

Code:
format PE GUI 4.0

include 'win32ax.inc'
include 'api/user32.inc'
include 'api/kernel32.inc'

entry winMain
;____________________________________
 IMAGE_DOS_SIGNATURE equ 5A4Dh ; MZ
 FILE_POINTER_MOVE_DISTANCE equ 4 + sizeof.IMAGE_FILE_HEADER
 IMAGE_NUMBEROF_DIRECTORY_ENTRIES equ 16

;____________________________________


section '.idata' import data readable
library \
         kernel32,'kernel32.dll',\
         user32  ,'user32.dll'        

section '.txt' code executable writable

struct IMAGE_DATA_DIRECTORY
  VirtualAddress  dd      ?; DWORD
  Size            dd      ?; DWORD
ends

struct IMAGE_OPTIONAL_HEADER
  Magic                         dw   ?;                                                 WORD
  MajorLinkerVersion            db   ?;                                                 BYTE
  MinorLinkerVersion            db   ?;                                                 BYTE
  SizeOfCode                    dd   ?;                                                 DWORD
  SizeOfInitializedData         dd   ?;                                                 DWORD
  SizeOfUninitializedData       dd   ?;                                                 DWORD
  AddressOfEntryPoint           dd   ?;                                                 DWORD
  BaseOfCode                    dd   ?;                                                 DWORD
  BaseOfData                    dd   ?;                                                 DWORD
  ImageBase                     dd   ?;                                                 DWORD
  SectionAlignment              dd   ?;                                                 DWORD
  FileAlignment                 dd   ?;                                                 DWORD
  MajorOperatingSystemVersion   dw   ?;                                                 WORD
  MinorOperatingSystemVersion   dw   ?;                                                 WORD
  MajorImageVersion             dw   ?;                                                 WORD
  MinorImageVersion             dw   ?;                                                 WORD
  MajorSubsystemVersion         dw   ?;                                                 WORD
  MinorSubsystemVersion         dw   ?;                                                 WORD
  Win32VersionValue             dd   ?;                                                 DWORD
  SizeOfImage                   dd   ?;                                                 DWORD
  SizeOfHeaders                 dd   ?;                                                 DWORD
  CheckSum                      dd   ?;                                                 DWORD
  Subsystem                     dw   ?;                                                 WORD
  DllCharacteristics            dw   ?;                                                 WORD
  SizeOfStackReserve            dd   ?;                                                 DWORD
  SizeOfStackCommit             dd   ?;                                                 DWORD
  SizeOfHeapReserve             dd   ?;                                                 DWORD
  SizeOfHeapCommit              dd   ?;                                                 DWORD
  LoaderFlags                   dd   ?;                                                 DWORD
  NumberOfRvaAndSizes           dd   ?;                                                 DWORD
  DataDirectory                 IMAGE_DATA_DIRECTORY ; IMAGE_NUMBEROF_DIRECTORY_ENTRIES
ends

struct IMAGE_DOS_HEADER
                   e_magic dw ?
                   e_cblp dw ?
                   e_cp dw ?
                   e_crlc dw ?
                   e_cparhdr dw ?
                   e_minalloc dw ?
                   e_maxalloc dw ?
                   e_ss dw ?
                   e_sp dw ?
                   e_csum dw ?
                   e_ip dw ?
                   e_cs dw ?
                   e_lfarlc dw ?
                   e_ovno dw ?
                   e_res dw ?
                   e_oemid dw ?
                   e_oeminfo dw ?
                   e_res2 dw ?
                   e_lfanew dd ?
ends

struct  IMAGE_FILE_HEADER
  Machine              dw ?; WORD
  NumberOfSections     dw ?; WORD
  TimeDateStamp        dd ?; DWORD
  PointerToSymbolTable dd ?; DWORD
  NumberOfSymbols      dd ?; DWORD
  SizeOfOptionalHeader dw ?; WORD
  Characteristics      dw ?; WORD
ends
;___________________________________________________
; ENTRY POINT HERE
;___________________________________________________
;
;
;
;___________________________________________________

proc winMain   ;hinstance,hPrevhinstance,LPSTR,int
     ;ESP+8  hinstance
     ;ESP+C  hPrevhinstance
     ;ESP+10 LPSTR lpCmdLine
     ;ESP+14 int nCmdShow

     PUSH EBP EDI EBX

     invoke CreateFile,'file.exe',GENERIC_ALL,FILE_SHARE_READ+FILE_SHARE_WRITE,0,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0
     mov   [hFile],eax
     test  eax,eax
     jnz    .err

     push  0
     push  rbytes
     push  sizeof.IMAGE_DOS_HEADER
     push  idosh
     push  [hFile]
     call  [ReadFile]
     ;jnz   .err

     mov   eax,idosh
     mov   dx,IMAGE_DOS_SIGNATURE
     test  dx,[eax+IMAGE_DOS_HEADER.e_magic] ; is this a valid header 'MZ'
     jnz   .cont
     invoke MessageBox,0,'The specified file is not a valid PE file!','Error',MB_OK+MB_ICONERROR
     push  [hFile]
     call  [CloseHandle]
     push  0
     call  [ExitProcess]
.cont:
     ;mov the structure again since we lost eax
     mov   eax,[idosh]
     mov   ebx,[eax+IMAGE_DOS_HEADER.e_lfanew]
     mov   [peHdr],ebx    ; we have the pointer to the PE structure here
     ;set the file pointer 4 bytes ahead of peHdr + the size of IMAGE_FILE_HEADER
     add   ebx,[peHdr]
     add   ebx,FILE_POINTER_MOVE_DISTANCE
     mov   [peOptionalHdr],ebx
     ;get a copy of 32bit low order bits in ebx
     mov   cx,bx
     mov   [LowWord],cx
     ;get a copy of 32bit high order bits in ebx
     mov   [HighWord],ebx
     shr   ebx,16

     push  FILE_CURRENT ; Move from current position
     push  ebx   ; Distance to move, high order of a 32 bit value which is ebx shr 16
     push  cx    ; Distance to move, low order of a 32 bit value which is bx=cx
     push  [hFile]
     call  [SetFilePointer]

     push  0
     push  rbytes
     push  sizeof.IMAGE_OPTIONAL_HEADER
     push  pOpt
     push  [hFile]
     call  [ReadFile]
     ;jnz   .err
     ;change the entry point and write it back
     mov     eax,[newEntryPoint]
     mov     ebx,[pOpt]
     mov     [ebx+IMAGE_OPTIONAL_HEADER.AddressOfEntryPoint],eax
     ;now set the file pointers back again(which means signed bits - ) and write starting from that pointer

     mov   ax,-1
     mul   [LowWord] ; Lowword * -1 = -Loword (signed)
     mov   eax,-1
     mul   [HighWord]

     push  FILE_CURRENT ; Move from current position going back
     push  [HighWord]   ; Distance to move, high order of a 32 bit value which is ebx shr 16
     push  [LowWord]    ; Distance to move, low order of a 32 bit value which is bx=cx
     push  [hFile]
     call  [SetFilePointer]
     ;write back
     push  0
     push  [bwriten]
     push  sizeof.IMAGE_OPTIONAL_HEADER
     push  pOpt
     push  [hFile]
     call  [WriteFile]
     test  eax,eax
     jnz   .err
     ;done here
     push  [hFile]
     call  [CloseHandle]

     POP EBX EDI EBP
     push 0
     call [ExitProcess]

.err:
    local buff:DWORD
    LEA   EDX,[buff]
    call  [GetLastError]

    push 0
    push 0
    push EDX
    push 0
    push eax
    push 0
    push FORMAT_MESSAGE_ALLOCATE_BUFFER+FORMAT_MESSAGE_FROM_SYSTEM
    call [FormatMessage]

    push MB_OK + MB_ICONINFORMATION
    push 0
    push dword[buff]
    push 0
    call [MessageBox]
    ret
endp


section '.data' data readable writable

newEntryPoint dd 99999999;00408080 ; valid , you can invalidate it for some other purposes

pOpt IMAGE_OPTIONAL_HEADER

idosh IMAGE_DOS_HEADER
hFile dd ?
rbytes dd ? ; read bytes

LowWord dw ?
HighWord dd ?

bwriten dd ?;

peHdr dd ?  ; pointer to PE Header ( e_lfanew )

peOptionalHdr dd ? ; It says optional but not really optional since its where we get the data from
    
Post 20 Mar 2011, 23:25
View user's profile Send private message Reply with quote
Overflowz



Joined: 03 Sep 2010
Posts: 1046
Overflowz 21 Mar 2011, 09:25
typedef
Thanks for your support. Smile It still doesn't work, I can't see changes in debugger.. I'll try to understand this myself.
Thank you all.
Post 21 Mar 2011, 09:25
View user's profile Send private message Reply with quote
vid
Verbosity in development


Joined: 05 Sep 2003
Posts: 7105
Location: Slovakia
vid 21 Mar 2011, 11:08
Overflow: Go over Iczlion's PE tutorial, it should give you infos you need in order to manipulate executables and/or dump them from memory.
Post 21 Mar 2011, 11:08
View user's profile Send private message Visit poster's website AIM Address MSN Messenger ICQ Number Reply with quote
idle



Joined: 06 Jan 2011
Posts: 440
Location: Ukraine
idle 21 Mar 2011, 12:11
modify victim.name
modify line #77
i did not check the code much
Code:
format pe gui

section '' code executable import readable writable
  dd 0,0,0,rva kernel_name,rva kernel_table,\
     0,0,0,rva user_name,rva user_table,\
     0,0,0,0,0

  kernel_name db 'kernel32.dll',0
  align 4
  kernel_table:
    CloseHandle      dd rva CloseHandle_
    CreateFileA      dd rva CreateFileA_
    ExitProcess      dd rva ExitProcess_
    ReadFile         dd rva ReadFile_
    SetFilePointer   dd rva SetFilePointer_
    WriteFile        dd rva WriteFile_
                     dd 0
    CloseHandle_     dw 0
                     db 'CloseHandle',0
    CreateFileA_     dw 0
                     db 'CreateFileA',0
    ExitProcess_     dw 0
                     db 'ExitProcess',0
    ReadFile_        dw 0
                     db 'ReadFile',0
    SetFilePointer_  dw 0
                     db 'SetFilePointer',0
    WriteFile_       dw 0
                     db 'WriteFile',0

  user_name db 'user32.dll',0
  align 4
  user_table:
    MessageBoxA      dd rva MessageBoxA_
                     dd 0
    MessageBoxA_     dw 0
                     db 'MessageBoxA',0

entry $
        push    0                      ;hTemplateFile
        push    0                      ;dwFlagsAndAttributes
        push    3                      ;dwCreationDistribution = OPEN_EXISTING
        push    0                      ;lpSecurityAttributes
        push    1                      ;dwShareMode = FILE_SHARE_READ
        push    $8000'0000+$4000'0000  ;dwDesiredAccess = GENERIC_READ+WRITE
        push    victim.name            ;lpFileName
        call    [CreateFileA]

        cmp     eax,-1
        je      exit
        mov     ebx,eax

        push    0                      ;lpOverlapped
        push    buffer.read            ;lpNumberOfBytesRead
        push    $40                    ;nNumberOfBytesToRead = sizeof.IMAGE_DOS_HEADER
        push    buffer.data            ;lpBuffer
        push    ebx                    ;hFile
        call    [ReadFile]

        cmp     [buffer.read],$40
        jne     leave_victim

        push    0                      ;dwMoveMethod = FILE_BEGIN
        push    0                      ;lpDistanceToMoveHigh
        push    dword[buffer.data+$3c] ;lDistanceToMove = IMAGE_DOS_HEADER.OffsetToNewExeHEader
        push    ebx                    ;hFile
        call    [SetFilePointer]

        cmp     eax,-1
        je      leave_victim

        push    0 buffer.read $38 buffer.data ebx
        call    [ReadFile]

        cmp     [buffer.read],$38      ;IMAGE_OPTIONAL_HEADER.ImageBase - IMAGE_NT_HEADERS + 4
        jne     leave_victim
        add     dword[buffer.data+$34],$1000;IMAGE_OPTIONAL_HEADER.ImageBase + 4

        push    1 0 (-4) ebx             ;hfile, lDistanceToMove = back to IMAGE_OPTIONAL_HEADER.ImageBase, lpDistanceToMoveHigh, dwMoveMethod = FILE_CURRENT
        call    [SetFilePointer]

        push    0                      ;lpOwerlapped
        push    buffer.read            ;lpNumberOfBytesWritten
        push    4                      ;nNumberOfBytesToWrite = sizeof.dword
        push    buffer.data+$34        ;lpBuffer = IMAGE_OPTIONAL_HEADER.ImageBase
        push    ebx                    ;hFile
        call    [WriteFile]

        push    0                      ;uType
        push    0                      ;lpCaption
        push    msg.base_updated       ;lpText
        push    0                      ;hWnd
        call    [MessageBoxA]

leave_victim:
        push    ebx                    ;hObject
        call    [CloseHandle]

exit:   push    eax                    ;uExitCode
        call    [ExitProcess]

victim:
  .name         db 'test\test.exe',0

msg:
  .base_updated db 'ImageBase updated',0

buffer:
  .read         dd ?
  .data         rb 512
    
Post 21 Mar 2011, 12:11
View user's profile Send private message Reply with quote
Overflowz



Joined: 03 Sep 2010
Posts: 1046
Overflowz 21 Mar 2011, 12:33
vid
idle
Thanks for your support. I've got much information now. I'll study on it. Thanks again.
Post 21 Mar 2011, 12:33
View user's profile Send private message Reply with quote
Overflowz



Joined: 03 Sep 2010
Posts: 1046
Overflowz 22 Mar 2011, 09:36
Last bit question. I don't understand the logic how people are getting PE header with this:
Code:
add esi,[esi+0x3c] ;<-- PE header    

ESI points beginning of file and then getting PE header like this but I really don't understand how.. Someone explain me please. Thank you Smile
Post 22 Mar 2011, 09:36
View user's profile Send private message Reply with quote
vid
Verbosity in development


Joined: 05 Sep 2003
Posts: 7105
Location: Slovakia
vid 22 Mar 2011, 09:43
0x3c = IMAGE_DOS_HEADER.e_lfanew

Did you start studying Iczlion's tutorial? It really contains all the answers you seek.
Post 22 Mar 2011, 09:43
View user's profile Send private message Visit poster's website AIM Address MSN Messenger ICQ Number Reply with quote
Overflowz



Joined: 03 Sep 2010
Posts: 1046
Overflowz 22 Mar 2011, 11:58
vid
Yes but I don't understand, when I try IMAGE_DOS_HEADER.e_lfanew it says it's 0x24.. why ? and another thing, why I can't just do 'mov esi,[esi+0x3c]' ?
Post 22 Mar 2011, 11:58
View user's profile Send private message Reply with quote
dancho



Joined: 06 Mar 2011
Posts: 74
dancho 22 Mar 2011, 12:28
@Overflowz
if you count bytes to the e_lfanew element you will score 60 ( 0x3C ) ,
data at e_lfanew represents the offset to the IMAGE_NT_HEADERS ,
to the Signature to be more accurate , skiping ms-dos stub...
Post 22 Mar 2011, 12:28
View user's profile Send private message Reply with quote
Overflowz



Joined: 03 Sep 2010
Posts: 1046
Overflowz 22 Mar 2011, 12:47
dancho
in above posts, I wrote IMAGE_DOS_HEADER structure. and when I'm accessing 'idosh.e_lfanew' it counts 0x24 but I know it is 0x3c and I don't understand why. Never mind, I'll try to figure out what's wrong there. Thanks!
Post 22 Mar 2011, 12:47
View user's profile Send private message Reply with quote
Display posts from previous:
Post new topic Reply to topic

Jump to:  


< Last Thread | Next Thread >
Forum Rules:
You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum
You cannot vote in polls in this forum
You cannot attach files in this forum
You can download files in this forum


Copyright © 1999-2025, Tomasz Grysztar. Also on GitHub, YouTube.

Website powered by rwasa.