jak podmienić kod programu w locie bez własnego runtime

3

załóżmy że mam programa odpalanego "natywnie"?

generuje sobie executable przy użyciu gcc

#include <stdio.h>

int main(int argc, char ** argv)
{
    printf("Argument %i = %s\n", 1, argv[1] );
    return 0;
}

gcc programik.c

wx@xdddd:~/test# ./a.out 50
Argument 1 = 50

czy w tym momencie moim runtime jest OS?

jeżeli tak, to czy da się bez tworzenia własnego "vma" "runtime" zmieniać kod w locie takiego programu? tak jak JIT?

jak to robią normalne języki?

1

Program to tez tylko sekwencja bajtow, wiec jesli moglbys wziac i w RAMie podmienic wartosci to wykonaloby sie to co bys tam sobie wpisal.
Kwestia jest jeszcze taka, ze masz wirtualna przestrzen adresowa wiec tak na prawde jadro musialoby dac Ci dostep do tej pamieci.

EDIT: To co pisalem wyzej dotyczylo podmiany kodu innego procesu. Proces ma dostep do swojego kodu ale domyslnie jest chyba read-only (??)

3

Można stworzyć obszar pamięci, który jest jednocześnie zapisywalny i wykonywalny. Jednak np. macOS/AArch64 tego zabrania, przez co trzeba zmienić JITy:
https://openjdk.java.net/jeps/391

macOS/AArch64 forbids memory segments from being executable and writeable at the same time, a policy known as write-xor-execute (W^X). The HotSpot VM routinely creates and modifies executable code, so this JEP will implement W^X support in HotSpot for macOS/AArch64.

https://en.wikipedia.org/wiki/W%5EX

2
WeiXiao napisał(a):

jeżeli tak, to czy da się bez tworzenia własnego "vma" "runtime" zmieniać kod w locie takiego programu? tak jak JIT?

Tak, w końcu JIT dokładnie to robi. Możesz nadpisać istniejący kod, możesz zaalokować nowy obszar pamięci i tam wygenerować kod, a potem go wykonać. Ograniczają Cię jedynie uprawnienia od systemu operacyjnego i architektury komputera.

0

@Wibowit:

Można stworzyć obszar pamięci, który jest jednocześnie zapisywalny i wykonywalny.

no dobra, a masz jakiegoś hello worlda gdzie wrzucam sobie np. Kod A, a za 5min podmieniam funkcje w kodzie A na inną implementacje?

za cholerę nie mogę nigdzie znaleźć jak takie coś ma wyglądać

@Afish

da się to z poziomu C# w miarę sensownie zrobić (tzn. nie pisać C w C# i wszystko w unsafe scopie)?

Tak, w końcu JIT dokładnie to robi

Zawsze wydawało mi się że to dzięki CLR/JVM jest ta podmiana w locie

8
WeiXiao napisał(a):

da się to z poziomu C# w miarę sensownie zrobić (tzn. nie pisać C w C# i wszystko w unsafe scopie)?

Sensownie jak sensownie, musisz generować kod maszynowy. Nie trzeba do tego unsafe'a, ale to jest niebezpieczne z definicji, więc to tylko żonglowanie API. Zerknij na - w ostatnim demie dokładnie to robię, podmieniam kawałek kodu maszynowego, jakbyś chciał, to mógłbyś sobie napisać framework do robienia tego. Tak samo w https://blog.adamfurmanek.pl/2018/10/13/net-inside-out-part-9-generating-func-from-a-bunch-of-bytes-in-c-revisited/ po prostu generuję tablicę bajtów (która jest siłą rzeczy gdzieś w pamięci) a potem wykonuję te bajty jako kod maszynowy.

WeiXiao napisał(a):

Zawsze wydawało mi się że to dzięki CLR/JVM jest ta podmiana w locie

No ale CLR/JVM to nie jest jakaś tam magia, to tylko kawałek kodu maszynowego. Masz kupę bajtów, która generuje kolejną kupę bajtów w locie i następnie skacze do tych bajtów aby je wykonać.

1

W asemblerze można sobie ustawić uprawnienia dla konkretnego segmentu binarki: https://flatassembler.net/docs.php?article=manual#2.4

section '.text' code readable executable

Dopisujemy writeable i mamy komplet :) Z drugiej strony, nie jestem pewien czy coś takiego przejdzie.

0

@Afish:

FunctionInt function = FuncGenerator.Generate<FunctionInt>
(
	new byte[]{
		// Accessing variables on the stack directly instead via base pointer since we are too lazy to generate method frame
		0x8B, 0x44, 0x24, 0x04,         // mov eax, DWORD PTR [esp + 0x4]
		0x83, 0xC0, 0x04,   // add eax, 4
		0xc3                // retn
	}
);

noooooooooooooo, to jest porządny kawał 🍖🍖🍖🍖

czyli zatem jakbym sobie wziął jakiś intermediate representation, wykorzystał np. llvmowy toolchain do wygenerowania native codu i wrzucił go w byte[] i odpalił na innym wątku, to brzmiałoby to jak plan?

1
WeiXiao napisał(a):

czyli zatem jakbym sobie wziął jakiś intermediate representation, wykorzystał np. llvmowy toolchain do wygenerowania native codu i wrzucił go w byte[] i odpalił na innym wątku, to brzmiałoby to jak plan?

Koncepcyjnie tak. Technicznie — to zależy, co chcesz osiągnąć. Jeżeli chcesz to wołać bezpiecznie i używać obiektów z dotneta, to musisz uwzględnić konwencje wywołania, zwalnianie obiektów przez GC, spójność sterty i takie tam. Ale koncepcyjnie jak najbardziej o to chodzi.

0

W Javie są jakieś możliwości wyładować klasę, i załadować ponownie zmienioną.
Szczegółów nie znam.

1

@AnyKtokolwiek: Debugger takie coś chyba może w standardzie zrobić. Bez debuggera trzeba poczekać, aż klasa zostanie sprzątnięta przez GC, a potem można załadować od nowa zmienioną. Poza tym jest http://dcevm.github.io/ oraz GraalVM, gdzie można robić bardzo dużo za pomocą Java-on-Java, czyli espresso: https://github.com/oracle/graal/tree/master/espresso (GraalVM jest przystosowany do JITowania języków dynamicznie typowanych, a tych mechanizmów można użyć też w przypadku Javy, np. dodać czy usunąć metodę)

Zamiast robić własnego JITa to lepiej skorzystać z gotowca: https://github.com/oracle/graal/tree/master/truffle Wystarczy zaimplementować tłumaczenie ze swojego formatu na Truffle AST, a JITowaniem zajmie się Truffle + Graal. W gratisie, oprócz JITa, dostanie się GC, profilera, debuggera, integrację z innymi językami, w tym z biblioteką standardową Javy, itp itd

2

Bez debuggera i sztuczek to w jvm można też podmieniać klasy przy pomocy classloaderów ( hierarchii) - pokręcone platformy typu OSGi na tym bazują.

5

@WeiXiao bez własnego runtime możesz to zrobić generalnie na 2 sposoby:

  1. mprotect() żeby zrobić jakiś kawałek pamięci "wykonywalny" (albo jakieś gcc -z execstack ) i wtedy możesz do pamięci wrzucić shellcode i potem go wykonać
  2. ROP-chain, czyli skakanie po istniejących instrukcjach programu, tylko w nieoczekiwanej kolejności :)

W .NET można refleksją tworzyć sobie dynamicznie kod (rozwiązywałem kiedyś jakieś RE które tak robiło), w Javie można coś podobnego osiągnąć za pomocą Classloaderów.

0

Jeśli stworzysz kod, który daje się programować w różny sposób (wydaje mi się, ze assembler mógłby do tego być lepszy niż C, można go użyć w bardzo kreatywny sposób), to chyba już mówimy o implementacji VM. Tak więc zostaje mprotect() czy jakiś równoważnik, jeśli masz innego OSa niż *nix. Pozostaje znaleźć kompilator w formie biblioteki. Pamiętam, że był taki projekt, wydaje mi się, że to Unicorn: https://www.unicorn-engine.org/ Jednocześnie daje JIT, debuggowanie i dezasemblację.

0

@elwis: Ten unicorn engine jest oparty o Qemu, a jego JIT (pochodzący z Qemu właśnie) polega na przyspieszaniu emulacji jednej architektury procesora za pomocą drugiej. Wątpię by było to tutaj komuś potrzebne.

0

Może kogoś zainteresować Hello, JIT World: The Joy of Simple JITs

0
Wibowit napisał(a):

Można stworzyć obszar pamięci, który jest jednocześnie zapisywalny i wykonywalny.

To już odchodzi do przeszłości. Zmiany od dłuższego czasu idą w tym kierunku, że system nie pozwala, aby ta sama strona pamięci wirtualnej była z prawami do zapisu i wykonania.

W niektórych systemach jest to zaimplementowane i domyślnie włączone, w innych nie.

Zazwyczaj jednak nic nie stoi na przeszkodzie, aby ten sam obszar pamięci był dostępny pod jednym adresem z prawami do oczytu i wykonania, a pod innym z prawami do zapisu i wykonania.

Wibowit napisał(a):

Jednak np. macOS/AArch64 tego zabrania, przez co trzeba zmienić JITy:
https://openjdk.java.net/jeps/391

macOS/AArch64 forbids memory segments from being executable and writeable at the same time, a policy known as write-xor-execute (W^X). The HotSpot VM routinely creates and modifies executable code, so this JEP will implement W^X support in HotSpot for macOS/AArch64.

https://en.wikipedia.org/wiki/W%5EX

A także w wielu innych systemach, np. w OpenBSD, gdzie jest konfigurowalne przez flagę montowania danego filesystemu i domyślnie włączone poza /usr/local.

Shalom napisał(a):

@WeiXiao bez własnego runtime możesz to zrobić generalnie na 2 sposoby:

  1. mprotect() żeby zrobić jakiś kawałek pamięci "wykonywalny" (albo jakieś gcc -z execstack ) i wtedy możesz do pamięci wrzucić shellcode i potem go wykonać

Akurat tak tego nie należy robić, chyba że rzeczywiście to jest jakiś shellcode...

Jeden problem z tym rozwiązaniem jest taki, że jeśli zmieniasz prawa dostępu do strony pamięci za pomocą mprotect, to operujesz na całych stronach, bo tak działa zarządzanie pamięcią we współczesnych procesorach.

Zasadniczo nie należy dawać jednocześnie prawa do zapisu i wykonania, wiele systemów na to nie pozwoli. A jeśli system pozwoli, to takie prawa zmniejszają bezpieczeństwo rozwiązania.

Należy więc dać prawo do odczytu i wykonania. Ale tak, jak napisałem wcześniej, operujesz na całych stronach, więc jeśli w ten sposób zrobisz "jakiś kawałem pamięci" w ten sposób wykonywalny, to odbierzesz prawo do zapisu także do tego, co w tych samych stronach obok tego wykonywalnego kodu może się znajdować.

I przy próbie zapisu danych znajdujących się obok tego kodu dostaniesz np. SIGSEGV.

Drugi problem z tym rozwiązaniem jest taki, że jeśli w ten sposób chcesz zmodyfikować istniejący fragment kodu programu, to aby modyfikacja była możliwa, należy chociażby na chwilę dać prawo do zapisu tego obszaru pamięci. A system może nie pozwolić na tak szerokie uprawnienia (RWX), więc będzie trzeba na czas modyfikacji odebrać prawo do wykonania tego kodu.

A ponieważ ochrona pamięci operuje na stronach, odbierzesz w ten sposób prawo wykonywania kodu znajdującego się w tych samych stronach pamięci. Jeśli będzie tam kod wykonywany podczas modyfikacji (np. kod zajmujący się modyfikacją), to dostaniesz np. SIGSEGV.

1

Akurat mi się chciało. Program generuje i wykonuje funkcję dodającą dwa inty. Testowany na OpenBSD/amd64:


#include <sys/types.h>
#include <sys/mman.h>
#include <stdint.h>
#include <stdio.h>
#include <err.h>

#define ADDER_SIZE	4

typedef int adder(int, int);

static void gen_adder(adder *a)
{
	uint8_t *p = (uint8_t *)a;
	
	*p++ = 0x8d; /* lea (%rdi,%rsi,1),%eax */
	*p++ = 0x04;
	*p++ = 0x37;
	
	*p++ = 0xc3; /* retq */
}

static adder *make_adder(void)
{
	uint8_t *p;
	adder *a;
	
	a = mmap(NULL, ADDER_SIZE, PROT_READ | PROT_WRITE, MAP_ANON, -1, 0);
	if (a == MAP_FAILED)
		err(1, "mmap");
	
	gen_adder(a);
	
	if (mprotect(a, ADDER_SIZE, PROT_READ | PROT_EXEC))
		err(1, "mprotect");
	
	return a;
}

static void free_adder(adder *a)
{
	if (munmap(a, ADDER_SIZE))
		err(1, "munmap");
}

int main()
{
	static const int a = 2, b = 3;
	
	adder *func;
	
	func = make_adder();
	
	printf("%i + %i = %i\n", a, b, func(a, b));
	
	free_adder(func);
	return 0;
}

0

Zasadniczo nie należy dawać jednocześnie prawa do zapisu i wykonania, wiele systemów na to nie pozwoli. A jeśli system pozwoli, to takie prawa zmniejszają bezpieczeństwo rozwiązania.

Autor nie pytał o bezpieczeństwo takiego rozwiązania tylko o to czy się da. Wiadomo że możliwość odpalania shellcodu to jest krytyczna podatność i nic innego jak RCE ;)

0
1a2b3c4d5e napisał(a):

jak to robią normalne języki?

Shalom napisał(a):

Autor nie pytał o bezpieczeństwo takiego rozwiązania tylko o to czy się da. Wiadomo że możliwość odpalania shellcodu to jest krytyczna podatność i nic innego jak RCE ;)

"Normalne języki" raczej same z siebie nie eksploitują RCE :-P

1 użytkowników online, w tym zalogowanych: 0, gości: 1