Skip to content

Commit

Permalink
UEFI inter-module example (#54)
Browse files Browse the repository at this point in the history
* UEFI inter-module example (initial to-do commit)

* g

* added uefi emulation

* Amend the inter-example

---------

Co-authored-by: Maxhonin <[email protected]>
  • Loading branch information
FedorNiskov and Maxhonin authored May 29, 2024
1 parent 4139d13 commit 5e379c3
Show file tree
Hide file tree
Showing 10 changed files with 285 additions and 0 deletions.
99 changes: 99 additions & 0 deletions Examples/Crusher/Linux/UEFI/DualEmuInterMod/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
### Общее описание

Этот пример демонстрирует фаззинг кода UEFI,
где происходит взаимодействие между встроенными модулями.
Это осуществляется на основе частичной эмуляции с помощью Dual-Emu.

### Описание целевого кода

Целевой код - это фрагмент из модуля консоли UEFI,
который взаимодействует с модулем клавиатуры.
Исходный код - функция `ShellPromptForResponse`
в файле `ShellPkg/Library/UefiShellLib/UefiShellLib.c`
(https://github.com/tianocore/edk2).
Функция ожидает события клавиатуры (нажатие клавиши),
а затем обрабатывает их.
Начальная точка - вход в эту функцию,
конечная точка - возврат из неё.
Входные данные подаются целевому коду
через функцию `ReadKeyStroke` (API модуля клавиатуры).

### Сборка

Рекомендуется выполнять сборку и запуск на машине с Ubuntu 22.04.
После установки зависимостей и подготовки репозитория
(https://github.com/tianocore/edk2.git, версия `edk2-stable202311`)
нужно ввести команды сборки UEFI
и запуска в эмуляторе:
```
$ EmulatorPkg/build.sh
$ EmulatorPkg/build.sh run
```

Перед сборкой можно для удобства добавить в код печать адресов
интересующих функций (например, см. `patch.txt`).

После запуска нужно поставить точку останова на целевую функцию,
достичь её и сделать снимок состояния (регистры и память).
Т.е. вводятся следующие команды в консоль GDB:
```
run
...
<Ctrl+C>
break * &ShellPromptForResponse
continue
...
info registers
info proc mappings
// нужна память по адресам 0x4???????
dump memory mem.40000000 0x40000000 0x40020000
...
```

Чтобы достигнуть начальную точку, можно, например,
дождаться появления консоли UEFI
и ввести команду `mm 0x40000000`.

### Общая схема эмуляции и фаззинга

Здесь основой фаззинга является частичная эмуляция -
выполнение интересующего фрагмента кода из начального состояния
на основе скрипта пользователя.
Снимок начального состояния (регистры и память) сохранён в папке `dump`.
Этот снимок получен с помощью эмулятора.
Пользовательский скрипт `script.py` использует API Dual-Emu,
чтобы указать снимок, конечную точку, входные данные и запустить эмуляцию.
В данном примере целевая функция эмулируется с параметром `ShellPromptResponseTypeYesNoAllCancel`;
при эмуляции подменяются функции ввода-вывода, в том числе `ReadKeyStroke`,
чтобы ввести один символ и завершить ввод.
Пользователь может отдельно запустить эмуляцию (конкретное или символьное выполнение)
на некотором образце входных данных,
а также может запустить фаззинг с DSE.

### Запуск

Для запуска примера не требуется компиляция ПО и получение снимка -
в файлах примера уже присутствует готовый к запуску снимок и всё необходимое.

Рекомендации по подготовке Dual-Emu к запуску
можно посмотреть [здесь](../Common/README.md).

Запуск конкретного выполнения:

```
/path/to/python3 ./script.py --qiling -i ./input
```

Запуск символьного выполнения:

```
/path/to/python3 ./script.py --angr -i ./input -o ./out
```

Запуск фаззинга:

```
rm -rf out; /path/to/crusher/bin_x86-64/fuzz_manager --start 4 --eat-cores 1 --dse-cores 1 -i ./in -o ./out -I dualemu -T dualemu -t 5000 -- ./script.py
```


31 changes: 31 additions & 0 deletions Examples/Crusher/Linux/UEFI/DualEmuInterMod/dump/info.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"sections": {
"0x40000000": "mem.40000000",
"0x41000000": "mem.41000000",
"0x44000000": "mem.44000000",
"0x48000000": "mem.48000000"
},
"arch": "x86_64",
"os": "linux",
"rootfs": "/path/to/rootfs",
"regs": {
"rax": "0x0",
"rbx": "0x1",
"rcx": "0x2",
"rdx": "0x0",
"rsi": "0xff",
"rdi": "0x48d739e8",
"rbp": "0x44f90420",
"rsp": "0x44f90288",
"r8": "0x44f90300",
"r9": "0x0",
"r10": "0x8",
"r11": "0x246",
"r12": "0x0",
"r13": "0x0",
"r14": "0x0",
"r15": "0x40000003",
"rip": "0x48d0f479",
"eflags": "0x246"
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
1 change: 1 addition & 0 deletions Examples/Crusher/Linux/UEFI/DualEmuInterMod/in/seed
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
X
1 change: 1 addition & 0 deletions Examples/Crusher/Linux/UEFI/DualEmuInterMod/input
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
X
21 changes: 21 additions & 0 deletions Examples/Crusher/Linux/UEFI/DualEmuInterMod/patch.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
diff --git a/ShellPkg/Library/UefiShellLib/UefiShellLib.c b/ShellPkg/Library/UefiShellLib/UefiShellLib.c
index 746c9ccece..c42c54eb29 100644
--- a/ShellPkg/Library/UefiShellLib/UefiShellLib.c
+++ b/ShellPkg/Library/UefiShellLib/UefiShellLib.c
@@ -3528,6 +3528,16 @@ ShellPromptForResponse (
IN OUT VOID **Response OPTIONAL
)
{
+ static int first = 1;
+ if (first) {
+ typedef unsigned long long ull;
+ ShellPrintEx (-1, -1, L"\n");
+ ShellPrintEx (-1, -1, L"&ShellPromptForResponse = 0x%llX\n", (ull) &ShellPromptForResponse);
+ ShellPrintEx (-1, -1, L"gBS->WaitForEvent = 0x%llX\n", (ull) gBS->WaitForEvent);
+ ShellPrintEx (-1, -1, L"gST->ConIn->ReadKeyStroke = 0x%llX\n", (ull) gST->ConIn->ReadKeyStroke);
+ ShellPrintEx (-1, -1, L"ShellPrintEx = 0x%llX\n", (ull) &ShellPrintEx);
+ first = 0;
+ }
EFI_STATUS Status;
EFI_INPUT_KEY Key;
UINTN EventIndex;
132 changes: 132 additions & 0 deletions Examples/Crusher/Linux/UEFI/DualEmuInterMod/script.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
from pathlib import Path
import dual_emu
import os, sys
import json
import qiling

dump_path = Path(__file__).parent / 'dump' / 'info.json'
args = dual_emu.parse_args_cli(input_file_required=True)

arch_name = None
if args.qiling:
arch_name = "x8664"
if args.angr:
arch_name = "x86_64"
# set the "arch" field in the config
os.system(f'sed -i \' s/"arch":.*/"arch": "{arch_name}",/ \' dump/info.json')

orig_qiling = qiling.Qiling
def wrap_qiling(*args, **kw):
ql = orig_qiling(*args, **kw)
ql.mem.unmap_all() # to remove unneeded internal mappings
return ql
qiling.Qiling = wrap_qiling

# return address
ex = [0x48D3F48E]
emu = dual_emu.make_emulator_with_input(args.angr, args.input, dump_file=dump_path, exits=ex,
lighthouse_out_path=args.lighthouse)
if args.qiling:
emu.hook_fuzzer_start(emu.start_addr)

# NOTE: considering the UEFI calling convention and 2-byte chars

# at ShellPromptForResponse
def entry_hook(emu):
print('Entry Hook')
# set Type to ShellPromptResponseTypeYesNoAllCancel
emu.write_reg('rcx', 4)

emu.hook_addr(emu.start_addr, entry_hook)

# helper to return from a function
def retfun(emu, val=None):
sp = emu.read_reg_i("rsp")
ra = emu.read_mem_u64(sp)
if val != None:
emu.write_reg("rax", val)
emu.write_reg("rip", ra)
emu.write_reg("rsp", sp + 8)

# replacing WaitForEvent
def WaitForEvent(emu):
print("WaitForEvent")
# skipping this function
retfun(emu)

emu.hook_addr(0x44F9D2C3, WaitForEvent)

# replacing ReadKeyStroke
def ReadKeyStroke(emu):
# put the first input byte to Key.UnicodeChar and return EFI_SUCCESS
print("ReadKeyStroke")
ptr = emu.read_reg_i("rdx")
emu.cur_input.mark_symbolic_span(0, 1)
emu.write_mem_b(ptr+0x2, emu.cur_input.get_span(0, 1))
retfun(emu, 0)

emu.hook_addr(0x4407A669, ReadKeyStroke)

# replacing ShellPrintEx
def ShellPrintEx(emu):
# printing the given message to the screen
sys.stdout.write("ShellPrintEx: ")
fmt = emu.read_reg_i("r8")
arg = emu.read_reg_i("r9")
i = 0
while (True):
ch = emu.read_mem_u16(fmt + i)
if (ch == ord('\n')):
sys.stdout.write(" <\\n> ")
i += 2
elif (ch == ord('\r')):
sys.stdout.write(" <\\r> ")
i += 2
elif (ch == ord('%')):
if (nch := emu.read_mem_u16(fmt+i+2) == ord('c')):
sys.stdout.write(chr(arg))
i += 4
else:
sys.stdout.write(chr(ch))
i += 2
elif (ch == ord('\0')):
break
else:
sys.stdout.write(chr(ch))
i += 2
sys.stdout.write('\n')
retfun(emu)

emu.hook_addr(0x48D0C5A3, ShellPrintEx)

flag = False

# replacing ShellGetExecutionBreakFlag
def ShellGetExecutionBreakFlag(emu):
print("ShellGetExecutionBreakFlag")
# for the first time return false, for the next times return true
global flag
if (not flag):
flag = True
retfun(emu, 0)
else:
retfun(emu, 1)

emu.hook_addr(0x48D09C6E, ShellGetExecutionBreakFlag)

# replacing AllocateZeroPool
def AllocateZeroPool(emu):
print("AllocateZeroPool")
# return any unused address
retfun(emu, 0x40000000)

emu.hook_addr(0x48D09D3C, AllocateZeroPool)

emu.run()

if args.angr:
if emu.sim_man.errored:
print('Crashes:')
print(emu.sim_man.errored)

dual_emu.dump_new_inputs(emu, args.out_dir, clean=True)

0 comments on commit 5e379c3

Please sign in to comment.