lavender dbg/
├── CMakeLists.txt
└── src/ ├── main.cpp ├── target.c └── ...
lavender dbg/
├── CMakeLists.txt
└── src/ ├── main.cpp ├── target.c └── ...
lavender dbg/
├── CMakeLists.txt
└── src/ ├── main.cpp ├── target.c └── ...
sudo apt update
sudo apt install g++ gcc
sudo apt install cmake #cmake
sudo apt install binutils #objdump
sudo apt install libcapstone-dev #capstone
sudo apt update
sudo apt install g++ gcc
sudo apt install cmake #cmake
sudo apt install binutils #objdump
sudo apt install libcapstone-dev #capstone
sudo apt update
sudo apt install g++ gcc
sudo apt install cmake #cmake
sudo apt install binutils #objdump
sudo apt install libcapstone-dev #capstone
mkdir build
cd build
cmake .. # First time must configure CMake, cmake is in the parent directory
make #compile program
mkdir build
cd build
cmake .. # First time must configure CMake, cmake is in the parent directory
make #compile program
mkdir build
cd build
cmake .. # First time must configure CMake, cmake is in the parent directory
make #compile program
./lavender ./target
./lavender ./target
./lavender ./target
pid_t pid = fork(); // if pid = -1 , error occured
if (pid == -1)
{ cerr << "Fork error" << strerror(errno) << endl; return -1;
} // pid is 0 , this is child program
else if (pid == 0)
{ // ask system to be traced by parents ptrace(PTRACE_TRACEME, 0, nullptr, nullptr); char *args[] = {const_cast<char *>(target_path), nullptr}; // fork into target_path state , if success , code below will not run // target path is for kernel , args for child program execv(target_path, args); cerr << "Errno : " << strerror(errno) << endl; exit(1);
}
pid_t pid = fork(); // if pid = -1 , error occured
if (pid == -1)
{ cerr << "Fork error" << strerror(errno) << endl; return -1;
} // pid is 0 , this is child program
else if (pid == 0)
{ // ask system to be traced by parents ptrace(PTRACE_TRACEME, 0, nullptr, nullptr); char *args[] = {const_cast<char *>(target_path), nullptr}; // fork into target_path state , if success , code below will not run // target path is for kernel , args for child program execv(target_path, args); cerr << "Errno : " << strerror(errno) << endl; exit(1);
}
pid_t pid = fork(); // if pid = -1 , error occured
if (pid == -1)
{ cerr << "Fork error" << strerror(errno) << endl; return -1;
} // pid is 0 , this is child program
else if (pid == 0)
{ // ask system to be traced by parents ptrace(PTRACE_TRACEME, 0, nullptr, nullptr); char *args[] = {const_cast<char *>(target_path), nullptr}; // fork into target_path state , if success , code below will not run // target path is for kernel , args for child program execv(target_path, args); cerr << "Errno : " << strerror(errno) << endl; exit(1);
}
// Run until hit 0xcc
ptrace(PTRACE_CONT, m_pid, nullptr, nullptr); // Wait for hit 0xcc
int status;
waitpid(m_pid, &status, 0); if (WIFSIGNALED(status))
{ cout << "[Debugger] : Program exited with code " << WTERMSIG(status) << endl; return false;
} // Write back the backup
ptrace(PTRACE_POKETEXT, m_pid, m_bp_addr, m_bp_backup); // Get current RIP
struct user_regs_struct regs;
ptrace(PTRACE_GETREGS, m_pid, nullptr, ®s); // RIP pointer already jumped to next instruction, need to pull back and write back regs
regs.rip = m_bp_addr;
ptrace(PTRACE_SETREGS, m_pid, nullptr, ®s); cout << Color::BOLD_CORAL_RED << "[Debugger] Hit breakpoint at 0x" << hex << m_bp_addr << dec << Color::RESET << endl << endl;
// Run until hit 0xcc
ptrace(PTRACE_CONT, m_pid, nullptr, nullptr); // Wait for hit 0xcc
int status;
waitpid(m_pid, &status, 0); if (WIFSIGNALED(status))
{ cout << "[Debugger] : Program exited with code " << WTERMSIG(status) << endl; return false;
} // Write back the backup
ptrace(PTRACE_POKETEXT, m_pid, m_bp_addr, m_bp_backup); // Get current RIP
struct user_regs_struct regs;
ptrace(PTRACE_GETREGS, m_pid, nullptr, ®s); // RIP pointer already jumped to next instruction, need to pull back and write back regs
regs.rip = m_bp_addr;
ptrace(PTRACE_SETREGS, m_pid, nullptr, ®s); cout << Color::BOLD_CORAL_RED << "[Debugger] Hit breakpoint at 0x" << hex << m_bp_addr << dec << Color::RESET << endl << endl;
// Run until hit 0xcc
ptrace(PTRACE_CONT, m_pid, nullptr, nullptr); // Wait for hit 0xcc
int status;
waitpid(m_pid, &status, 0); if (WIFSIGNALED(status))
{ cout << "[Debugger] : Program exited with code " << WTERMSIG(status) << endl; return false;
} // Write back the backup
ptrace(PTRACE_POKETEXT, m_pid, m_bp_addr, m_bp_backup); // Get current RIP
struct user_regs_struct regs;
ptrace(PTRACE_GETREGS, m_pid, nullptr, ®s); // RIP pointer already jumped to next instruction, need to pull back and write back regs
regs.rip = m_bp_addr;
ptrace(PTRACE_SETREGS, m_pid, nullptr, ®s); cout << Color::BOLD_CORAL_RED << "[Debugger] Hit breakpoint at 0x" << hex << m_bp_addr << dec << Color::RESET << endl << endl;
bool isCall = (insn[0].id == X86_INS_CALL); // Place to return after call
uint64_t nextaddr = rip + insn[0].size;
uint64_t call_target = 0;
if (buf[0] == 0xE8)
{ int32_t rel; memcpy(&rel, buf + 1, 4); call_target = rip + 5 + rel;
}
cs_free(insn, count);
cs_close(&handle); if (isCall)
{ // First find from symbol, is there jump target function name, if in libc will not find for (auto &s : symbols) { if (s.offset + base_address == call_target) { cout << Color::BOLD_LIGHT_RED << "Call -> [ " << s.name << " ]" << endl; break; } } // If call, set breakpoint at end of call set_breakpoint(nextaddr); return run_to_breakpoint();
}
bool isCall = (insn[0].id == X86_INS_CALL); // Place to return after call
uint64_t nextaddr = rip + insn[0].size;
uint64_t call_target = 0;
if (buf[0] == 0xE8)
{ int32_t rel; memcpy(&rel, buf + 1, 4); call_target = rip + 5 + rel;
}
cs_free(insn, count);
cs_close(&handle); if (isCall)
{ // First find from symbol, is there jump target function name, if in libc will not find for (auto &s : symbols) { if (s.offset + base_address == call_target) { cout << Color::BOLD_LIGHT_RED << "Call -> [ " << s.name << " ]" << endl; break; } } // If call, set breakpoint at end of call set_breakpoint(nextaddr); return run_to_breakpoint();
}
bool isCall = (insn[0].id == X86_INS_CALL); // Place to return after call
uint64_t nextaddr = rip + insn[0].size;
uint64_t call_target = 0;
if (buf[0] == 0xE8)
{ int32_t rel; memcpy(&rel, buf + 1, 4); call_target = rip + 5 + rel;
}
cs_free(insn, count);
cs_close(&handle); if (isCall)
{ // First find from symbol, is there jump target function name, if in libc will not find for (auto &s : symbols) { if (s.offset + base_address == call_target) { cout << Color::BOLD_LIGHT_RED << "Call -> [ " << s.name << " ]" << endl; break; } } // If call, set breakpoint at end of call set_breakpoint(nextaddr); return run_to_breakpoint();
}
lavender-dbg/
├── CMakeLists.txt
└── src/ ├── main.cpp # Module integration and CLI ├── target.c # Target program in C ├── common/ │ └── color.h # Output color definitions ├── debugger/ # Debugger module │ ├── debugger.cpp │ └── debugger.h ├── memory/ # /proc/maps reading │ ├── memory.cpp │ └── memory.h ├── process/ # Subprocess launching and control │ ├── process.cpp │ └── process.h └── symbols/ # ELF symbol parsing ├── symbols.cpp └── symbols.h
lavender-dbg/
├── CMakeLists.txt
└── src/ ├── main.cpp # Module integration and CLI ├── target.c # Target program in C ├── common/ │ └── color.h # Output color definitions ├── debugger/ # Debugger module │ ├── debugger.cpp │ └── debugger.h ├── memory/ # /proc/maps reading │ ├── memory.cpp │ └── memory.h ├── process/ # Subprocess launching and control │ ├── process.cpp │ └── process.h └── symbols/ # ELF symbol parsing ├── symbols.cpp └── symbols.h
lavender-dbg/
├── CMakeLists.txt
└── src/ ├── main.cpp # Module integration and CLI ├── target.c # Target program in C ├── common/ │ └── color.h # Output color definitions ├── debugger/ # Debugger module │ ├── debugger.cpp │ └── debugger.h ├── memory/ # /proc/maps reading │ ├── memory.cpp │ └── memory.h ├── process/ # Subprocess launching and control │ ├── process.cpp │ └── process.h └── symbols/ # ELF symbol parsing ├── symbols.cpp └── symbols.h
Parent Process (lavender)
└─ fork() ├─ Child Process → execv (CMake automatically compiles target.c as the default debug target) │ + PTRACE_TRACEME ← Requests the kernel to be traced by the parent └─ Parent Process → Waits for and controls the child process
Parent Process (lavender)
└─ fork() ├─ Child Process → execv (CMake automatically compiles target.c as the default debug target) │ + PTRACE_TRACEME ← Requests the kernel to be traced by the parent └─ Parent Process → Waits for and controls the child process
Parent Process (lavender)
└─ fork() ├─ Child Process → execv (CMake automatically compiles target.c as the default debug target) │ + PTRACE_TRACEME ← Requests the kernel to be traced by the parent └─ Parent Process → Waits for and controls the child process - Introduction
- Environment Requirements
- Core Features
- Core Design and Code Analysis
- Actual Execution Demo
- Architecture Overview
- How You Can Expand
- Future Plans & Conclusion - Beginners in system programming
- Users who want to understand debugger principles
- People curious about underlying principles - Users who need professional features
- Penetration testing or CTF competition environments - Code is readable and easy to understand, clearly demonstrating the underlying principles of the debugger
- Clear architecture, easy to expand, can add or remove features according to your needs
- Lays the foundation for system programming - Operating System Linux (recommended Linux MINT)
- Compilation tools g++(C++17), gcc
- Build tools CMake 3.16+
- External packages capstone, objdump - Write back the backed-up machine code from when the breakpoint was set
- Pull back the RIP (when the CPU executes to 0xcc, RIP has already pointed to the next instruction)
- Write back the current registers - Determine if the instruction is call(X86_INS_CALL)
- If it is a call, set a breakpoint at the next instruction of this instruction (after jumping, return will come back here)
- Check if there is a function name corresponding to the address in symbols (if it is libc etc., it will show not found)
- Run to the breakpoint - User provides the debugged program name
- target.c is compiled along with the build as the default debug target - Read /proc/[PID]/maps
- Obtain maps and base_address - Use objdump to parse ELF file
- Obtain function offsets - breakpoint After setting breakpoint, immediately executes to the breakpoint
- Executes line by line; if encountering call, automatically queries the symbol table, looks up the jump function name, and sets breakpoint at the return location - Cannot directly modify the memory content of the target process
- Parent program currently shares the same terminal with child process; if using step to enter input function, may cause abnormality
- Uses parent program to start target child process; cannot attach to any process
- May have some functions missing or unstable - Modify child process memory content
Lavender currently can only read; can add PTRACE_POKETEXT to change memory
- Attach to any program
Lavender currently uses fork(); you can change to PTRACE_ATTACH
- Custom CLI interface
Commands in main.cpp are easy to replace and change
- Terminal separation
Lavender currently has child and parent sharing the same terminal; you can use dup2 to separate