Page 1 of 3 123 LastLast
Results 1 to 10 of 21
  1. #1
    Dwar
    Dwar is offline
    Veteran Dwar's Avatar
    Join Date
    2010 Mar
    Posts
    2,222
    Thanks Thanks Given 
    211
    Thanks Thanks Received 
    2,230
    Thanked in
    292 Posts
    Rep Power
    10

    [Delphi] Memory Modification Tutorial & Template

    Memory Modification Tutorial & Template
    Useful snippets for creation game bots, trainers and other programs with ability to read and write memory of selected process.

    Code Features Focused On For Using Memory Template:

    ToolHelp Win32 API Function:
    • - Arguments to retrieve the PID of your game
      - Variables for Calling the Function


    Write Memory Procedures
    • - Arguments to write to the memory
      - Variable to Pass the ProcessID to WriteMemoryProcess
      - Calling the the procedure for WriteMemoryProcess:
      • -1 byte (byte)
        -2 bytes(word)
        -4 bytes(Cardinal)
        -Byte Array(Array of Bytes)


    Timer Procedures
    • -Setting Timer Interval Speed
      -Using Virtual Key Codes to Activate Hotkey


    Instructions for operating this memory modification template.
    and
    Functional Delphi Memory Modification Trainer Source Attached.

    Lets get right to it, I'm working with Delphi 2005, Delphi 7 shouldn't be
    much different, so Open Delphi Go to >
    File > New > VCL Forms Application -Delphi for Win32

    You will be presented with a blank GUI form and this skeleton code
     unit Unit1;

    interface

    uses
    Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
    Dialogs;

    type
    TForm1 = class(TForm)
    private
    { Private declarations }
    public
    { Public declarations }
    end;

    var
    Form1: TForm1;

    implementation

    {$R *.dfm}

    end.

    Now before you can use the API Function we have to tell Delphi we will be utilizing the ToolHelp 32 API so add it to the USES clause
     uses
    Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
    Dialogs, StdCtrls, tlhelp32;

    Lets begin by taking a look at the Toolhelp API function arguements we will be using.
     function GetProcessID(Const ExeFileName: string; var ProcessId: integer): boolean; 

    First we will be adding the variables we need to use the toolhelp32 function that retreives
    the ProcessID by looping through your running processes looking for the target process " SOME_GAME.exe".

    We will need 1 Const String ( Some_Game.exe) and 1 integer variable (for holding the process ID integer) to
    call this function, Boolean just means its either true or false ( SOME_GAME.exe is running or not)

    So add them.
     var
    Form1: TForm1;
    PidID : integer;
    Const
    ProgramName = ' SOME_GAME.exe';

    Lets take a look at the whole function though you just have to focus on the arguements for now.
     function GetProcessID(Const ExeFileName: string; var ProcessId: integer): boolean;
    var
    ContinueLoop: BOOL;
    FSnapshotHandle: THandle;
    FProcessEntry32: TProcessEntry32;
    begin
    result := false;
    FSnapshotHandle := CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    FProcessEntry32.dwSize := Sizeof(FProcessEntry32);
    ContinueLoop := Process32First(FSnapshotHandle, FProcessEntry32);
    while integer(ContinueLoop) <> 0 do begin
    if (StrIComp(PChar(ExtractFileName(FProcessEntry32.sz ExeFile)), PChar(ExeFileName)) = 0)
    or (StrIComp(FProcessEntry32.szExeFile, PChar(ExeFileName)) = 0) then begin
    ProcessId:= FProcessEntry32.th32ProcessID;
    result := true;
    break;
    end;
    ContinueLoop := Process32Next(FSnapshotHandle, FProcessEntry32);
    end;
    CloseHandle(FSnapshotHandle);
    end;

    Now lets make procedures you can easily call that writes to the games memory.
    Remember:
    byte = 1 byte
    word = 2 bytes
    cardinal = 4 bytes
    So we will use those data lenghts types to make quick pokes

    This procedure will write 1 byte to the games memory
     procedure poke1(Address: Cardinal; Data: Byte);
    var
    Written: Cardinal;
    begin
    WriteProcessMemory(?, Pointer(Address), @Data, SizeOf(Data), Written);
    end;

    Lets take a look at the function argument:
    procedure poke1(Address: Cardinal; Data: Byte);

    So we will be calling it with the address(offset) and the Data(opcodes) to modify it to.
    The data type, in this case "Byte" dictates the size.

    Lets take a look at the WriteProcessMemory part of the function.
     WriteProcessMemory(?, Pointer(Address), @Data, SizeOf(Data), Written); 

    The compiler won't like that question mark there, I just put it there so you would notice it.
    We need a process ID to write to, we don't know the process ID until our GetProcessID API function has it, so we will need a global variable there. Add your Global variable, mine is PidHandle
     var
    Form1: TForm1;
    PidHandle: integer;
    PidID : integer;

    So then we will have
     procedure poke1(Address: Cardinal; Data: Byte);
    var
    Written: Cardinal;
    begin
    WriteProcessMemory(PidHandle, Pointer(Address), @Data, SizeOf(Data), Written);
    end;

    Data "Word" for 2 bytes
     procedure poke2(Address: Cardinal; Data: Word);
    var
    Written: Cardinal;
    begin
    WriteProcessMemory(PidHandle, Pointer(Address), @Data, SizeOf(Data), Written);
    end;

    Data "Cardinal" for 4 bytes
     procedure poke4(Address: Cardinal; Data: Cardinal);
    var
    Written: Cardinal;
    begin
    WriteProcessMemory(PidHandle, Pointer(Address), @Data, SizeOf(Data), Written);
    end;

    So the only difference in these procedures is length of data being written, 1, 2 and 4 bytes.
    Great for a few NOPs or small pokes but writing a string of opcodes using small data types would be rather annoying, so to handle many opcodes we will specify the data as being an "array of byte".
     procedure pokeX(Address: Cardinal; Data: array of Byte);
    var
    Written: Cardinal;
    begin
    WriteProcessMemory(PidHandle, Pointer(Address), @Data, SizeOf(Data), Written);
    end;

    Now we have are functions setup, we have to call them.
    Switch to Form View, look at your Tool Palette > Categories > Standard > TButton.
    Put a button on your forum, double click the form button to enter Code view inside the
    button procedure.

    It automatically writes the needed types that have to add here:
     type
    TForm1 = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);

    And the button click start procedure is generated in the code section.
     procedure TForm1.Button1Click(Sender: TObject);
    begin
    end;

    Now our "ProgramName" Const String equals ' SOME_GAME.exe' and we declared an Integer Variable 'PidId' to hold ProcessID when the function is called.
    We will call it with an If statement so only if the ' SOME_GAME.exe' Process is found the code inside the if statement continue to execute
     if GetProcessID(ProgramName, PidId) then begin 

    Now remember that PidHandle Integer variable we declared and use for WriteMemoryProcess? The PidId can now pass the true processID.
    So now we can prepare the SOME_GAME.exe Process memory to be modified
     PidHandle  :=  OpenProcess(PROCESS_ALL_ACCESS,False,PidId); 

    And we can call our poke procedures now with the address and
    opcodes your writing, then close PidHandle.
     Poke1($401000, $90);
    poke2($401001, $9090);
    poke4($401003, $90909090);
    closehandle(PidHandle);

    Lets take a look at the whole Button Procedure
     procedure TForm1.Button1Click(Sender: TObject);
    begin
    if GetProcessID(ProgramName, PidId) then
    begin
    PidHandle := OpenProcess(PROCESS_ALL_ACCESS,False,PidId);
    poke1($401000, $90);
    poke2($401001, $9090);
    poke4($401003, $90909090);
    closehandle(PidHandle);
    end;
    end;

    Great, so lets take a look at everything we have so far.
     unit Unit1;

    interface

    uses
    Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
    Dialogs, tlhelp32, StdCtrls;

    type
    TForm1 = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
    private
    { Private declarations }
    public
    { Public declarations }
    end;

    var
    Form1: TForm1;
    PidHandle: integer;
    PidID : integer;
    Const
    ProgramName = ' SOME_GAME.exe';

    implementation

    {$R *.dfm}

    function GetProcessID(Const ExeFileName: string; var ProcessId: integer): boolean;
    var
    ContinueLoop: BOOL;
    FSnapshotHandle: THandle;
    FProcessEntry32: TProcessEntry32;
    begin
    result := false;
    FSnapshotHandle := CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    FProcessEntry32.dwSize := Sizeof(FProcessEntry32);
    ContinueLoop := Process32First(FSnapshotHandle, FProcessEntry32);
    while integer(ContinueLoop) <> 0 do begin
    if (StrIComp(PChar(ExtractFileName(FProcessEntry32.sz ExeFile)), PChar(ExeFileName)) = 0)
    or (StrIComp(FProcessEntry32.szExeFile, PChar(ExeFileName)) = 0) then begin
    ProcessId:= FProcessEntry32.th32ProcessID;
    result := true;
    break;
    end;
    ContinueLoop := Process32Next(FSnapshotHandle, FProcessEntry32);
    end;
    CloseHandle(FSnapshotHandle);
    end;
    //Write 1 byte to memory
    procedure poke1(Address: Cardinal; Data: Byte);
    var
    Written: Cardinal;
    begin
    WriteProcessMemory(PidHandle, Pointer(Address), @Data, SizeOf(Data), Written);
    end;
    //Write 2 bytes to memory
    procedure poke2(Address: Cardinal; Data: Word);
    var
    Written: Cardinal;
    begin
    WriteProcessMemory(PidHandle, Pointer(Address), @Data, SizeOf(Data), Written);
    end;
    //Write 4 bytes to memory
    procedure poke4(Address: Cardinal; Data: Cardinal);
    var
    Written: Cardinal;
    begin
    WriteProcessMemory(PidHandle, Pointer(Address), @Data, SizeOf(Data), Written);
    end;
    //Write an Array of bytes to memory
    procedure pokeX(Address: Cardinal; Data: Array of Byte);
    var
    Written: Cardinal;
    begin
    WriteProcessMemory(PidHandle, Pointer(Address), @Data, SizeOf(Data), Written);
    end;
    //Example Function Call 1
    procedure TForm1.Button1Click(Sender: TObject);
    begin
    if GetProcessID(ProgramName, PidId) then
    begin
    PidHandle := OpenProcess(PROCESS_ALL_ACCESS,False,PidId);
    poke1($401000, $90);
    poke2($401001, $9090);
    poke4($401003, $90909090);
    closehandle(PidHandle);
    end;
    end;

    end.

    If you need to write in a long string of opcodes Declare the Array of byte
     Var
    Form1: TForm1;
    PidHandle: integer;
    PidID : integer;
    byteArr : Array of byte;

    And this is an example of a Byte Array in a new button that was added.
     procedure TForm1.Button2Click(Sender: TObject);
    begin
    if GetProcessID(ProgramName, PidId) then
    begin
    PidHandle := OpenProcess(PROCESS_ALL_ACCESS,False,PidId);
    SetLength(byteArr, 16);
    byteArr[0] := $8B;
    byteArr[1] := $71;
    byteArr[2] := $10;
    byteArr[3] := $0F;
    byteArr[4] := $85;
    byteArr[5] := $6A;
    byteArr[6] := $9D;
    byteArr[7] := $FD;
    byteArr[8] := $FF;
    byteArr[9] := $83;
    byteArr[10] := $7E;
    byteArr[11] := $0C;
    byteArr[12] := $00;
    byteArr[13] := $0F;
    byteArr[14] := $85;
    byteArr[15] := $60;
    pokeX($401007, byteArr);
    SetLength(byteArr, 15)
    closehandle(PidHandle);
    end;
    end;

    On to the Timer and Hotkeys, Toggle View to see the form, Tool Palette> Categories again,
    this time choose System and pick Ttimer, drop it on your form.
    Select the Timer1 on your form, go to Object Inspector > Properties, set the timer intervals to
    200 ms. Double click on the form to jump within the timer procedure.
    Pick your virtual key
    I am going to use F1 (VK_F1)
    We are just going to check GetAsyncKeyState, if your hotkey is pressed then click the button.
     procedure TForm1.Timer1Timer(Sender: TObject);
    begin
    if
    (GetAsyncKeyState(VK_F1) <> 0)
    then Button1.Click;
    end;

    One last thing, like this if the SOME_GAME.exe process is not found, it will do nothing
    since all the code is inside the if statement, lets create a message if the process is not
    located.
     procedure TForm1.Button1Click(Sender: TObject);
    begin
    if GetProcessID(ProgramName, PidId) then
    begin
    PidHandle := OpenProcess(PROCESS_ALL_ACCESS,False,PidId);
    poke1($401000, $90);
    poke2($401001, $9090);
    poke4($401003, $90909090);
    closehandle(PidHandle);
    end else
    begin
    MessageDlg('Start SOME_GAME First.', mtwarning, [mbOK],0);
    end;
    end;

    Thats it, lets see the whole source code .
     unit Unit1;

    interface

    uses
    Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
    Dialogs, tlhelp32, StdCtrls, ExtCtrls;

    type
    TForm1 = class(TForm)
    Button1: TButton;
    Button2: TButton;
    Timer1: TTimer;
    Label1: TLabel;
    Label18: TLabel;
    Label2: TLabel;
    procedure Timer1Timer(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure Button1Click(Sender: TObject);
    private
    { Private declarations }
    public
    { Public declarations }
    end;

    var
    Form1: TForm1;
    PidHandle: integer;
    PidID : integer;
    byteArr : Array of byte;
    Const
    ProgramName = ' SOME_GAME.exe';

    implementation

    {$R *.dfm}

    // tlhelp32 function to Loop through processes and locate your target
    function GetProcessID(Const ExeFileName: string; var ProcessId: integer): boolean;
    var
    ContinueLoop: BOOL;
    FSnapshotHandle: THandle;
    FProcessEntry32: TProcessEntry32;
    begin
    result := false;
    FSnapshotHandle := CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    FProcessEntry32.dwSize := Sizeof(FProcessEntry32);
    ContinueLoop := Process32First(FSnapshotHandle, FProcessEntry32);
    while integer(ContinueLoop) <> 0 do begin
    if (StrIComp(PChar(ExtractFileName(FProcessEntry32.sz ExeFile)), PChar(ExeFileName)) = 0)
    or (StrIComp(FProcessEntry32.szExeFile, PChar(ExeFileName)) = 0) then begin
    ProcessId:= FProcessEntry32.th32ProcessID;
    result := true;
    break;
    end;
    ContinueLoop := Process32Next(FSnapshotHandle, FProcessEntry32);
    end;
    CloseHandle(FSnapshotHandle);
    end;

    //Write 1 byte to memory
    procedure poke1(Address: Cardinal; Data: Byte);
    var
    Written: Cardinal;
    begin
    WriteProcessMemory(PidHandle, Pointer(Address), @Data, SizeOf(Data), Written);
    end;

    //Write 2 bytes to memory
    procedure poke2(Address: Cardinal; Data: Word);
    var
    Written: Cardinal;
    begin
    WriteProcessMemory(PidHandle, Pointer(Address), @Data, SizeOf(Data), Written);
    end;

    //Write 4 bytes to memory
    procedure poke4(Address: Cardinal; Data: Cardinal);
    var
    Written: Cardinal;
    begin
    WriteProcessMemory(PidHandle, Pointer(Address), @Data, SizeOf(Data), Written);
    end;

    //Write an Array of bytes to memory
    procedure pokeX(Address: Cardinal; Data: Array of Byte);
    var
    Written: Cardinal;
    begin
    WriteProcessMemory(PidHandle, Pointer(Address), @Data, SizeOf(Data), Written);
    end;


    //Example Function Call 1
    procedure TForm1.Button1Click(Sender: TObject);
    begin
    if GetProcessID(ProgramName, PidId) then
    begin
    PidHandle := OpenProcess(PROCESS_ALL_ACCESS,False,PidId);
    poke1($401000, $90);
    poke2($401001, $9090);
    poke4($401003, $90909090);
    closehandle(PidHandle);
    end else
    begin
    MessageDlg('Start SOME_GAME First.', mtwarning, [mbOK],0);
    end;
    end;

    //Example Function Call 2
    procedure TForm1.Button2Click(Sender: TObject);
    begin
    if GetProcessID(ProgramName, PidId) then
    begin
    PidHandle := OpenProcess(PROCESS_ALL_ACCESS,False,PidId);
    SetLength(byteArr, 16);
    byteArr[0] := $8B;
    byteArr[1] := $71;
    byteArr[2] := $10;
    byteArr[3] := $0F;
    byteArr[4] := $85;
    byteArr[5] := $6A;
    byteArr[6] := $9D;
    byteArr[7] := $FD;
    byteArr[8] := $FF;
    byteArr[9] := $83;
    byteArr[10] := $7E;
    byteArr[11] := $0C;
    byteArr[12] := $00;
    byteArr[13] := $0F;
    byteArr[14] := $85;
    byteArr[15] := $60;
    pokeX($401007, byteArr);
    SetLength(byteArr, 15);
    closehandle(PidHandle);
    end else
    begin
    MessageDlg('Start SOME_GAME First.', mtwarning, [mbOK],0);
    end;
    end;

    // Timer to Detect Hotkey and Execute your Buttons Code
    procedure TForm1.Timer1Timer(Sender: TObject);
    begin
    if
    (GetAsyncKeyState(VK_F1) <> 0)
    then Button1.Click;
    end;
    end.

    Created by Dubbls
    Please, post your questions on forum, not by PM or mail

    I spend my time, so please pay a little bit of your time to keep world in equilibrium

  2. The Following 2 Users Say Thank You to Dwar For This Useful Post:


  3. #2
    MrSmith
    MrSmith is offline
    Member-in-training
    Join Date
    2010 Aug
    Posts
    85
    Thanks Thanks Given 
    9
    Thanks Thanks Received 
    7
    Thanked in
    4 Posts
    Rep Power
    0

    Re: [Delphi] Memory Modification Tutorial & Template

    Thank you Dwar for these nice guides
    Ever Danced With The Devil By The Pale Moonlight ?

  4. #3
    DJK
    DJK is offline
    New member
    Join Date
    2010 Sep
    Posts
    11
    Thanks Thanks Given 
    1
    Thanks Thanks Received 
    0
    Thanked in
    0 Posts
    Rep Power
    0

    Re: [Delphi] Memory Modification Tutorial & Template

    First of all, thanks for doing this !

    You might remember me from another topic a while ago where I was working on a delphi trainer. You gave me some code back then but it didn't fully work as I wanted it to. (there was a part with a mem buf or something that sometimes worked when set to 1 and other times to 100 etc which was confusing)

    So what I did today was change that "old?" code to this method in this post. It works as well. And no mem,buf values to be seen

    One thing though, and I asked it before but the methods proposed I didn't work or I was to dumb to make em work

    How do I freeze a value ? I got for example the location "0250DB10" that I put to "0", now I want to freeze it to 0... any suggestions to do this in delphi ?

    Thanks !

  5. #4
    MrSmith
    MrSmith is offline
    Member-in-training
    Join Date
    2010 Aug
    Posts
    85
    Thanks Thanks Given 
    9
    Thanks Thanks Received 
    7
    Thanked in
    4 Posts
    Rep Power
    0

    Re: [Delphi] Memory Modification Tutorial & Template

    The easiest way to do this is with a TTimer control, use readprocessmemory to retrieve the value and write a compare function ex:

    Code:
    Var
    MyVar : Integer;
    Procedure TTimer.Timer()
    ReadProcessMemory(Handle, Ptr(Address), @MyVar, SizeOf(MyVar), BytesRead);
    
    If MyVar := 1 Then
    begin
    WriteProcessMemory(Handle, Ptr(Address), 1, 1,BytesRead);
    end;
    end.
    This is not the most efficient way though although it is enough for what you need. As Dwar recommended a thread for something like this would be efficient although the code suggested here is more than adequate.

    Regards, MrSmith
    Ever Danced With The Devil By The Pale Moonlight ?

  6. #5
    DJK
    DJK is offline
    New member
    Join Date
    2010 Sep
    Posts
    11
    Thanks Thanks Given 
    1
    Thanks Thanks Received 
    0
    Thanked in
    0 Posts
    Rep Power
    0

    Re: [Delphi] Memory Modification Tutorial & Template

    Two questions.

    1. Can I use "cardinal" for float values ? If not do I need some other code for those ?

    2. I'm using Cheat Engine as my debugger to get the values. The green values (statics) are easy to change and work with, however how can I work with those black (non static) values ? They change everytime the game is rebooted...

  7. #6
    Dwar
    Dwar is offline
    Veteran Dwar's Avatar
    Join Date
    2010 Mar
    Posts
    2,222
    Thanks Thanks Given 
    211
    Thanks Thanks Received 
    2,230
    Thanked in
    292 Posts
    Rep Power
    10

    Re: [Delphi] Memory Modification Tutorial & Template

    Can I use "cardinal" for float values Yes, you can. For example, hex representation of 5.55 (float) is 0x40B1999A (big endian)
    how can I work with those black (non static) values
    find base address and offsets for those values
    Please, post your questions on forum, not by PM or mail

    I spend my time, so please pay a little bit of your time to keep world in equilibrium

  8. #7
    MrSmith
    MrSmith is offline
    Member-in-training
    Join Date
    2010 Aug
    Posts
    85
    Thanks Thanks Given 
    9
    Thanks Thanks Received 
    7
    Thanked in
    4 Posts
    Rep Power
    0

    Re: [Delphi] Memory Modification Tutorial & Template

    To read a value from a pointer you will have to declare each offset as a variable ex:
    Code:
     var
    HP = $20;
    BP = $0A458D1;
    mHP : Integer;
    BytesRead : cardinal;
    procedure yourfunction()
    ReadProcessMemory(handle, Ptr(BP), @mHP, SizeOf(mHP), BytesRead);
     ReadProcessMemory(handle, Ptr(BP + HP), @mHP, SizeOf(mHP), BytesRead);
      // here you can place the read variable into an editbox or label
      // Label1.Caption := IntToStr(mHP);
    Regards, MrSmith
    Ever Danced With The Devil By The Pale Moonlight ?

  9. #8
    DJK
    DJK is offline
    New member
    Join Date
    2010 Sep
    Posts
    11
    Thanks Thanks Given 
    1
    Thanks Thanks Received 
    0
    Thanked in
    0 Posts
    Rep Power
    0

    Re: [Delphi] Memory Modification Tutorial & Template

    Hey,

    I have been experimenting the last few days... got another question..

    If I have a value like this (using cheatengine):
    Game.exe+1D0DB10

    I take that means it's a sort of pointer right ? As in, it will work even if the resulted memory address isn't "static".

    If I'm right in that assumption, then how would I go aobut it to use such value in the tutorial of this thread ? I presume I can't just use "Game.exe+1D0DB10" like this:

    poke4(100, Game.exe + $1D0DB10);

    I KNOW this is wrong, it's just that I'm not sure how to adapt such pointer thing to the code Im working with here And I'm hoping im right about the "this works on non static addresses" part as that would be awesome for my progress

  10. #9
    beBoss
    beBoss is offline
    New member beBoss's Avatar
    Join Date
    2010 Nov
    Location
    In her heart
    Posts
    9
    Thanks Thanks Given 
    0
    Thanks Thanks Received 
    0
    Thanked in
    0 Posts
    Rep Power
    0

    Re: [Delphi] Memory Modification Tutorial & Template

    For non static address you need base address.

    If you address is static (green in cheatengine) should works like this:

    Code:
     poke1($1D0DB10, $64);
    value: 100 - in hex 64
    [Please, register to view links]
    Живота е като терена или ставаш силен, блокираш, сваляш и оцеляваш
    или се отказваш, падаш и си заминаваш !


    beBoss™

  11. #10
    DJK
    DJK is offline
    New member
    Join Date
    2010 Sep
    Posts
    11
    Thanks Thanks Given 
    1
    Thanks Thanks Received 
    0
    Thanked in
    0 Posts
    Rep Power
    0

    Re: [Delphi] Memory Modification Tutorial & Template

    Green values are not any problem It's the black values that give me headaches..

Page 1 of 3 123 LastLast

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •