add selfbind functionality
This commit is contained in:
parent
6815ea6556
commit
4ee62a2d93
@ -15,6 +15,15 @@ type saveImports struct{
|
||||
}
|
||||
|
||||
func (action *saveImports) withMacho(mf *MachoFile) error {
|
||||
action.saveToInfo(mf)
|
||||
mc := mf.Context()
|
||||
if mc.Header().IsDylib() {
|
||||
mc.WriteInfoToData(mf.Info())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (action *saveImports) saveToInfo(mf *MachoFile) error {
|
||||
// calculateHash := func(name string) uint32 {
|
||||
// var h uint32 = 0x811c9dc5
|
||||
// for _, s := range name {
|
||||
|
@ -7,9 +7,11 @@ import (
|
||||
"math/rand"
|
||||
"time"
|
||||
"strings"
|
||||
"encoding/binary"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"ios-wrapper/pkg/protomodel"
|
||||
. "ios-wrapper/pkg/ios"
|
||||
)
|
||||
|
||||
@ -681,3 +683,130 @@ func (mc *MachoContext) RemoveExportTrie() {
|
||||
// should never occur unless this binary is modified
|
||||
}
|
||||
}
|
||||
|
||||
func (mc *MachoContext) AddSection(segname string, name string, size int) Section {
|
||||
// mc.file.WriteAt(mc.header.Serialize(mc), 0)
|
||||
var ret Section
|
||||
var buffer bytes.Buffer
|
||||
for _, command := range mc.commands {
|
||||
switch command.(type) {
|
||||
case *Segment64:
|
||||
var virtualAddr uint64
|
||||
var fileOffset uint64
|
||||
|
||||
var segment = command.(*Segment64)
|
||||
if bytes.Compare(bytes.Trim(segment.SegName(), "\x00"), []byte(segname)) != 0 {
|
||||
buffer.Write(segment.Serialize(mc))
|
||||
continue
|
||||
} else {
|
||||
virtualAddr = segment.Vmaddr()
|
||||
fileOffset = segment.Fileoff()
|
||||
for _, section := range segment.Sections() {
|
||||
virtualAddr += section.Size()
|
||||
// if section.Offset() != 0 {
|
||||
fileOffset += section.Size()
|
||||
// }
|
||||
}
|
||||
|
||||
align := uint64(4)
|
||||
alignment := align - (fileOffset % align)
|
||||
fileOffset += alignment
|
||||
virtualAddr += alignment
|
||||
|
||||
enoughSpace := segment.Fileoff() + segment.Filesize() >= fileOffset + uint64(size)
|
||||
if !enoughSpace {
|
||||
fmt.Println("Not enough space to store saved info in __DATA segment, need resize (not supported now)")
|
||||
panic("Not enough space to store saved info in __DATA segment, need resize (not supported now)")
|
||||
}
|
||||
}
|
||||
|
||||
var section Section64
|
||||
section.sectname = make([]byte, 16)
|
||||
copy(section.sectname, name)
|
||||
section.segname = make([]byte, 16)
|
||||
copy(section.segname, segname)
|
||||
section.size = uint64(size)
|
||||
section.reloff = 0
|
||||
section.nreloc = 0
|
||||
section.flags = 0
|
||||
section.align = 3
|
||||
section.reserved1 = 0
|
||||
section.reserved2 = 0
|
||||
section.reserved3 = 0
|
||||
|
||||
// addr will increment from the last section
|
||||
// offset will increment from the last section + size
|
||||
// be careful of Virtual section (bss)
|
||||
section.addr = virtualAddr
|
||||
section.offset = uint32(fileOffset)
|
||||
segment.nsects += 1
|
||||
buffer.Write(segment.Serialize(mc))
|
||||
buffer.Write(section.Serialize(mc))
|
||||
|
||||
fmt.Printf("Add a new section with addr=0x%x, fileoffset=0x%x\n", section.addr, section.offset)
|
||||
|
||||
ret = §ion
|
||||
continue
|
||||
|
||||
default:
|
||||
buffer.Write(command.Serialize(mc))
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
mc.header.sizeofcmds = uint32(buffer.Len())
|
||||
header := mc.header.Serialize(mc)
|
||||
mc.file.WriteAt(header, 0)
|
||||
mc.file.WriteAt(buffer.Bytes(), int64(len(header)))
|
||||
return ret
|
||||
}
|
||||
|
||||
func (mc *MachoContext) WriteInfoToData(info *protomodel.MachoInfo) {
|
||||
encode := func () []byte {
|
||||
buffer := new(bytes.Buffer)
|
||||
for _, table := range info.Symbols.Tables {
|
||||
binary.Write(buffer, mc.byteorder, table.LibIndex)
|
||||
binary.Write(buffer, mc.byteorder, table.Nsymbols)
|
||||
for _, symbol := range table.Symbols {
|
||||
binary.Write(buffer, mc.byteorder, (symbol.SymbolIndex<<8)|symbol.SegmentIndex)
|
||||
binary.Write(buffer, mc.byteorder, symbol.Offset)
|
||||
}
|
||||
}
|
||||
instructions := buffer.Bytes()
|
||||
|
||||
buffer = new(bytes.Buffer)
|
||||
for _, lib := range info.Symbols.Libs {
|
||||
buffer.WriteString(lib)
|
||||
buffer.WriteByte(0)
|
||||
}
|
||||
liblist := buffer.Bytes()
|
||||
|
||||
buffer = new(bytes.Buffer)
|
||||
for _, symbol := range info.Symbols.Symbols {
|
||||
buffer.WriteString(symbol)
|
||||
buffer.WriteByte(0)
|
||||
}
|
||||
symbollist := buffer.Bytes()
|
||||
|
||||
buffer = new(bytes.Buffer)
|
||||
// offset to liblist
|
||||
binary.Write(buffer, mc.byteorder, uint32(len(instructions)))
|
||||
// offset to symbollist
|
||||
binary.Write(buffer, mc.byteorder, uint32(len(instructions) + len(liblist)))
|
||||
buffer.Write(instructions)
|
||||
buffer.Write(liblist)
|
||||
buffer.Write(symbollist)
|
||||
|
||||
return buffer.Bytes()
|
||||
}
|
||||
encoded := encode()
|
||||
// encoded := []byte{0x11,0x22,0x33, 0x44}
|
||||
section := mc.AddSection("__DATA", "selfbind", len(encoded))
|
||||
if mc.Is64bit() {
|
||||
var s *Section64 = section.(*Section64)
|
||||
mc.file.WriteAt(encoded, int64(s.Offset()))
|
||||
} else {
|
||||
var s *Section32 = section.(*Section32)
|
||||
mc.file.WriteAt(encoded, int64(s.Offset()))
|
||||
}
|
||||
}
|
||||
|
@ -216,6 +216,51 @@ uint64_t get_slide(const void *header) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
void* get_selfbind(const void *header) {
|
||||
const uint32_t magic = *(uint32_t *)header;
|
||||
char *ptr = (char *)header;
|
||||
if (magic == magic64) {
|
||||
ptr += 0x20;
|
||||
} else {
|
||||
ptr += 0x20 - 0x4;
|
||||
}
|
||||
|
||||
const uint32_t ncmds = *((uint32_t *)header + 4);
|
||||
char* command_ptr = ptr;
|
||||
|
||||
uint64_t slide;
|
||||
for (int i = 0; i < ncmds; i++) {
|
||||
const uint32_t cmd = *((uint32_t *)ptr + 0);
|
||||
const uint32_t cmdsize = *((uint32_t *)ptr + 1);
|
||||
if (cmd == LC_SEGMENT_64) {
|
||||
char *name = (char *)((uint64_t *)ptr + 1);
|
||||
uint64_t vmaddr = *((uint64_t *)ptr + 3);
|
||||
uint64_t fileoffset = *((uint64_t *)ptr + 5);
|
||||
// this assumes that __TEXT comes before __DATA_CONST
|
||||
if (custom_strcmp(name, "__TEXT") == 0) {
|
||||
slide = (uint64_t)header - vmaddr;
|
||||
} else if (custom_strcmp(name, "__DATA") == 0) {
|
||||
uint64_t nsect = *((uint32_t *)ptr + 8*2);
|
||||
char* sections_ptr = (char*)((uint32_t*)ptr + 18);
|
||||
sections_ptr += (16 * 2 + 8 * 2 + 4 * 8) * (nsect - 1);
|
||||
|
||||
for (int sec = 0; sec < nsect; sec++) {
|
||||
char* secname = sections_ptr;
|
||||
if (custom_strcmp(secname, "selfbind") == 0) {
|
||||
uint64_t addr = *((uint64_t*)sections_ptr + 4);
|
||||
uint64_t size = *((uint64_t*)sections_ptr + 5);
|
||||
uint32_t *data_ptr = (uint32_t*)(addr + slide);
|
||||
return (void*)data_ptr;
|
||||
}
|
||||
sections_ptr += 16 * 2 + 8 * 2 + 4 * 8;
|
||||
}
|
||||
}
|
||||
}
|
||||
ptr += cmdsize;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void print_macho_summary(const void *header) {
|
||||
const uint32_t magic = *(uint32_t *)header;
|
||||
char *ptr = (char *)header;
|
||||
@ -450,11 +495,11 @@ void *find_in_reexport(struct libcache *cache, struct libcache_item *lib,
|
||||
char *name = (char *)ptr + name_offset;
|
||||
uint32_t hash = calculate_libname_hash(cache, name);
|
||||
for (int j = 0; j < cache->size; j++) {
|
||||
struct libcache_item reexport = cache->libs[j];
|
||||
if (reexport.hash != hash) {
|
||||
struct libcache_item* reexport = &cache->libs[j];
|
||||
if (reexport->hash != hash) {
|
||||
continue;
|
||||
}
|
||||
void *found = find_in_lib(cache, &reexport, symbol);
|
||||
void *found = find_in_lib(cache, reexport, symbol);
|
||||
if (found)
|
||||
return found;
|
||||
}
|
||||
@ -466,33 +511,27 @@ void *find_in_reexport(struct libcache *cache, struct libcache_item *lib,
|
||||
void *find_in_lib(struct libcache *cache, struct libcache_item *lib,
|
||||
const char *symbol) {
|
||||
void *direct = find_in_export_trie(lib->header, lib->trie, symbol);
|
||||
if (direct)
|
||||
if (direct) {
|
||||
return direct;
|
||||
}
|
||||
// cannot find in directly exported trie, loop through all reexport libs
|
||||
return find_in_reexport(cache, lib, symbol);
|
||||
}
|
||||
|
||||
void *custom_dlsym(struct libcache *cache, uint32_t hash, const char *symbol) {
|
||||
for (size_t i = 0; i < cache->size; i++) {
|
||||
struct libcache_item* cache_lib = &cache->libs[i];
|
||||
if (cache_lib->hash == hash) {
|
||||
return find_in_lib(cache, cache_lib, symbol);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void *custom_dlsym(struct libcache *cache, const char *libname,
|
||||
const char *symbol) {
|
||||
uint32_t hash = calculate_libname_hash(cache, libname);
|
||||
for (int i = 0; i < cache->size; i++) {
|
||||
struct libcache_item cache_lib = cache->libs[i];
|
||||
if (cache_lib.hash == hash) {
|
||||
return find_in_lib(cache, &cache_lib, symbol);
|
||||
}
|
||||
}
|
||||
printf("cannot find lib with hash 0x%x\n", hash);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void *custom_dlsym(struct libcache *cache, uint32_t hash, const char *symbol) {
|
||||
for (int i = 0; i < cache->size; i++) {
|
||||
struct libcache_item cache_lib = cache->libs[i];
|
||||
if (cache_lib.hash == hash) {
|
||||
return find_in_lib(cache, &cache_lib, symbol);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
return custom_dlsym(cache, hash, symbol);
|
||||
}
|
||||
|
||||
void bootstrap_libcache_item(struct libcache_item* item, const void* header, const char* name) {
|
||||
@ -536,7 +575,6 @@ void bootstrap_libcache_item(struct libcache_item* item, const void* header, con
|
||||
ptr += cmdsize;
|
||||
}
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
struct libcache_item* get_libcache_with_name(struct libcache* cache, const char* name) {
|
||||
@ -707,7 +745,7 @@ __attribute__((constructor)) static void
|
||||
bruh(int argc, const char *const argv[], const char *const envp[],
|
||||
const char *const apple[], const struct ProgramVars *vars) {
|
||||
printf("=== manual symbol bind starts ===\n");
|
||||
set_cwd(envp);
|
||||
// set_cwd(envp);
|
||||
|
||||
// ProgramVars contains pointer to main executable (mapped) file
|
||||
|
||||
@ -817,6 +855,72 @@ void build_cache(struct libcache& cache, void* main) {
|
||||
}
|
||||
}
|
||||
|
||||
void fix_binds(struct libcache_item* libfixing, struct libcache* cache,
|
||||
int n_ins, uint32_t* instructions, char* libs, char* symbols) {
|
||||
uint32_t libsystem_hash =
|
||||
calculate_libname_hash(cache, "/usr/lib/libSystem.B.dylib");
|
||||
|
||||
typedef void *(*vm_protect_t)(void *, uint64_t, uint64_t, int, int);
|
||||
typedef void *(*mach_task_self_t)();
|
||||
mach_task_self_t mach_task_self_func =
|
||||
(mach_task_self_t)custom_dlsym(cache, libsystem_hash, "_mach_task_self");
|
||||
vm_protect_t vm_protect_func =
|
||||
(vm_protect_t)custom_dlsym(cache, libsystem_hash, "_vm_protect");
|
||||
|
||||
int npage_rw_fixed = 0;
|
||||
uint64_t page_rw_fixed[10]; // should be dynamic, but works for now
|
||||
|
||||
int pc = 0;
|
||||
for (;pc != n_ins;) {
|
||||
uint32_t libidx = instructions[pc];
|
||||
uint32_t nsym = instructions[pc + 1];
|
||||
pc += 2;
|
||||
|
||||
char* lib = libs + libidx;
|
||||
for (int i = 0; i < nsym; i++) {
|
||||
uint32_t op = instructions[pc];
|
||||
uint32_t offset = instructions[pc + 1];
|
||||
pc += 2;
|
||||
|
||||
uint32_t symidx = op >> 8;
|
||||
uint32_t segment = op & 0xff;
|
||||
char* sym = symbols + symidx;
|
||||
|
||||
uint64_t fix_at = offset + libfixing->segment[segment];
|
||||
|
||||
// enable WRITE protection for this data segment
|
||||
int need_rw_fix = true;
|
||||
for (int j = 0; j < npage_rw_fixed; j++) {
|
||||
if (page_rw_fixed[j] <= fix_at &&
|
||||
page_rw_fixed[j] + 0x1000 > fix_at) {
|
||||
need_rw_fix = false;
|
||||
}
|
||||
}
|
||||
if (need_rw_fix) {
|
||||
uint64_t start_page = fix_at - (fix_at % 0x1000);
|
||||
vm_protect_func(mach_task_self_func(), start_page, 0x1000, 0,
|
||||
VM_PROT_READ | VM_PROT_WRITE);
|
||||
page_rw_fixed[npage_rw_fixed++] = start_page;
|
||||
printf("modify page starts at 0x%llx to RW\n", start_page);
|
||||
}
|
||||
|
||||
void *resolved = 0;
|
||||
// search with hash is faster
|
||||
// resolved = custom_dlsym(&cache, symbol.hash, symbol.name);
|
||||
if (resolved == 0) {
|
||||
// but fuck apple they have relative path and rpath
|
||||
resolved = custom_dlsym(cache, lib, sym);
|
||||
}
|
||||
*(uint64_t *)fix_at = (uint64_t)resolved;
|
||||
|
||||
printf("imports need to fix: %s at 0x%llx\n", sym, fix_at);
|
||||
printf(" from=%s\n", lib);
|
||||
printf(" segment id=%d; offset=0x%x;", segment, offset);
|
||||
printf(" resolved=%llx(%p)\n", *(uint64_t*)fix_at, resolved);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void fix_objc(struct libcache_item* libfixing, struct libcache& cache);
|
||||
void fix_initializer(struct libcache_item* libfixing, struct libcache& cache);
|
||||
@ -916,80 +1020,52 @@ void fix(struct libcache& cache) {
|
||||
// may need to look into why this happens so we can deal with this more
|
||||
// generic
|
||||
|
||||
uint32_t libsystem_hash =
|
||||
calculate_libname_hash(&cache, "/usr/lib/libSystem.B.dylib");
|
||||
// resolve selfbind if exist
|
||||
{ // stored inside __DATA,selfbind
|
||||
struct libcache_item* libfixing = get_libcache_with_name(&cache, "thislib");
|
||||
struct selfbind_t {
|
||||
uint32_t liblist_offset;
|
||||
uint32_t symbollist_offset;
|
||||
};
|
||||
struct selfbind_t* selfbind = (struct selfbind_t*)get_selfbind(libfixing->header);
|
||||
|
||||
typedef void *(*vm_protect_t)(void *, uint64_t, uint64_t, int, int);
|
||||
typedef void *(*mach_task_self_t)();
|
||||
mach_task_self_t mach_task_self_func =
|
||||
(mach_task_self_t)custom_dlsym(&cache, libsystem_hash, "_mach_task_self");
|
||||
vm_protect_t vm_protect_func =
|
||||
(vm_protect_t)custom_dlsym(&cache, libsystem_hash, "_vm_protect");
|
||||
if (selfbind) {
|
||||
char* libs = (char*)(selfbind+1) + selfbind->liblist_offset;
|
||||
char* symbols = (char*)(selfbind+1) + selfbind->symbollist_offset;
|
||||
uint64_t n_instructions = ((uint64_t)libs - (uint64_t)(selfbind + 1)) / 4;
|
||||
uint32_t* encoded_table = (uint32_t*)(selfbind+1);
|
||||
|
||||
int npage_rw_fixed = 0;
|
||||
uint64_t page_rw_fixed[10]; // should be dynamic, but works for now
|
||||
|
||||
// think of a way to get what binary to fix
|
||||
// so we can iterate through them
|
||||
char* lib_to_resolve = "main";
|
||||
struct libcache_item* libfixing = get_libcache_with_name(&cache, lib_to_resolve);
|
||||
|
||||
int pc = 0;
|
||||
for (;pc != bshield_data::n_instructions;) {
|
||||
uint32_t libidx = bshield_data::encoded_table[pc];
|
||||
uint32_t nsym = bshield_data::encoded_table[pc + 1];
|
||||
pc += 2;
|
||||
|
||||
char* lib = bshield_data::libs + libidx;
|
||||
for (int i = 0; i < nsym; i++) {
|
||||
uint32_t op = bshield_data::encoded_table[pc];
|
||||
uint32_t offset = bshield_data::encoded_table[pc + 1];
|
||||
pc += 2;
|
||||
|
||||
uint32_t symidx = op >> 8;
|
||||
uint32_t segment = op & 0xff;
|
||||
char* sym = bshield_data::symbols + symidx;
|
||||
|
||||
uint64_t fix_at = offset + libfixing->segment[segment];
|
||||
|
||||
// enable WRITE protection for this data segment
|
||||
int need_rw_fix = true;
|
||||
for (int j = 0; j < npage_rw_fixed; j++) {
|
||||
if (page_rw_fixed[j] <= fix_at &&
|
||||
page_rw_fixed[j] + 0x1000 > fix_at) {
|
||||
need_rw_fix = false;
|
||||
}
|
||||
}
|
||||
if (need_rw_fix) {
|
||||
uint64_t start_page = fix_at - (fix_at % 0x1000);
|
||||
vm_protect_func(mach_task_self_func(), start_page, 0x1000, 0,
|
||||
VM_PROT_READ | VM_PROT_WRITE);
|
||||
page_rw_fixed[npage_rw_fixed++] = start_page;
|
||||
printf("modify page starts at 0x%llx to RW\n", start_page);
|
||||
}
|
||||
|
||||
void *resolved = 0;
|
||||
// search with hash is faster
|
||||
// resolved = custom_dlsym(&cache, symbol.hash, symbol.name);
|
||||
if (resolved == 0) {
|
||||
// but fuck apple they have relative path and rpath
|
||||
resolved = custom_dlsym(&cache, lib, sym);
|
||||
}
|
||||
*(uint64_t *)fix_at = (uint64_t)resolved;
|
||||
|
||||
printf("imports need to fix: %s at 0x%llx\n", sym, fix_at);
|
||||
printf(" from=%s\n", lib);
|
||||
printf(" segment id=%d; offset=0x%llx;", segment, offset);
|
||||
printf(" resolved=%llx(%p)\n", *(uint64_t*)fix_at, resolved);
|
||||
printf("[*] performing selfbind (instructions=%p)\n", selfbind);
|
||||
fix_binds(libfixing, &cache,
|
||||
n_instructions, encoded_table,
|
||||
libs, symbols);
|
||||
}
|
||||
}
|
||||
|
||||
// the rest of the fixes are in main executable
|
||||
printf("[*] performing bind for main executable\n");
|
||||
struct libcache_item* libfixing = get_libcache_with_name(&cache, "main");
|
||||
fix_binds(libfixing, &cache,
|
||||
bshield_data::n_instructions, bshield_data::encoded_table,
|
||||
bshield_data::libs, bshield_data::symbols);
|
||||
|
||||
// TODO: Reformat the region as per before, or leave as it
|
||||
// for (int j = 0; j < npage_rw_fixed; j++) {
|
||||
// uint64_t start_page = page_rw_fixed[j];
|
||||
// vm_protect_func(mach_task_self_func(), start_page, 0x4000, 0, VM_PROT_READ);
|
||||
// }
|
||||
|
||||
// Encrypted __TEXT segment
|
||||
// char* text_start = (char*)libfixing->header + 0x3000;
|
||||
// vm_protect_func(mach_task_self_func(), (uint64_t)text_start, 0x1000, 0,
|
||||
// VM_PROT_READ | VM_PROT_WRITE);
|
||||
// printf("text fix at %p\n", text_start + 0xb8c);
|
||||
// for (int i = 0; i < 0x2ac; i++) {
|
||||
// text_start[0xb8c + i] = text_start[0xb8c + i] ^ 0xcc;
|
||||
// }
|
||||
// vm_protect_func(mach_task_self_func(), (uint64_t)text_start, 0x1000, 0,
|
||||
// VM_PROT_READ | VM_PROT_EXECUTE);
|
||||
|
||||
fix_objc(libfixing, cache);
|
||||
fix_initializer(libfixing, cache);
|
||||
}
|
||||
|
@ -64,12 +64,15 @@ clang -fobjc-arc -ObjC -mmacosx-version-min=$VERSION -o $OUT/a -L"./out" -lb a.m
|
||||
|
||||
# extract symbols from a
|
||||
# ../../macho-go/bin/ios-wrapper pepe -o $OUT/a-fixed -b $OUT/b.bcell --remove-imports --remove-exports --remove-symbol-table --keep-imports _printf $OUT/a
|
||||
../../macho-go/bin/ios-wrapper pepe -o $OUT/a-fixed -b $OUT/b.bcell --remove-imports --remove-exports --keep-imports _printf $OUT/a
|
||||
../../macho-go/bin/ios-wrapper pepe -o $OUT/a-fixed -b $OUT/b.bcell --remove-imports --remove-exports $OUT/a
|
||||
../../macho-go/bin/ios-wrapper bcell2header -b $OUT/b.bcell -o $OUT/b.h
|
||||
# build libb with symbols extracted from a
|
||||
clang++ -mmacosx-version-min=$VERSION -o $OUT/libb.dylib -shared -Wl,-reexport_library out/libc.dylib b.cc
|
||||
../../macho-go/bin/ios-wrapper pepe -o $OUT/libb.dylib -b $OUT/libb.bcell --remove-imports --remove-exports --keep-imports _dyld_get_sdk_version --keep-imports _malloc --keep-imports _printf --keep-imports ___stack_chk_guard $OUT/libb.dylib
|
||||
|
||||
# resign
|
||||
codesign --force --deep -s - $OUT/a-fixed
|
||||
codesign --force --deep -s - $OUT/libb.dylib
|
||||
|
||||
# export OBJC_PRINT_LOAD_METHODS=1
|
||||
# export OBJC_PRINT_CLASS_SETUP=1
|
||||
|
Loading…
Reference in New Issue
Block a user