Sprint Pwndbg (10.08.2022)

Hi! Witaj na stronie sprintu Pwndbg. Poniżej możesz przeczytać opisy przykładowych rzeczy, które można by dodać lub usprawnić w Pwndbg :).

Sprint zaczniemy od małego wstępu "co to jest Pwndbg" i "co mogę z tym zrobić", a następnie podzielimy się na zadania wylistowane poniżej (ofc można dodać również i swoje).

Jak rozpocząć debugowanie programu w GDB?

$ echo 'int main() { puts("Hello world"); )}' > a.c
$ gcc a.c
$ gdb --quiet ./a.out

pwndbg> break main
pwndbg> continue

Przydatne komendy:

break <symbol albo adres>     - ustawia breakpoint na danym adresie
run [<argumenty>]             - uruchamia procesu z danymi argumentami (jeśli nie ustawiliśmy breakpointa, to proces może się skończyć)
starti [<argumenty>]          - uruchamia proces i zatrzymuje się na jego pierwszej instrukcji
continue                      - kontynuuje wykonywanie programu
si                            - skrót od "step instruction", czyli: wykonaj dokładnie jedną instrukcję (wchodząc również do funkcji które są wywoływane)
ni                            - skrót od "next instruction", czyli: wykonaj jedną instrukcję, ale jeśli jest ona wywołaniem funkcji (np. instrukcja "call") to wykonaj całe wnętrze i zatrzymaj się dopiero na następnej instrukcji w funkcji w której jesteśmy

info break                    - wyświetl ustawione breakpointy
delete <numer-breakpointa>    - usuwa dany breakpoint; jeśli nie podamy numeru, to usunie wszystkie


print <wyrazenie>             - oblicz i wypisz dane wyrażenie 

x/10i $rip                    - komenda GDB "examine", posiadająca różne formaty; tu: wyświetlająca 10 instrukcji rozpoczynając od adresu z rejestru RIP (IP == Instruction Pointer), który wskazuje na instrukcję którą program będzie właśnie wykonywać

Polecam również GDB cheatsheet.

Taski na sprincie

Zadania mają różną trudność i wymagają różnej wiedzy. Prostsze zadania mogą pomóc w rozeznaniu się w strukturze projektu lub różnych schematach, np. jak wygląda "komenda".

PS: Na samym dole dodałem kilka przykładów róznych API w Pwndbg, które mogą się przydać oraz informacji o samym GDB.

Taski / kto co robi (poniżej jest dłuższy opis tasków):


Poprawienie opisu komendy memfrob

Komenda memfrob robi XORa danej pamięci z kluczem '*'.

Powinniśmy to ująć w jej opisie, tak, żeby memfrob --help lub help memfrob jasno opisywało co to robi.

  • Komendę memfrob możemy znaleźć szukając w projekcie funkcji memfrob naturalnie :)

Sprzątanie po Pythonie 2

Stare wersje Pwndbg wspierały Pythona 2, ale od pewnej wersji usunęliśmy support dla Pythona 2, gdyż obecnie żadne nowe distro nie buduje GDB z Pythonem 2.

W kodzie Pwndbg pozostały jednak rzeczy, które potrzebne były jedynie w Pythonie 2, a teraz możemy się ich pozbyć.

Na przykład:

  • komentarze odnośnie kodowania plików (-*- coding: utf-8 -*- itp.)
  • w sumie to nie związane z Py2, ale niektóre pliki mają też shebang (#!/usr/bin/env python), który jest zbędny, gdyż nigdy nie są uruchamiane bezpośrednio

Być może coś jeszcze?

Rejestr X30 na architekturze aarch64

Aarch64 to 64-bitowa architektura ARM (czyli to, co mamy m.in. w telefonach czy Macbookach M1/M2).
Na tej architekturze jest dość dużo rejestrów - X0-X30 - i obecnie chyba źle wyświetlamy jeden z nich.

Tu więcej info: https://github.com/pwndbg/pwndbg/issues/1039

To wszystko można przetestować lokalnie na architekturze x64 używając QEMU user emulation. Aby to zrobić musimy:

  • zainstalować paczkę z QEMU user (sudo apt install qemu-user)
  • zainstalować kompilator gcc na aarch64, aby cross-kompilować przykładowy program napisany w C do AARCH64 (sudo apt install gcc-11-aarch64-linux-gnu)
  • stworzyć przykładowy program (echo 'int main() {}' > main.c)
  • skompilować go
  • wtedy możemy uruchomić ten program pod qemu: qemu-aarch64 -L <sciezka-do-/lib-aarch64> ./a.out
  • dodając flagę -g 1234 QEMU wystawi gdbserver na danym porcie (tu: 1234)
  • wtedy możemy w GDB zrobić target remote :1234 aby podłączyć się do tego gdbservera i debugować emulowany proces

Dodanie możliwości telescope --reversed

  • TL;DR: Trzeba wysłać PR z patchem, który jest w https://github.com/pwndbg/pwndbg/issues/1047
  • Oraz pomyśleć, czy nie nazwać tego argumentu inaczej
  • Warto zbadać co się stanie jeśli będziemy klikać <enter> dalej - czy pokażą się kolejne adresy? Czy powinniśmy renderować poprzednie czy kolejne wartości? Do przedyskutowania

Poprawienie parsowania adresów

TL;DR: https://github.com/pwndbg/pwndbg/issues/1050

Dodanie komendy patch <adres> <dane lub instrukcje>

Przydałaby się komenda do "patchowania" kodu, tak, żeby można było w łatwy sposób zmienić kod programu na inny - na przykład usunąć "ifa" z programu, albo zrobić tak, żeby dana funkcja zawsze zwracała zero (lub inną wartość).

Przykładowo, patch 0x1234 nop; nop; nop (na x86/x64) powinno wpisać pod adres 0x1234 zasemblowane instrukcje nop; nop; nop, czyli wartość 0x90 trzy razy, bo instrukcja nop, która jest tak zwanym "no operation", tzn. nic nie robi, odpowiada właśnie bajtowi 0x90 na architekturach x86/x64.

Przydatne rzeczy:

  • musimy stworzyć nową komendę w pliku pwndbg/commands/patch.py - możemy
    Do tego zadania przyda się biblioteka pwntools (którą trzeba dodać jako zależność)

Kod plus minus:

# prawdopodobnie trzeba string arch jakoś zmapować z nazw Pwndbg na nazwy architektur używane przez Pwntools
import pwnlib

arch = pwndbg.arch.current
data = pwnlib.asm.asm(assembly_code, arch=arch)
pwndbg.memory.write(address, data)

Potencjalne rozszerzenia:

  1. Zamiast pobierać architekturę za każdym razem podczas wykonywania komendy, moglibyśmy ustawiać pwnlib.context.context.arch = ... gdy ustawiane jest pwndbg.arch.current
  2. Można rozszerzyć interfejs tak, aby pozwolić wprowadzać bajty instrukcji heksadecymalnie, zamiast tylko poprzez instrukcje asemblera; na przykład:
  • patch 0x1234 0x90 - powinno wpisać pod 0x1234 bajt 0x90 (czyli instrukcję nop na x86/x64)
  • patch 0x1234 9090 - powinno wpisać dwie instrukcje nop
    • Do zastanowienia się: czy powinniśmy supportować hexy bez prefixu 0x?
  1. Można zapisywać listę wszystkich dodanych patchy i wtedy:
  • patch --list - listowałoby zaaplikowane patche
  • patch --disable <id...> - wyłączałoby dane patche
  • patch --enable <id...> - włączałoby dane patche
  • patch --revert <id...> - pozwołiłoby zrevertować dany patch
  • patch --revert-all - cofałoby wszystkie patche
  1. Powinniśmy pozwalać na export patchy do pliku
  2. Dodanie testów :)

Potencjalne problemy:

  • Co z rebase'owaniem binarki (ASLR/PIE)?

Dodanie testów do wybranych funkcjonalności

  • obecnie Pwndbg ma dość mało testów
  • testy na CI są uruchamiane przez PWNDBG_GITHUB_ACTIONS_TEST_RUN=1 sudo --preserve-env ./tests.sh i są zaimplementowane w ./tests/

Potrzebujemy testów m.in. dla wielu komend:

  • hexdump ...
  • nawigacja po programie - nextret, nextsyscall, stepsyscall, etc.
  • wyszukanie wartości w programie: search ...
  • zmieniania uprawnień do pamięci - mprotect
  • listowanie layoutu pamięci - vmmap
    • również dla targetów z QEMU user emulation
    • również dla targetów z QEMU kernel
  • komend wyświetlających pamięć w ciekawy sposób - probeleak, leakfind
  • (bardzo trudne) funkcjonalności związane ze stertą (heap) z Pull Requesta, dla różnych wersji glibc i architektur

Dodanie funkcjonalności/komendy "dump memory as code"

W skrócie, można by dodać, żeby niektóre komendy, jak na przykład dq, dw, dd, db pozwalały na zdumpowanie danych w formie kodu, tak, żeby można było łatwo wkleić je np. jako tablicę w C?

Dziwny bug #1008

Ktoś zgłosił bug, w którym jak ustawimy rejestr RIP (Instruction Pointer) na niezmapoawny adres, to Pwndbg się crashuje. Trzeba by to sprawdzić, zdebugować i naprawić: https://github.com/pwndbg/pwndbg/issues/1008 (lub zamknąć jeśli to nieaktualne).

Renderowanie argumentów printf-podobnych funkcji

Chodzi o to, żeby gdy jesteśmy na instrukcji call printf wyświetlać ładnie argumenty tego printfa.

Co nieco informacji o GDB

GDB (GNU Debugger) to konsolowy debugger na Linuxa, który obsługuje debugowanie programów napisanych w przeróżnych językach, na wiele różnych targetów - architektur czy ABI. Można tu np. zobaczyć wewnątrz GDB wykonanie komend:

  • show arch
  • show osabi
  • show language - warto wiedzieć, że "wybrany" język wpływa na to jakie wyrażenia możemy używać w GDB debugując dany program

GDB pod spodem korzysta pod spodem z wywołania systemowego ptrace, które pozwala na śledzenie procesów na Linuxie oraz na czytanie i modyfikację ich stanu (pamięci i rejestrów procesora).

Wcześniej wymienione arch, osabi czy language, to właściwie "parametry" GDB. Takich parametrów jest sporo i można je wszystkie wyświetlić komendą show, a co niektórych zmieniać wartość poprzez komendę set <parametr> <wartosc>.

Useful Pwndbg APIs

# Reading memory
# NOTE: may raise gdb.error(...) if `addr` is not readable or mapped in the target

pwndbg.memory.write(addr, data)             # write `data` bytes into memory, e.g. b'asdf'
data = pwndbg.memory.read(addr, count)      # read `count` bytes from memory, returns e.g. b'asdf'

# Useful constants
pwndbg.arch.endian                  # endianess used by the architecture
pwndbg.arch.ptrsize                 # size of a pointer on a given architecture
pwndbg.arch.ptrmask                 #

# Getting useful info about process
pwndbg.arch.current                 # name of target architecture (string)
pwndbg.regs.current                 # list of target registers
pwndbg.heap.current                 # currently used heap

pwndbg.vmmap.get()                  # get list of memory maps mapped into the process
pwndbg.auxv.get()                   # get ELF Auxiliary Vectors info
pwndbg.file.get(filepath)           # download a file from the target's filesystem
pwndbg.symbol.get(address)          # fetch text name for a symbol at given address
pwndbg.disasm.get(address, n)       # disassebmle N instructions at address
pwndbg.chain.get(addr, limit, offset, ...)  # recursively dereference address
pwndbg.string.get(addr, ...)        # returns a printable C-string from address
Select a repo