Groups | Search | Server Info | Keyboard shortcuts | Login | Register [http] [https] [nntp] [nntps]


Groups > ger.ct > #552329 > unrolled thread

Onleihe

Started byBonita Montero <Bonita.Montero@gmail.com>
First post2022-04-21 07:36 +0200
Last post2022-05-27 19:39 +0200
Articles 15 — 4 participants

Back to article view | Back to ger.ct


Contents

  Onleihe Bonita Montero <Bonita.Montero@gmail.com> - 2022-04-21 07:36 +0200
    Re: Onleihe Lothar Kimmeringer <news201705@kimmeringer.de> - 2022-04-21 18:59 +0200
      Re: Onleihe Bonita Montero <Bonita.Montero@gmail.com> - 2022-04-22 05:13 +0200
        Re: Onleihe "Wendelin Uez" <wuez@online.de> - 2022-04-24 13:03 +0200
          Re: Onleihe Bonita Montero <Bonita.Montero@gmail.com> - 2022-04-24 17:24 +0200
            Re: Onleihe Dietz Proepper <dietz-usenet@rotfl.franken.de> - 2022-04-24 18:14 +0200
              Re: Onleihe Bonita Montero <Bonita.Montero@gmail.com> - 2022-04-24 19:01 +0200
                Re: Onleihe "Wendelin Uez" <wuez@online.de> - 2022-04-25 15:43 +0200
                  Re: Onleihe Bonita Montero <Bonita.Montero@gmail.com> - 2022-04-26 05:02 +0200
    Re: Onleihe "Wendelin Uez" <wuez@online.de> - 2022-04-21 15:02 +0200
    Re: Onleihe Bonita Montero <Bonita.Montero@gmail.com> - 2022-05-18 08:30 +0200
      Es wird noch verrückter ... Bonita Montero <Bonita.Montero@gmail.com> - 2022-05-23 13:07 +0200
        Und noch einen oben drauf Bonita Montero <Bonita.Montero@gmail.com> - 2022-05-27 10:11 +0200
          Re: Und noch einen oben drauf "Wendelin Uez" <wuez@online.de> - 2022-05-27 12:11 +0200
            Re: Und noch einen oben drauf Bonita Montero <Bonita.Montero@gmail.com> - 2022-05-27 19:39 +0200

#552329 — Onleihe

FromBonita Montero <Bonita.Montero@gmail.com>
Date2022-04-21 07:36 +0200
SubjectOnleihe
Message-ID<t3qqfg$2lh$1@dont-email.me>
Ich hab mich gestern hier bei der Stadtbibliothek mal angemeldet,
eigentlich nur um Bücher, Zeitschriften und Zeitungen online ausleihen
zu können. Die Online-Bücher kann man über eine Seite namens Onleihe
([1]) herunterladen und das ist eine Seite von irgendeiner Firma, die
die Technik für Bibliotheken in verschiedenen Ländern Europas managt.
Auf jeden Fall sind die heruntergeladenen Bücher und Zeitschriften
zeitlich befristet und lassen sich danach nicht mehr mit Adobe Digital
Editions (schreckliches Programm) öffnen.
Wenn ich versuche, ein PDF mit Calibre zu öffnen, dann scheitert das
trotz installiertem De-DRM Plugin. E-Books die ich bei buecher.de
gekauft habe lassen sich damit aber einwandfrei entschlüsseln. Ich
weiß nicht, ob es am De-DRM-Plugin oder Calibre selbst eine Funktion
zum Entschlüsseln von EPUBs hat, aber diese EPUBs aus dem Ordner "My
Digital Edidions" lassen sich problemlos mit Calibre öffnen.
Hat es hier irgendwie schon mal jemand geschafft, ein Onleihe-PDF
zu entschlüsseln ?

[1] https://de.wikipedia.org/wiki/Onleihe

[toc] | [next] | [standalone]


#552363

FromLothar Kimmeringer <news201705@kimmeringer.de>
Date2022-04-21 18:59 +0200
Message-ID<j83hmbp0nb5e.dlg@kimmeringer.de>
In reply to#552329
Bonita Montero wrote:

> Hat es hier irgendwie schon mal jemand geschafft, ein Onleihe-PDF
> zu entschlüsseln ?

Das zu tun ist genauso legal, wie sich das Zeug von den
einschlaegigen Quellen im Internet zu ziehen. Letzteres
duerfte aber deutlich unkomplizierter sein und du spielst
der Initiative Fair Lesen nicht in die Haende.

Dies ist keine Aufforderung, nur eine Feststellung.


Gruesse, Lothar
-- 
Lothar Kimmeringer                E-Mail: spamfang@kimmeringer.de
               PGP-encrypted mails preferred (Key-ID: 0x8BC3CD81)

Always remember: The answer is forty-two, there can only be wrong
                 questions!

[toc] | [prev] | [next] | [standalone]


#552382

FromBonita Montero <Bonita.Montero@gmail.com>
Date2022-04-22 05:13 +0200
Message-ID<t3t6fe$kjm$4@dont-email.me>
In reply to#552363
Am 21.04.2022 um 18:59 schrieb Lothar Kimmeringer:
> Bonita Montero wrote:
> 
>> Hat es hier irgendwie schon mal jemand geschafft, ein Onleihe-PDF
>> zu entschlüsseln ?
> 
> Das zu tun ist genauso legal, wie sich das Zeug von den
> einschlaegigen Quellen im Internet zu ziehen. ...

Das ist mir komplett egal.

> Letzteres duerfte aber deutlich unkomplizierter sein und
> du spielst der Initiative Fair Lesen nicht in die Haende.

Das geht selten, denn wenn ich z.B. auf boerse.im schaue, dann
ist das meiste was dort verlinkt ist schon wieder wg. Copyright
Beschwerden offline.

> Dies ist keine Aufforderung, nur eine Feststellung.

Idiot.

[toc] | [prev] | [next] | [standalone]


#552595

From"Wendelin Uez" <wuez@online.de>
Date2022-04-24 13:03 +0200
Message-ID<t43kuc$bsd$2@dont-email.me>
In reply to#552382
> Idiot.

Für einen Idioten würde ich angesichts deiner unzureichenden Recherche eher 
jemanden halten, der öffentlich nach illegalren Praktiken fragt und mit 
korrekter detaillierter Antwort nicht klar kommt. 

[toc] | [prev] | [next] | [standalone]


#552604

FromBonita Montero <Bonita.Montero@gmail.com>
Date2022-04-24 17:24 +0200
Message-ID<t43q1s$igu$1@dont-email.me>
In reply to#552595
Am 24.04.2022 um 13:03 schrieb Wendelin Uez:
>> Idiot.
> 
> Für einen Idioten würde ich angesichts deiner unzureichenden Recherche 
> eher jemanden halten, der öffentlich nach illegalren Praktiken fragt ...

Ach, meinste da kommt die Polizei herbei ?
Ich mein Urheberrechtsdelikte sind Antragsdelikte, da müsste irgendein
Rechteinhaber kommen und sagen, "der hat was von mir kopiert" oder "der
hat meinen Content verteilt". Ist ja so erstmal nicht so ohne weiteres
möglich.

>... und mit korrekter detaillierter Antwort nicht klar kommt.

An der Stelle wo ich mit Idiot geantwortet habe war keine detailierte
Antwort.

[toc] | [prev] | [next] | [standalone]


#552611

FromDietz Proepper <dietz-usenet@rotfl.franken.de>
Date2022-04-24 18:14 +0200
Message-ID<20220424181455.6c761db8.dietz-usenet@rotfl.franken.de>
In reply to#552604
Bonita Montero <Bonita.Montero@gmail.com> wrote:

> Am 24.04.2022 um 13:03 schrieb Wendelin Uez:
> >> Idiot.  
> > 
> > Für einen Idioten würde ich angesichts deiner unzureichenden
> > Recherche eher jemanden halten, der öffentlich nach illegalren
> > Praktiken fragt ...  
> 
> Ach, meinste da kommt die Polizei herbei ?

Nicht deswegen. Aber gegen Beifang hat ggf. niemand was.

-- 
 SIC SEMPER
+--|=======>
  TYRANNIS

[toc] | [prev] | [next] | [standalone]


#552615

FromBonita Montero <Bonita.Montero@gmail.com>
Date2022-04-24 19:01 +0200
Message-ID<t43vo1$tg$1@dont-email.me>
In reply to#552611
Am 24.04.2022 um 18:14 schrieb Dietz Proepper:
> Bonita Montero <Bonita.Montero@gmail.com> wrote:
> 
>> Am 24.04.2022 um 13:03 schrieb Wendelin Uez:
>>>> Idiot.
>>>
>>> Für einen Idioten würde ich angesichts deiner unzureichenden
>>> Recherche eher jemanden halten, der öffentlich nach illegalren
>>> Praktiken fragt ...
>>
>> Ach, meinste da kommt die Polizei herbei ?
> 
> Nicht deswegen. Aber gegen Beifang hat ggf. niemand was.

Wieso Beifang ? Nochmal: das ist ein Antrags-Delikt, d.h. das wird nicht
von Staats wegen allein verfolgt und ein Rechteinhaber muss hier seine
Rechte geltend machen, und das kann er nicht weil er nicht weiß ob er
betroffen ist.

[toc] | [prev] | [next] | [standalone]


#552692

From"Wendelin Uez" <wuez@online.de>
Date2022-04-25 15:43 +0200
Message-ID<t46m89$on5$2@dont-email.me>
In reply to#552615
> Nochmal: das ist ein Antrags-Delikt, d.h. das wird nicht
> von Staats wegen allein verfolgt und ein Rechteinhaber muss hier seine
> Rechte geltend machen, und das kann er nicht weil er nicht weiß ob er
> betroffen ist.

Deine Art der Argumentation ist generall sozialschädlich aka asozial, denn 
sie beruht erkennbar nicht darauf, Recht von Unrecht zu unterscheiden, 
sondern Unrecht durchzusetzen auf Kosten anderer, die keinen solchen 
Rechtsanspruch geltend machen können.

Die fehlende rechtliche Verfolgungsmöglichkeit ist übrigens kein Friebrief, 
Verbotenes trotzdem tun zu dürfen.

Im übrigen gibt es außerdem auch Dinge, die "tut man nicht", auch wenn sie 
ungestraft bleiben würden.

Deine Rechthaberei hat dieselbe Qualität wie die, einem Blinden das Geld aus 
der hingehaltenen Mütze zu nehmen, der kann sich auch nicht wehren. Trotzdem 
ist das weder rechtens noch zulässig.

[toc] | [prev] | [next] | [standalone]


#552731

FromBonita Montero <Bonita.Montero@gmail.com>
Date2022-04-26 05:02 +0200
Message-ID<t47nc6$aiv$1@dont-email.me>
In reply to#552692
> Deine Art der Argumentation ist generall sozialschädlich aka asozial, 
> denn sie beruht erkennbar nicht darauf, Recht von Unrecht zu 
> unterscheiden, sondern Unrecht durchzusetzen auf Kosten anderer, die 
> keinen solchen Rechtsanspruch geltend machen können.

Dir geht es nicht um Moral, sondern um anderen ein Stigma anzuhängen.
Wer hier asozial ist, das bist Du.

[toc] | [prev] | [next] | [standalone]


#552366

From"Wendelin Uez" <wuez@online.de>
Date2022-04-21 15:02 +0200
Message-ID<t3sapc$s14$1@dont-email.me>
In reply to#552329
Das DRM-AddOn eines Drittherstellers zur Entfernung der Entschlüsselung 
funktioniert mit der neueren PDF-Verschlüsselung nicht mehr, fürher konnte 
man damit eine unverschlüsselte Kopie erzeugen. 

[toc] | [prev] | [next] | [standalone]


#555049

FromBonita Montero <Bonita.Montero@gmail.com>
Date2022-05-18 08:30 +0200
Message-ID<t623pc$45g$1@dont-email.me>
In reply to#552329
Auf jeden Fall habe ich mal angenommen, dass Adobe Digital Editions
das PDF im Prozess-Adressraum liegen hat und ich es aus diesem aus-
lesen könnte. Da hab ich mir mal ein kleines Windows-Programm in
C++20 geschrieben das den Prozess-Adressraum nach einem Speicherblock
durchsucht der einen PDF-Header ("%PDF-*.*" - * steht für Versions-
nummer-Anteile) sowie einen PDF-Tailer ("%%EOF") enthält. Eigentlich
ganz einfach und hier ist der Code:

#include <Windows.h>
#include <iostream>
#include <vector>
#include <charconv>
#include <cstring>
#include <vector>
#include <stdexcept>
#include <sstream>
#include <cctype>
#include <fstream>

using namespace std;

template<typename FilterFn>
	requires requires( FilterFn fn, MEMORY_BASIC_INFORMATION &mbi ) { { fn( 
mbi ) } -> std::convertible_to<bool>; }
vector<vector<MEMORY_BASIC_INFORMATION>> pageTree( HANDLE hProcess, 
FilterFn filterFn );
void savePdf( char const *header, size_t nBytes );

using XHANDLE = unique_ptr<void, decltype([]( HANDLE h ) { h && h != 
INVALID_HANDLE_VALUE && CloseHandle( h ); })>;

int main( int argc, char **argv )
{
	if( argc < 2 )
		return EXIT_FAILURE;
	try
	{
		DWORD dwProcessId = [&]() -> DWORD
			{
				DWORD dwRet;
				if( from_chars_result fcr = from_chars( argv[1], argv[1] + strlen( 
argv[1] ), dwRet ); fcr.ec != errc() || *fcr.ptr )
					throw invalid_argument( "process-id unparseable" );
				return dwRet;
			}();
		XHANDLE hProcess( [&]() -> HANDLE
			{
				HANDLE hRet = OpenProcess( PROCESS_QUERY_INFORMATION | 
PROCESS_VM_READ, FALSE, dwProcessId );
				if( !hRet )
					throw system_error( (int)GetLastError(), system_category(), "can't 
open process" );
				return hRet;
			}() );
		vector<vector<MEMORY_BASIC_INFORMATION>> vvmbi = pageTree( 
hProcess.get(), []( MEMORY_BASIC_INFORMATION &mbi ) -> bool
			{
				return mbi.State == MEM_COMMIT;
			} );
		vector<char> processRegion;
		for( vector<MEMORY_BASIC_INFORMATION> const &vmbi : vvmbi )
			for( MEMORY_BASIC_INFORMATION const &vmbi : vmbi )
			{
				processRegion.resize( vmbi.RegionSize );
				if( !ReadProcessMemory( hProcess.get(), vmbi.BaseAddress, 
to_address( processRegion.begin() ), vmbi.RegionSize, nullptr ) )
					continue;
				char const *end;
				auto detectBegin = [&]( char const *p ) -> char const *
				{
					if( end - p < 8 )
						return nullptr;
					if( memcmp( p, "%PDF-", 5 ) != 0 )
						return p;
					if( !isdigit( p[5] ) || p[6] != '.' || !isdigit( p[7] ) )
						return p;
					return p + 8;
				};
				auto detectEnd = [&]( char const *p ) -> char const *
				{
					if( end - p < 5 )
						return nullptr;
					if( memcmp( p, "%%EOF", 5 ) != 0 )
						return p;
					return p + 5;
				};
				char const
					*scn = to_address( processRegion.begin() ),
					*next, *header;
				end = scn + processRegion.size();
				while( scn != end )
				{
					next = detectBegin( scn );
					if( !next )
						goto nextRegion;
					if( next == scn )
					{
						++scn;
						continue;
					}
					header = scn;
					for( scn = next; scn != end; ++scn )
					{
						next = detectEnd( scn );
						if( !next )
							goto nextRegion;
						if( next != scn )
						{
							savePdf( header, next - header );
							scn = next;
							break;
						}
					}
				}
			nextRegion:;
			}
	}
	catch( exception const &exc )
	{
		cout << exc.what() << endl;
	}
}

void savePdf( char const *header, size_t nBytes )
{
	string strFileName;
	for( unsigned iFileNo = 0; ; )
	{
		strFileName = (ostringstream() << iFileNo << ".pdf").str();
		using FIND_HANDLE = unique_ptr<void, decltype([]( HANDLE h ) { h && h 
!= INVALID_HANDLE_VALUE && FindClose( h ); })>;
		WIN32_FIND_DATAA fd;
		FIND_HANDLE fh( FindFirstFileA( strFileName.c_str(), &fd ) );
		if( fh.get() == INVALID_HANDLE_VALUE )
			if( DWORD dwErr = GetLastError(); dwErr == ERROR_FILE_NOT_FOUND )
				break;
			else
				throw system_error( (int)dwErr, system_category(), "error 
enumerating files to write" );
		if( !++iFileNo )
			return;
	}
	ofstream ofs;
	ofs.exceptions( ofstream::failbit | ofstream::badbit );
	ofs.open( strFileName.c_str(), ofstream::out | ofstream::trunc | 
ofstream::binary );
	ofs.write( header, nBytes );
}

template<typename Fn>
	requires requires( Fn fn, MEMORY_BASIC_INFORMATION &mbi ) { { fn( mbi ) 
} -> std::convertible_to<bool>; }
void enumProcessMemory( HANDLE hProcess, Fn fn );

template<typename FilterFn>
	requires requires( FilterFn fn, MEMORY_BASIC_INFORMATION &mbi ) { { fn( 
mbi ) } -> std::convertible_to<bool>; }
vector<vector<MEMORY_BASIC_INFORMATION>> pageTree( HANDLE hProcess, 
FilterFn filterFn )
{
	vector<vector<MEMORY_BASIC_INFORMATION>> vvmbis;
	enumProcessMemory( hProcess, [&]( MEMORY_BASIC_INFORMATION &mbi ) -> bool
		{
			if( !filterFn( mbi ) )
				return true;
			if( !vvmbis.size() || vvmbis.back().back().BaseAddress != 
mbi.BaseAddress )
				vvmbis.emplace_back( vector<MEMORY_BASIC_INFORMATION>() );
			vvmbis.back().emplace_back( mbi );
			return true;
		} );
	return vvmbis;
}

template<typename Fn>
	requires requires( Fn fn, MEMORY_BASIC_INFORMATION &mbi ) { { fn( mbi ) 
} -> std::convertible_to<bool>; }
void enumProcessMemory( HANDLE hProcess, Fn fn )
{
	MEMORY_BASIC_INFORMATION mbi;
	for( char *last = nullptr; ; last = (char *)mbi.BaseAddress + 
mbi.RegionSize )
	{
		size_t nBytes = VirtualQueryEx( hProcess, last, &mbi, sizeof mbi );
		if( nBytes != sizeof mbi )
			if( DWORD dwErr = GetLastError(); dwErr == ERROR_INVALID_PARAMETER )
				break;
			else
				throw system_error( (int)dwErr, system_category(), "can't query 
process pages" );
		if( !fn( mbi ) )
			break;
	}
}

Mit verschiedenen PDF-Readern funktioniert das auch einwandfrei und
der Code speichert alle in dem Prozess-Adressraum gefundenen PDF ab
0.pdf durchnummeriert und überschreibt dabei keine exisitierenden
Dateien.
Dummerweise funktoniert das nicht mit Adobe Digital Editions bzw.
ich vermute, dass die das PDF verschlüsselt im RAM halten und on
the fly entschlüsseln.

[toc] | [prev] | [next] | [standalone]


#555690 — Es wird noch verrückter ...

FromBonita Montero <Bonita.Montero@gmail.com>
Date2022-05-23 13:07 +0200
SubjectEs wird noch verrückter ...
Message-ID<t6fpt3$hq0$1@dont-email.me>
In reply to#555049
Da die letzte idee mit dem Durchscannen der Prozesse nicht funktioniert
hat ist mir nochwas durch den Kopf gegangen:
Speicherverwaltung bei heutiger Software funktioniert so, dass Speicher-
anforderungen wie malloc() in C oder new in C++ aus einem Pool von Spei-
cher im Userland bedient wird und wenn der keine passende Blockgröße
abgibt, dann wird der Kernel nach einem größeren Block gefragt der den
Pool dann erweitert. Das gilt dann für kleinere oder mittelgroße Alloka-
tionen. Große Allokationen gehen immer an den Kernel und werden nie
direkt an den Pool, sondern auch an den Kernel, zurückgegeben damit
sich der Verbrauch an Speicher in Grenzen hält.
Meine Idee war jetzt: wenn Digital Editions (DE) das PDF in den Speicher
lädt, dann sicher erstmal in einen größeren Block, der bei der Freigabe
garantiert an den Kernel zurückgeht. Das Dumme wäre, wenn DE den Block
vor der Freigabe beliebig überschreibt. So ein simples memset() kann
man im Binary nämlich oft gar nicht so leicht ausmachen unter vielen
anderen memset()-Aufrufen, und da die Funktion bei eigentlich allen
Compilern intrinsisch ist kann die gar komlpet ge-inline-t sein.
Ich hatte halt angenommen, das vielleicht eine Chance besteht, dass das
nicht so ist. Dann müsste ich nur den Aufruf von VirtualFree(), das ist
die Funktion um unter Windows virtuellen Speicher wieder freizugeben,
also sowas wie ähnlichesmunmap(), extern trappen.
Es gibt unter Windows eine recht komfortable Debugging API mit dem man
den Ablauf einer Anwendung nachverfolgen kann. Man kann Events darüber
empfangen wenn der Prozess kreiert oder beendet wurde, wenn ein Thread
gestartet oder beendet wurde, wenn eine DLL geladen oder entladen wurde,
wenn eine sog SEH-Exception generiert wurde - SEH Exceptions ersetzen
sind das selbe wie Signale unter Unix, aber nur die synchronen davon,
also die die selbst aus dem Instruktions-Strom kommen wie SIGSEGV oder
SIGFPE (das aber 10 mal geiler als unter Unix) oder auch für für mich
so unwichtige Dinge wie, wenn ein Debug-String ausgegeben werden soll.
Das Trappen eines DLL-Einstiegspunkts muss man selbst übernehmen indem
man beim Laden einer DLL einfach einen Breakpoint auf den Einstiegs-
punkt setzt. Für den Breakpoint gibt es bei x86 eine spezielle Instruk-
tion, den Interrupt 3 (INT3), der nur einen Byte lang ist und den Opcode
$CC hat.
Das Trappen ist dann aber nicht so einfach, denn wenn ich das trappe,
dann muss ich das Programm dann wieder weiterlaufen lassen, also den
Trap wegnehmen. Wenn ich das dann aber mache, dann können andere Threads
unbemerkt die vormals getrappte Funktion nutzen. Also mache ich folgen-
des: ich protokolliere das Kreieren aller Threads in einer Hash-Map
die die Thread-ID - die später Teil jeder der Debug-Benachrichtigung,
auch der über den Trap - und lege alle Threads bis auf den wo der Trap
passiert ist nach dem Wegnehmen des Traps schlafen und setze, bevor
ich den Prozess mit nur einem laufenden Thread wieder losrennen lasse
einen Trap (INT3) auf die Rücksprungsadrese des der Funktion. Teil
des ersten Trap ist nämlich eine Kontext-Datenstruktur wo ich die
Register sehe, also unter anderem auch den Stack-Pointer, dass ich
die Rücksprung-Adresse auf dem Stack auslesen kann. Habe ich einen
Trap auf der RÜcksprungadresse, dann schalte ich diesen aus, den
ersten wieder an, mache wieder alle anderen Threads lauffähig und
warte auf den nächsten Trap auf den Einstiegspunkt auf die überwachte
Funktion.
Aus dem Stack kann ich beim ersten der beiden Traps dann noch die
Paramter für VirtualFree() auslesen, unter anderem auch die Adresse
des Speicher-Blocks. Anders als bei munmap() kriegt diese Funktion
keine Länge mit bzw. der Kernel weiß selbst, wie lange ein reservierter
Bereich ist. Innerhalb eines solchen reservierten Bereichs können die
Pages unterschiedlich belegt sein. Die können auf gar keinen zugreif-
baren Speicher gemappt sein, auf nur lesbaren, nur les und schreibbaren,
ausführbaren und was weiß ich noch alles (mehr steht in der Doku zu
VirtualAlloc()). Daher muss ich mit VirtualQueryEx() diese reservierte
Range nach lesbaren Bereichen durchlaufen.
Jedes dieser reservierten Bereiche durchsuche ich dann nach etwas, das
nach einem PDF aussieht, d.h. einen Header von %PDF*.* (* wg. Version)
und einen Tailer von %%EOF hat.
Das Ganze würde ich ja speichern, aber ich hab den Debugger mit mit
meinem kleinen eigenen "Debugger" angeworfen und der Suchlauf hat
leider bei DE rein gar nichts gefunden, dass ich dann erst gar keine
Speicher-Funktion geschrieben habe.
Also ein recht trickreiches Programm für _NIX_.
Eine kleine Sache war auch noch nicht unkompliziert: ich bekomme ja
Benachrichtigungen für das Laden und Entladen von DLLs, und was ich
dann benötige ist der relative Offset des Einstiegspunkts in der DLL.
Da mein Programm die DLL aber nicht laden kann weil es selbst ein
64 Bit programm ist, DE aber ein 32 Bit Programm, habe ich mir ein
kleines 32-bittiges Hilfs-Programm geschrieben, dass die selbe DLL
(kernel32.dll in dem Fall) lädt und den relativen Offset des getrapp-
ten Einstiegspunkt als Rückgaberwer ausgibt.

Ich fühl mich in letzter Zeit irgendwi mental immer fitter, dass mir
sowas mega Spaß macht und ich keinerlei Probleme mit der Kondition
habe. Ich habe gestern 14 Stunden am Stück programmiert und war
danach noch super-fit.

Hier der Code ohne das besafte Hilfs-Programm:

#include <Windows.h>
#include <tlhelp32.h>
#include <iostream>
#include <cstring>
#include <stdexcept>
#include <system_error>
#include <thread>
#include <unordered_map>
#include <algorithm>
#include <atomic>
#include <utility>
#include <cassert>
#include <iomanip>

#if defined(_MSC_VER)
	#pragma warning(disable: 6335)
	// disable pointer narrowing / widening warnings
	#pragma warning(disable: 4302)
	#pragma warning(disable: 4312)
	#pragma warning(disable: 4311)
#endif

using namespace std;

using XHANDLE = unique_ptr<void, decltype([]( void *h ) { h && h != 
INVALID_HANDLE_VALUE && CloseHandle( (HANDLE)h ); })>;

[[noreturn]]
void throwSysErr( char const *str );
size_t getProcOffset( char const *dll, char const *exp );
using rpm_it = typename vector<char>::const_iterator;
size_t getPageSize();
template<typename Pointer>
	requires is_pointer_v<Pointer>
Pointer curPage( Pointer pointer, size_t pageSize );
void hookVirtualFree( HANDLE hProcess, LPVOID lpAddress, DWORD dwSize, 
DWORD dwFreeType );

int wmain( int argc, wchar_t *argv[] )
{
	if( argc < 2 )
		return EXIT_FAILURE;
	PROCESS_INFORMATION pi;
	auto terminate = [&]()
	{
		TerminateProcess( pi.hProcess, 0 );
		ExitProcess( EXIT_FAILURE );
	};
	try
	{
		size_t
			offsetFree( getProcOffset( "msvcrt.dll", "free" ) ),
			offsetVirtualFree( getProcOffset( "kernel32.dll", "VirtualFree" ) );
		wstring commandLine;
		for( size_t i = 1; i != (unsigned)argc; ++i )
			commandLine += i >= 2 ? L" \"" : L"\"",
			commandLine += argv[i],
			commandLine += L"\"";
		STARTUPINFOW si;
		memset( &si, 0, sizeof si );
		si.cb = sizeof si;
		if( !CreateProcess( nullptr, (LPWSTR)commandLine.c_str(), nullptr, 
nullptr, FALSE, CREATE_SUSPENDED, nullptr, nullptr, &si, &pi) )
			throwSysErr( "can't create process" );
		DWORD dwProcessId = GetProcessId( pi.hProcess );
		if( !dwProcessId )
			throw exception( "can't get process id" );
		if( !DebugActiveProcess( dwProcessId ) )
			throwSysErr( "can't enable debugging for process" );
		if( ResumeThread( pi.hThread ) == -1 )
			throwSysErr( "can't resume main thread" );
		using dll_map = unordered_map<LPVOID, HANDLE>;
		using dll_map_it = typename dll_map::iterator;
		using threads_map = unordered_map<DWORD, HANDLE>;
		dll_map dllMap;
		threads_map threadMap;
		DEBUG_EVENT de;
		auto resume = [&]( DWORD dwContinueStatus = DBG_CONTINUE )
		{
			if( !ContinueDebugEvent( de.dwProcessId, de.dwThreadId, 
dwContinueStatus ) )
				terminate();
		};
		DWORD dwUniqueThreadId;
		auto stopExceptUnique = [&]( DWORD dwUniqueId )
		{
			for( pair<DWORD const, HANDLE> const &thr : threadMap )
				if( thr.first != dwUniqueId )
					if( SuspendThread( thr.second ) == -1 )
						terminate();
			dwUniqueThreadId = dwUniqueId;
		};
		auto resumeExceptUnique = [&]()
		{
			for( pair<DWORD const, HANDLE> const &thr : threadMap )
				if( thr.first != dwUniqueThreadId )
					if( ResumeThread( thr.second ) == -1 )
						terminate();
		};
		auto addThread = [&]()
		{
			constexpr DWORD THREAD_ACCESS = THREAD_GET_CONTEXT | 
THREAD_SET_CONTEXT | THREAD_SUSPEND_RESUME;
			HANDLE hThread = OpenThread( THREAD_ACCESS, FALSE, de.dwThreadId );
			if( !hThread || !threadMap.emplace( de.dwThreadId, hThread ).second )
				terminate();
		};
		char breakpointBeforeContent;
		void *breakpointAddr = nullptr;
		auto trap = [&]( void *int3Addr )
		{
			static char const int3 = (char)0xCC;
			SIZE_T bytes;
			if( !ReadProcessMemory( pi.hProcess, int3Addr, 
&breakpointBeforeContent, 1, &bytes ) || bytes != 1 )
				terminate();
			breakpointAddr = int3Addr;
			if( !WriteProcessMemory( pi.hProcess, int3Addr, &int3, 1, &bytes ) || 
bytes != 1 )
				terminate();
		};
		auto untrap = [&]()
		{
			SIZE_T actually;
			if( !WriteProcessMemory( pi.hProcess, breakpointAddr, 
&breakpointBeforeContent, 1, &actually ) || actually != 1 )
				terminate();
			breakpointAddr = nullptr;
		};
		char *virtualFreeAddr = nullptr;
		while( WaitForDebugEventEx( &de, INFINITE ) )
			switch( de.dwDebugEventCode )
			{
			case CREATE_PROCESS_DEBUG_EVENT:
				{
					cout << "process created (PID: " << de.dwProcessId << ", TID: " << 
de.dwThreadId << ")" << endl;
					addThread();
					resume();
					break;
				}
			case CREATE_THREAD_DEBUG_EVENT:
				{
					cout << "thread created (TID: " << dec << de.dwThreadId << ")" << endl;
					addThread();
					resume();
					break;
				}
			case EXIT_THREAD_DEBUG_EVENT:
				{
					cout << "thread terminated: (TID: " << dec << de.dwThreadId << ")" 
<< endl;
					threads_map::iterator itThread = threadMap.find( de.dwThreadId );
					if( itThread == threadMap.end() || !CloseHandle( itThread->second ) )
						terminate;
					threadMap.erase( itThread );
					resume();
					break;
				}
			case EXCEPTION_DEBUG_EVENT:
				{
					typename threads_map::iterator itThread = threadMap.find( 
de.dwThreadId );
					if( itThread == threadMap.end() )
						terminate();
					WOW64_CONTEXT context;
					memset( &context, 0, sizeof context );
					context.ContextFlags = WOW64_CONTEXT_ALL;
					if( !Wow64GetThreadContext( itThread->second, &context ) )
						terminate();
					cout << "exception (code: " << hex << setw( 8 ) << setfill( '0' ) 
<< de.u.Exception.ExceptionRecord.ExceptionCode << ")" << endl;
					if( context.Eip - 1 == (DWORD)breakpointAddr )
					{
						auto untrapAndBefore = [&]()
						{
							untrap();
							--context.Eip;
							if( !Wow64SetThreadContext( itThread->second, &context ) )
								terminate();
						};
						if( context.Eip - 1 == (DWORD)virtualFreeAddr )
						{
							cout << "\tVirtualFree() trapped" << endl;
							untrapAndBefore();
							DWORD aDwStack[4];
							if( SIZE_T read; !ReadProcessMemory( pi.hProcess, (void 
*)context.Esp, aDwStack, sizeof aDwStack, &read ) || read != sizeof 
aDwStack )
								terminate();
							void *returnAddr = (void *)aDwStack[0];
							LPVOID lpAddress = (LPVOID)aDwStack[1];
							DWORD dwSize = aDwStack[2], dwFreeType = aDwStack[3];
							trap( returnAddr );
							stopExceptUnique( de.dwThreadId );
							hookVirtualFree( pi.hProcess, lpAddress, dwSize, dwFreeType );
						}
						else
						{
							cout << "\treturn of VirtualFree() trapped" << endl;
							untrapAndBefore();
							trap( virtualFreeAddr );
							resumeExceptUnique();
						}
						resume( DBG_EXCEPTION_HANDLED );
					}
					else
						resume( DBG_EXCEPTION_NOT_HANDLED );
					break;
				}
			case EXIT_PROCESS_DEBUG_EVENT:
				cout << "process exited" << endl;
				terminate();
				break;
			case LOAD_DLL_DEBUG_EVENT:
				{
					constexpr size_t PATH_LENGTH = 256;
					wchar_t dllPath[PATH_LENGTH];
					DWORD dwRet = GetFinalPathNameByHandleW( de.u.LoadDll.hFile, 
dllPath, PATH_LENGTH, 0 );
					if( !dwRet || dwRet > PATH_LENGTH - 1 )
						terminate();
					wcout << L"DLL loaded: " << dllPath << endl;
					char *dllBase = (char *)de.u.LoadDll.lpBaseOfDll;
					if( !dllMap.emplace( dllBase, de.u.LoadDll.hFile ).second )
						terminate();
					constexpr wchar_t const *KERNEL32_PATH = 
L"\\\\?\\C:\\Windows\\SysWOW64\\kernel32.dll";
					if( _wcsicmp( dllPath, KERNEL32_PATH ) == 0 )
					{
						virtualFreeAddr = dllBase + offsetVirtualFree;
						trap( virtualFreeAddr );
					}
					resume();
					break;
				}
			case UNLOAD_DLL_DEBUG_EVENT:
				{
					cout << "DLL unloaded " << endl;
					dll_map_it itDll = dllMap.find( de.u.UnloadDll.lpBaseOfDll );
					if( itDll == dllMap.end() || !CloseHandle( itDll->second ) )
						terminate();
					dllMap.erase( itDll );
					resume();
					break;
				}
			default:
				resume();
				break;
			}
		terminate();
	}
	catch( exception const &exc )
	{
		cout << exc.what() << endl;
		terminate();
	}
}

using region_vector = string;
using region_it = typename region_vector::const_iterator;

vector<string_view> scanRegion( string_view const &sv );

void hookVirtualFree( HANDLE hProcess, LPVOID lpAddress, DWORD dwSize, 
DWORD dwFreeType )
{
	try
	{
		size_t
			pageSize = getPageSize(),
			alignHead = curPage( (char *)lpAddress, pageSize ) - (char *)lpAddress;
		lpAddress = (char *)lpAddress - alignHead;
		auto query = [&]( LPCVOID lpAddress ) -> MEMORY_BASIC_INFORMATION
		{
			MEMORY_BASIC_INFORMATION mbi;
			if( VirtualQueryEx( hProcess, lpAddress, &mbi, sizeof mbi ) != sizeof 
mbi )
				throwSysErr( "can't get process memory region attributes" );
			return mbi;
		};
		char *regionBegin = (char *)query( lpAddress ).AllocationBase;
		auto scan = [&]<typename Callback>( Callback callback )
			requires requires( Callback callback, char *p ) { { callback( p, p ) }; }
		{
			MEMORY_BASIC_INFORMATION mbi;
			auto validProtect = []( DWORD dwProtect ) ->bool { return dwProtect 
== PAGE_READONLY || dwProtect == PAGE_READWRITE || dwProtect == 
PAGE_WRITECOPY; };
			for( char *scn = regionBegin; ; scn += mbi.RegionSize )
				if( (mbi = query( scn )).BaseAddress == scn )
				{
					if( mbi.AllocationBase != regionBegin || !mbi.RegionSize )
						return;
					if( mbi.State == MEM_COMMIT && validProtect( mbi.Protect ) )
						callback( scn, scn + mbi.RegionSize );
				}
				else
					throw invalid_argument( "VirtualQuey() returned wrong base address" );
		};
		region_vector processRegion;
		size_t maxSize = 0;
		scan( [&]( char *regionBegin, char *regionEnd )
			{
				maxSize = maxSize >= (size_t)(regionEnd - regionBegin) ? maxSize : 
regionEnd - regionBegin;
			} );
		processRegion.reserve( maxSize );
		scan( [&]( char *regionBegin, char *regionEnd )
			{
				processRegion.resize( regionEnd - regionBegin );
				SIZE_T read;
				if( !ReadProcessMemory( hProcess, regionBegin, processRegion.data(), 
regionEnd - regionBegin, &read ) || read != regionEnd - regionBegin )
					return;
				scanRegion( string_view( processRegion.cbegin(), 
processRegion.cend() ) );
			} );
	}
	catch( exception const & )
	{
	}
}

vector<string_view> scanRegion( string_view const &sv )
{
	constexpr size_t
		HEADER_SIZE = 8,
		TAIL_SIZE = 5,
		HEADER_AND_TAIL = HEADER_SIZE + TAIL_SIZE;
	if( sv.cend() - sv.cbegin() <= HEADER_AND_TAIL )
		return vector<string_view>();
	using region_it = typename string_view::const_iterator;
	auto detectBegin = [&]( region_it it ) -> bool
	{
		(void)(it + HEADER_SIZE);
		if( memcmp( to_address( it ), "%PDF-", 5 ) != 0 )
			return false;
		if( !isdigit( it[5] ) )
			return false;
		if( it[6] != '.' )
			return false;
		if( !isdigit( it[7] ) )
			return false;
		return true;
	};
	auto detectEnd = [&]( region_it it ) -> bool
	{
		(void)(it + TAIL_SIZE);
		if( memcmp( to_address( it ), "%%EOF", TAIL_SIZE ) != 0 )
			return false;
		return true;
	};
	region_it scn = sv.cbegin();
	vector<string_view> results;
	do
	{
		if( !detectBegin( scn ) )
		{
			++scn;
			continue;
		}
		region_it header = scn;
		for( scn += HEADER_AND_TAIL; scn <= sv.end() - TAIL_SIZE; )
			if( !detectEnd( scn ) )
				++scn;
			else
			{
				results.emplace_back( header, scn + TAIL_SIZE );
				scn += TAIL_SIZE;
			}
	} while( scn <= sv.end() - HEADER_AND_TAIL );
	return results;
}

[[noreturn]]
void throwSysErr( char const *str )
{
	throw system_error( (int)GetLastError(), system_category(), str );
}

size_t getProcOffset( char const *dll, char const *exp )
{
	string strCommandLine;
	strCommandLine += "DllProcBase \"";
	strCommandLine += dll;
	strCommandLine += "\" \"";
	strCommandLine += exp;
	strCommandLine += "\"";
	STARTUPINFOA si;
	memset( &si, 0, sizeof si );
	si.cb = sizeof si;
	PROCESS_INFORMATION pi;
	static char const strErr[] = "can't get 32 bit procedure address";
	if( !CreateProcessA( nullptr, (LPSTR)strCommandLine.c_str(), nullptr, 
nullptr, FALSE, DETACHED_PROCESS, nullptr, nullptr, &si, &pi ) )
		throwSysErr( strErr );
	XHANDLE
		xhProcess( pi.hProcess ),
		xhThread( pi.hThread );
	while( WaitForSingleObject( pi.hProcess, INFINITE ) == WAIT_FAILED );
	DWORD dwOffset;
	if( !GetExitCodeProcess( pi.hProcess, &dwOffset ) )
		throwSysErr( strErr );
	return dwOffset;
}

template<typename Pointer>
	requires is_pointer_v<Pointer>
Pointer curPage( Pointer pointer, size_t pageSize )
{
	return (Pointer)((size_t)pointer & -(ptrdiff_t)pageSize);
}

size_t getPageSize()
{
	static atomic<size_t> aDwPageSize( 0 );
	size_t pageSize;
	if( !(pageSize = aDwPageSize.load( memory_order_relaxed )) )
	{
		SYSTEM_INFO si;
		GetSystemInfo( &si ),
		aDwPageSize.store( pageSize = si.dwPageSize, memory_order_relaxed );
	}
	return pageSize;
}

[toc] | [prev] | [next] | [standalone]


#555957 — Und noch einen oben drauf

FromBonita Montero <Bonita.Montero@gmail.com>
Date2022-05-27 10:11 +0200
SubjectUnd noch einen oben drauf
Message-ID<t6q13n$47k$1@dont-email.me>
In reply to#555690
Meine weitere Vermutung war, dass die die PDFs zwar im Speicher am
Stück decodieren, dass dann irgendwie parsen und in eine hierarchische
Datenstrutur bringen, die einfacher zu handeln ist als ein lineares
PDF im Speicher, und am Ende vor der Freigaben des Speicherblocks in
dem das PDF liegt Sicherheits-halber einfach ein memset() drüberlaufen
lassen der das entschlüsselte PDF löscht.
Also könnte ich eigentlich genauso wie in der vorangegangenen Situation
aus Debugger-Sicht das memset() hooken. Das wäre aber von der Perfor-
mance her nicht möglich, denn memset ist eine mit recht hoher Frequenz
aufgerufene Funktion. Ein Debug-Event führt ja erstmal dazu, dass alle
Threads in Ziel Prozess angehalten werden und mein Debugger dann weiter
entscheiden kann. Meine ursprüngliche Vorgehensweise war ja, dass ich
im Folgenden dann den Breakpoint auf dem Einstiegspunkt den ich getrappt
habe wegnehme, nur den Thread der diese Funktion gerade aufgerufen hat
bis zu einem Breakpoint an der Rücksprungs-Adresse weiterlaufen lasse,
dann den ersten Breakpoint wieder einschalte, den zweiten ausschalte
und dann alle anderen Threads wieder aktiviere. Würde ich diesen ganzen
Aufwand für jedes popelige memset() machen, dann wäre Digital Editions
von der Performace her sicher nicht mehr benutzbar.
Also dachte ich mir was anderes aus: wie wäre es denn, wenn ich das
memset() direkt im Ziel-Prozess patche ? Das führte aber dann doch zu
einem enormen Aufwand:
Ich musste erst eine eigenen DLL in den Ziel-Prozess bringen. Das
Problem ist dann aber: es gibt einen Aufruf LoadLibrary(), aber mit
dem kann ich nur DLLs in den eigenen Prozess laden. Meine Lösung war
dann einfach folgende: ich lade die DLL in meinen eigenen Prozess und
lasse die vom Loader im Kernel implizit auf ihre Lade-Adresse relokie-
ren. Dann ermittle ich den von der DLL benötigten Adressraum und ver-
suche im Ziel-Prozess mit VirtualAllocEx() genau an der selben Adresse
Speicher zu allokieren. Gelingt das nicht, dann entlade ich die DLL
wieder und reserviere im injezierenden Prozess den Adressraum den die
DLL zuvor belegt hat. Dann versuche ich die DLL wieder neu zu laden
und weil ich zuvor ihre alte Lade-Adresse reserviert habe muss das
an einer anderen Adresse geschehen. Das mache ich dann eben so lange
bis ich eine Lade-Adresse erwischt habe die auch im Ziel-Prozess
allokierbar ist.
Die DLL kopiere ich dann einfach mit WriteProcesMemory() in den Ziel-
prozess und starte an der Adresse eines Exports dieser DLL mit Create
RemoteThread() einen eigenen Thread.
Das Problem dabei ist: das was ich im Ziel-Prozess dann habe ist keine
normal geladene DLL. Wenn ich eine DLL normal in einen Prozess lade,
dann lädt der Kernel auch ihre ganzen abhängigen DLLs und verküpft
Pointer in Daten und Code dieser DLLs untereinander, das das Ganze
dann auf die ganzen Lade-Adressen der DLL und der abhängigen DLLs
passt. D.h. ich kann mich nicht darauf verlassen, dass mein Code
dann beliebige Library-Abhängigkeiten nutzen kann.
Die Lösung ist aber gar nicht so komplex: denn ist gibt bestimtme
Abhängigkeiten die der injezierende und injezierte Prozess miteinan-
der teilen die immer an der selben Adresse liegen. Die Library
kernel32.dll wird nämlich ganz früh beim initialisieren des Exe-
cutable-Image geladen, noch bevor da irgendwelcher Code läuft.
Und die wird nicht an eine beliebige Adresse geladen, sondern an
eine bevorzugte Adresse die im Header der DLL steht. Und die wird
so lange bevorzugt so lange keine vor ihr geladene DLL da schon
stehen wollte oder zufällig da geladen ist.
Die Sache ist aber: kernel32.dll steht noch mit ganz vorne in der
Linker-Reihenfolge und die anderen System-spezifischen DLLs die
noch vor kernel32.dll geladen werden haben von Microsoft so ausge-
legt andere bevorzugte Ladeadressen. Das soll ermöglichen, dass
häufig benutzte DLLs gleichermaßen in mehrere Prozesse gemappt
werden können und nur einmal Speicher belegen. Würden solche DLLs
in jeden Prozess an unterschiedliche Adressen relokiert werden,
dann würden die mehrfach Speicher verbrauchen.
Auf jeden Fall bin ich damit schon mal auf der sicheren Seite beim
Aufruf von Fuktionen von kernel32.dll. Und da gibt es eine für den
Fall nutzliche Funktion, das bekannte LoadLibrary(), das eine DLL
nachlädt. Wenn ich jetzt dann im Ziel-Prozess eine DLL nachelade,
dann zieht die ihre ganzen DLL-Abhängigkeiten hinterher und ich
kann beliebigen Code im Ziel-Prozess zur Ausführung bringen.
Das habe ich dann auch so gemacht bzw. der injezierende Prozess
reserviert im Ziel-Prozess Speicher für einen String, der dann
den Namen der nachzuladenden DLL enthält und der im Ziel-Prozess
kreierte Remote-Thread kriegt als Parameter die Adresse des Strings,
damit er dann eine beliebige DLL nachladen kann.
Die nachgeladnene DLL hat keinerlei spezifische Einstiegspunkte
die von der injezierten DLL angesprochen werden, sondern muss ihre
ganze Arbeit in DllMain() erledigen. Da man da nur kurt verweilen
darf wird aus DllMain() dann eben schnell ein Thread keriert der
den Rest erledigt.
So, und jetzt kommt die Sache mit dem Hooken von memset(): Ich habe
mir testweise ein kleines Programm geschrieben, dass alle von ADE
geladenen DLLs listet und nachhsaut, welche davon einen Einstiegs-
punkt haben der memset() heißt. Dann habe ich geschaut, ob denn
alle Einstigspunkte die selbe Aufrufs-Konvention haben und das
Gleiche tun, also Speicher löschen. Alle Einstiegspunkte benutzten
die __cdecl Konvention, d.h. alle Prameter wurden auf dem Stack
übergeben, und taten das Gleiche, also Speicher löschen.
Das Hooken ansich ist recht einfach: Ich überschreibe einfach die
ersten fünf Byte der insgesamt dann fünf memset() Varianten mit
einem relativen Sprung auf meine memset() Implementation, die das
selbe tut, aber mit ein wenig Monitoring dazu.
Das Hooken war auch selbst im Ziel-Prozess nicht ganz einfach, denn
ich kann ja nicht so einfach Code überschreiben der vielleicht gerade
ausgeführt wird. Also habe ich im besagt kreierten Thread dann einfach
alle Thread außer diesen Thread an. Dann gehe ich jeden der angehalte-
nen Threads durch und schaue, ob der sich in den ersten fünf Byte von
memset() befindet. Wenn ja, dann lasse ich ihn weiterlaufen mache
selbst ein Sleep( 0 ), dass ich einen Timeslice verstreichen lasse,
halte den Thread dann wieder an und checke nochmal, ob der sich immer
noch oder wieder in diesem Bereich befindet. Wenn ja, dann versuche
ich das so lange nochmal bis dann halt nicht mehr.
Das eigene memset() hat dann einfach geschaut, ob der zu überschrei-
bende Block >= 64kB ist und wenn ja, dann hat er darin ein PDF gesucht.
Ich habe diese Grenze gesetzt weil wenn ich das nicht getan hätte, dann
würde ich jedes Popels-memset() mit hocher Frequenz mit analysieren,
dass ADE von der Performance her nicht mehr benutzbar wäre.
Es ist auch so mit integrierter Monitoring-Funktion bei >= 64kB Block-
größe so, dass ADE an machen Stellen recht langsam wird, da möchte ich
erst gar nicht wissen was passiert, wenn ich diese Grenze weglasse.
Und was hat's gebracht ? Zwei Tage mit je 14h Arbeit und gefunden habe
ich: NIX. Aber dazugelernt hab ich echt viel. Ich denke so eine smarte
Art, beliebigen Code in einen fremden Windows Prozess einzuschleusen
hat vor mir noch keiner programmiert.
Vielleicht hat da noch jemand anders eine Idee, die ich umsetzen könnte.

[toc] | [prev] | [next] | [standalone]


#555968 — Re: Und noch einen oben drauf

From"Wendelin Uez" <wuez@online.de>
Date2022-05-27 12:11 +0200
SubjectRe: Und noch einen oben drauf
Message-ID<t6qb9f$7ug$2@dont-email.me>
In reply to#555957
Sehr iInteressant, aber vielleicht statt Perlen unter Säue zu werfen lieber 
gleich in fachlich qualifizierten Foren präsentieren? 

[toc] | [prev] | [next] | [standalone]


#556005 — Re: Und noch einen oben drauf

FromBonita Montero <Bonita.Montero@gmail.com>
Date2022-05-27 19:39 +0200
SubjectRe: Und noch einen oben drauf
Message-ID<t6r2ca$geq$2@dont-email.me>
In reply to#555968
Am 27.05.2022 um 12:11 schrieb Wendelin Uez:
> Sehr iInteressant, aber vielleicht statt Perlen unter Säue zu werfen 
> lieber gleich in fachlich qualifizierten Foren präsentieren?

Ich kenne hier mindestens einen fast stillen Mitleser
der das versteht und auch mindestens spanned findet.

[toc] | [prev] | [standalone]


Back to top | Article view | ger.ct


csiph-web