Return Address Verifier¶
이번 장에서 Zigzi
를 활용하여 Runtime
에 함수의 복귀주소를 검증하는 기법인
Return Address Verifier
를 구현해보고 이를 통해서 Zigzi
를 기반으로
Binary Instrumentation
을 활용한 기능을 Binary
에 추가하는 방법을
살펴보겠습니다.
PE¶
먼저 Instrumentation 기능을 사용하기 위해 PEInstrument
모듈을,
Binary rewriting
기능을 사용하기 위해 File Manager
인 PEManager
모듈을 import 합니다.
from PEInstrument import *
from PEManager import *
from WindowAPIHelper import *
또한 Return Address Verifier 를 통해 함수의 복귀주소가 변조됐을 시에 이를
시각적으로 보여주기 위해서 MessageBoxA
를 사용할 목적으로 WindowAPIHelper
모듈 또한 import 합니다.
# PE Manager
pe_manager = PEManager(filename)
# add api
window_api_helper = WindowAPIHelper(pe_manager)
message_box_fn_rva = window_api_helper.add_message_box()
# set new instrumentation
pe_instrument = PEInstrument(pe_manager)
# instrument return address verifier feature to binary
do_return_address_verifier(pe_instrument, pe_manager, message_box_fn_rva)
이제 원하는 Binary의 경로와 이름으로 생성된 PEManager와 WindowAPIHelper를 통해 PEInstrument 와 MessageBox API의 Relative Virtual Address 값을 얻습니다.
Return Address Verifier 기능을 구현하는 과정은 아래에서 다룹니다.
def do_return_address_verifier(pe_instrument, pe_manager, fn_rva):
simple_instrument_error_handler(pe_instrument, pe_manager, fn_rva)
# case of relative branch instruction
pe_instrument.register_after_relative_branch(
simple_instrument_return_address_at_after_branch
)
# case of indirect branch instruction
pe_instrument.register_after_indirect_branch(
simple_instrument_return_address_at_after_branch
)
# case of return instruction
pe_instrument.register_pre_return(
simple_instrument_return_address_verifier_at_pre_return
)
pe_instrument.do_instrument()
Return Address Verifier 을 구현하는 과정은 크게 세 단계로 이루어집니다. 첫 번째,
과정으로 함수의 복귀주소 검증이 실패했을시 이를 시각적으로 보여주기 위해서
MessageBox
를 띄우는 API 호출 로직을 추가하는 과정입니다.
def simple_instrument_error_handler(pe_instrument, pe_manager, fn_rva):
global code_rva
fn_va = pe_manager.get_abs_va_from_rva(fn_rva)
allocation = pe_instrument.falloc(0x1000)
caption = "Zigzi"
text = "Failed to Verifying Return Address."
allocation_va = allocation.get_va()
caption_start_pos = 0
text_start_pos = 0x100
allocation[caption_start_pos:len(caption)] = caption
allocation[text_start_pos:len(text)] = text
code = ("push 0;" # UINT uType
"push {};" # LPCTSTR lpCaption,
"push {};" # LPCTSTR lpText,
"push 0;" # HWND hWnd
"call [{}];").format(allocation_va + caption_start_pos,
allocation_va + text_start_pos,
fn_va)
code_rva = pe_instrument.append_code(code)
pe_manager.register_rva_to_relocation(code_rva
+ 3 # push 0; push
)
pe_manager.register_rva_to_relocation(code_rva
+ 3 # push 0; push
+ 5 # lpCaption;push
)
pe_manager.register_rva_to_relocation(code_rva
+ 3 # push 0; push
+ 5 # lpCaption;push
+ 4 # lptext
+ 2 # push 0;
+ 2 # call
)
첫 번째 과정은,
1. MessageBox 에 인자로 사용할 Text Zigzi
와 Failed to Verifying Return Address.
를 Binary에 삽입 합니다.
2. 1에서 Text가 삽입된 주소와 MessageBox API의 RVA(Relative Virtual Address)를
기반으로 MessageBox를 호출하는 로직을 어셈블리어로 코딩하여 추가합니다.
3. Text들과 MessageBox의 RVA는 상대주소이므로 Relocation 목록에 추가합니다.
로 이루어지게 됩니다.
위의 과정을 통해서 MessageBox를 호출하는 함수를 Binary에 추가했고 해당 함수의 RVA를 획득합니다.
def simple_instrument_return_address_at_after_branch(instruction):
code = ("prefetch [{0}]".format(instruction.address
+ instruction.size
+ 0x1000))
hex_code = binascii.hexlify(code).decode('hex')
try:
# Initialize engine in X86-32bit mode
ks = Ks(KS_ARCH_X86, KS_MODE_32)
encoding, count = ks.asm(hex_code)
return encoding, count
except KsError as ex:
print("ERROR: %s" % ex)
return None, 0
우리가 이미 알다시피 함수를 호출하는 경우에 복귀할 주소는 해당 함수를 호출한 지점의 다음 명령어입니다. 때문에 분기 명령어 계통의 다음 주소는 항상 복귀할 주소입니다.
이 원리에 따라 모든 분기 명령어의 다음에 해당 분기 명령어의 다음 주소(복귀주소) 에 해당 주소의 값을 저장하는 공간을 만듭니다.
실행 간에 영향을 미치지 않도록 이 주소의 값을 저장하기 위해서 우리는 prefetch
명령어를 사용할 것 입니다.
def simple_instrument_return_address_verifier_at_pre_return(instruction):
global code_rva
code = (
"push ecx;" # store value
"mov ecx, [esp+4];" # load return address to ecx
"cmp [ecx+3], ecx;" # compare return address with RAV
"jne {};" # if not equal, jump to error handler
"pop ecx;" # recover value
).format(code_rva - instruction.address - 0x1000
+ 0xF # instruction size till end of instruction.
)
hex_code = binascii.hexlify(code).decode('hex')
try:
# Initialize engine in X86-32bit mode
ks = Ks(KS_ARCH_X86, KS_MODE_32)
encoding, count = ks.asm(hex_code)
return encoding, count
except KsError as ex:
print("ERROR: %s" % ex)
return None, 0
이제 함수가 호출된 후에 복귀를 하는 시점에, 스택에 저장된 복귀할 주소와 해당 주소에
우리가 instrumentation 한 명령어 prefetch
의 operand 주소를 비교하면 복귀주소의
검증을 수행할 수 있습니다.
결과는 다음과 같네요.