리버스 엔지니어링용 Python#1: ELF 바이너리 파일

레이더 2입니다.아니오, 우리의 스크립트는 이렇게 보이지 않습니다. (

자체 해체 도구 구축 — 틀림없다 — 즐거움과 이윤


복잡한 역방향 도전을 해결할 때, 우리는 레이더 2나 IDA 같은 성숙한 도구를 자주 사용하여 분해와 디버깅을 한다.그러나 때로는 일의 배후에서 어떻게 작동하는지 좀 더 깊이 있게 알아야 한다.
자동화의 일부 과정과 관련이 있을 때, 자신의 어셈블리 스크립트를 스크롤하는 것이 매우 도움이 되고, 최종적으로 자신의 자체 제작 역방향 도구 체인을 구축할 수 있습니다.적어도 이것은 내가 시도하고 있는 것이다.

설치 프로그램


제목과 같이 Python 3 해석기가 필요합니다.
다른일단 네가 확실히 이렇게 한 것을 의심할 여지없이 확인하면,
사실 시스템에 Python 3 해석기가 설치되어 있으면 실행하십시오.
$ pip install capstone pyelftools
그 중에서 capstone은 ELF 파일을 해석하는 데 도움을 주기 위해 우리가 사용할 어셈블리 엔진과pyelftools 스크립트입니다.
이제 우리는 기본적인 반전의 예부터 시작하자
도전하다
/\* chall.c \*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {
   char \*pw = malloc(9);
   pw[0] = 'a';
   for(int i = 1; i <= 8; i++){
       pw[i] = pw[i — 1] + 1;
   }
   pw[9] = '\0';
   char \*in = malloc(10);
   printf("password: ");
   fgets(in, 10, stdin); // 'abcdefghi'
   if(strcmp(in, pw) == 0) {
       printf("haha yes!\n");
   }
   else {
       printf("nah dude\n");
   }
}
GCC/Clang으로 컴파일:
$ gcc chall.c -o chall.elf

스크립트 작성


우선, 바이너리 파일에 존재하는 서로 다른 부분을 봅시다.
# sections.py

from elftools.elf.elffile import ELFFile

with open('./chall.elf', 'rb') as f:
    e = ELFFile(f)
    for section in e.iter\_sections():
        print(hex(section[’sh\_addr’]), section.name)
이 스크립트는 모든 부분을 훑어보고 불러오는 위치를 보여 줍니다.이것은 앞으로 매우 유용할 것이다.이걸 실행해 주셨어요.
› python sections.py
0x238 .interp
0x254 .note.ABI-tag
0x274 .note.gnu.build-id
0x298 .gnu.hash
0x2c0 .dynsym
0x3e0 .dynstr
0x484 .gnu.version
0x4a0 .gnu.version\_r
0x4c0 .rela.dyn
0x598 .rela.plt
0x610 .init
0x630 .plt
0x690 .plt.got
0x6a0 .text
0x8f4 .fini
0x900 .rodata
0x924 .eh\_frame\_hdr
0x960 .eh\_frame
0x200d98 .init\_array
0x200da0 .fini\_array
0x200da8 .dynamic
0x200f98 .got
0x201000 .data
0x201010 .bss
0x0 .comment
0x0 .symtab
0x0 .strtab
0x0 .shstrtab
그중 대다수는 우리와 무관하지만, 여기에는 몇 가지 부분이 주의해야 한다.이것텍스트 부분에는 우리가 찾으려는 명령어 (조작 코드) 가 포함되어 있습니다.이것데이터 세그먼트는 컴파일할 때 문자열과 상수를 초기화해야 합니다.마지막으로plt는 프로그램 링크표와 입니다.글로벌 오프셋 테이블을 가져옵니다.만약 이것이 무슨 뜻인지 확실하지 않다면 ELF 형식과 그 내부를 읽어 보세요.
우리가 알기 때문이다.텍스트 부분에 조작 코드가 있습니다. 이 주소부터 2진 코드를 어셈블합니다.
# disas1.py

from elftools.elf.elffile import ELFFile
from capstone import \*

with open('./bin.elf', 'rb') as f:
    elf = ELFFile(f)
    code = elf.get\_section\_by\_name('.text')
    ops = code.data()
    addr = code['sh\_addr']
    md = Cs(CS\_ARCH\_X86, CS\_MODE\_64)
    for i in md.disasm(ops, addr):        
        print(f'0x{i.address:x}:\t{i.mnemonic}\t{i.op\_str}')
코드가 상당히 간단하다.우리는 달리기를 할 때 이 점을 보아야 한다
› python disas1.py | less      
0x6a0: xor ebp, ebp
0x6a2: mov r9, rdx
0x6a5: pop rsi
0x6a6: mov rdx, rsp
0x6a9: and rsp, 0xfffffffffffffff0
0x6ad: push rax
0x6ae: push rsp
0x6af: lea r8, [rip + 0x23a]
0x6b6: lea rcx, [rip + 0x1c3]
0x6bd: lea rdi, [rip + 0xe6]
**0x6c4: call qword ptr [rip + 0x200916]**
0x6ca: hlt
... snip ...
굵은 줄은 우리에게 상당히 재미있다.[rip+0x200916]의 주소는 [0x6ca+0x200916]에 해당하고 후자는 반대로 0x200fe0으로 계산한다.0x200fe0의 함수를 처음 호출합니까?이 함수는 무엇입니까?
이를 위해, 우리는 반드시 재배치를 고려해야 한다.참조 linuxbase.org

Relocation is the process of connecting symbolic references with symbolic definitions. For example, when a program calls a function, the associated call instruction must transfer control to the proper destination address at execution. Relocatable files must have “relocation entries’’ which are necessary because they contain information that describes how to modify their section contents, thus allowing executable and shared object files to hold the right information for a process’s program image.


이러한 재배치 항목을 찾으려고 세 번째 스크립트를 작성했습니다.
# relocations.py

import sys
from elftools.elf.elffile import ELFFile
from elftools.elf.relocation import RelocationSection

with open('./chall.elf', 'rb') as f:
    e = ELFFile(f)
    for section in e.iter\_sections():
        if isinstance(section, RelocationSection):
            print(f'{section.name}:')
            symbol\_table = e.get\_section(section['sh\_link'])
            for relocation in section.iter\_relocations():
                symbol = symbol\_table.get\_symbol(relocation['r\_info\_sym'])
                addr = hex(relocation['r\_offset'])
                print(f'{symbol.name} {addr}')
이 코드를 빠르게 훑어봅시다.우선 이 부분을 반복해서 훑어보고 Relocation Section 유형에 속하는지 확인합니다.그리고 나서 우리는 각 부분의 기호표의 재정립을 교체한다.마지막으로, 그것을 실행하면 우리는
› python relocations.py
.rela.dyn:
 0x200d98
 0x200da0
 0x201008
\_ITM\_deregisterTMCloneTable 0x200fd8
**\_\_libc\_start\_main 0x200fe0**
\_\_gmon\_start\_\_ 0x200fe8
\_ITM\_registerTMCloneTable 0x200ff0
\_\_cxa\_finalize 0x200ff8
stdin 0x201010
.rela.plt:
puts 0x200fb0
printf 0x200fb8
fgets 0x200fc0
strcmp 0x200fc8
malloc 0x200fd0
이전에 0x200fe0에서 함수 호출을 기억하십니까?예, 이것은 모두가 알고 있는 _ulibc\ustart\umain에 대한 호출입니다.동일, 근거linuxbase.org

The __libc_start_main() function shall perform any necessary initialization of the execution environment, call the main function with appropriate arguments, and handle the return from main(). If the main() function returns, the return value shall be passed to the exit() function.


그것의 정의는 이렇다
int \_\_libc\_start\_main(int \*(main) (int, char \* \*, char \* \*), int argc, char \* \* ubp\_av, void (\*init) (void), void (\*fini) (void), void (\*rtld\_fini) (void), void (\* stack\_end));
탈부착 검토
0x6a0: xor ebp, ebp
0x6a2: mov r9, rdx
0x6a5: pop rsi
0x6a6: mov rdx, rsp
0x6a9: and rsp, 0xfffffffffffffff0
0x6ad: push rax
0x6ae: push rsp
0x6af: lea r8, [rip + 0x23a]
0x6b6: lea rcx, [rip + 0x1c3]
**0x6bd: lea rdi, [rip + 0xe6]**
0x6c4: call qword ptr [rip + 0x200916]
0x6ca: hlt
... snip ...
그러나 이번에는lea나Load의 유효한 주소 명령에서 일부 주소 [rip+0xe6]를 rdi 레지스터에 불러옵니다.[rip+0xe6] 계산 결과는 0x7aa입니다. 이것은 마침main () 함수의 주소입니다!내가 어떻게 알아?_ulibc\ustart\umain () 때문에 모든 일을 한 후에 rdi 함수로 넘어갑니다. 보통main () 함수입니다.이렇게 보여요.

main의 어셈블리를 보려면 앞에서 작성한 스크립트 (disas1.py) 의 출력에서 0x7aa를 찾으십시오.
우리가 이전에 발견한 상황을 보면, 모든 호출 명령은 일부 함수를 가리키며, 우리는 다시 위치를 정하는 항목에서 이 함수를 볼 수 있다.그래서 매번 그들의 이전 전화를 받은 후에 우리는 이 점을 볼 수 있다
printf 0x650
fgets 0x660
strcmp 0x670
malloc 0x680
이 모든 것을 한데 놓으니 일이 윤곽이 잡히기 시작했다.여기서 해체의 관건적인 부분을 강조하겠습니다.이것은 해석하기 쉽다.
0x7b2: mov edi, 0xa ; 10
0x7b7: call 0x680 ; malloc
*pw 문자열을 채우는 순환
0x7d0: mov eax, dword ptr [rbp - 0x14]
0x7d3: cdqe    
0x7d5: lea rdx, [rax - 1]
0x7d9: mov rax, qword ptr [rbp - 0x10]
0x7dd: add rax, rdx
0x7e0: movzx eax, byte ptr [rax]
0x7e3: lea ecx, [rax + 1]
0x7e6: mov eax, dword ptr [rbp - 0x14]
0x7e9: movsxd rdx, eax
0x7ec: mov rax, qword ptr [rbp - 0x10]
0x7f0: add rax, rdx
0x7f3: mov edx, ecx
0x7f5: mov byte ptr [rax], dl
0x7f7: add dword ptr [rbp - 0x14], 1
0x7fb: cmp dword ptr [rbp - 0x14], 8
0x7ff: jle 0x7d0
이것은 우리의 strcmp () 처럼 보인다
0x843: mov rdx, qword ptr [rbp - 0x10] ; \*in
0x847: mov rax, qword ptr [rbp - 8] ; \*pw
0x84b: mov rsi, rdx             
0x84e: mov rdi, rax
0x851: call 0x670 ; strcmp  
0x856: test eax, eax ; is = 0? 
0x858: jne 0x868 ; no? jump to 0x868
0x85a: lea rdi, [rip + 0xae] ; "haha yes!" 
0x861: call 0x640 ; puts
0x866: jmp 0x874
0x868: lea rdi, [rip + 0xaa] ; "nah dude"
0x86f: call 0x640 ; puts  
나는 얘가 왜 여기서puts를 사용하는지 모르겠다.내가 뭘 놓쳤을지도 몰라.printf에서put를 호출할 수도 있습니다.내가 틀렸나봐.이 위치들은 사실상 문자열인 "하하, 네!"그리고'아니오, 점원'.

결론


와, 그럼 시간이 많이 걸렸네.하지만 우리는 끝났다.만약 당신이 초보자라면, 이것은 매우 곤혹스럽거나, 심지어는 무슨 일이 일어났는지 이해하지 못할 수도 있다.괜찮아요.읽기와 탐색의 직각을 세우려면 실천이 필요하다.나도 못해.
본고에서 사용한 모든 코드는 여기에 있다. https://github.com/icyphox/asdf/tree/master/reversing-elf
안녕히 계세요. — PE 바이너리 파일언제나

I’m Anirudh, a CS undergrad, focusing on offensive security and digital forensics. I go by “icyphox” on the Internet.


링크

  • GitHub
  • Website

  • 전화[email protected]로 메일을 보내주신 것을 환영합니다.

    좋은 웹페이지 즐겨찾기