Home > 졸업프로젝트 > Ghidra P-Code 알아보기 2

Ghidra P-Code 알아보기 2
Ghidra P-Code

실전 Raw P-Code 분석(using Java)

 지난 주에 Raw P-Code가 뭔지 등 이론은 배웠으니 여기서는 바로 실전으로 들어가보자.

import ghidra.app.script.GhidraScript;
import ghidra.program.model.listing.*;
import ghidra.program.model.lang.*;
import ghidra.program.model.pcode.PcodeOp;
import ghidra.program.model.pcode.Varnode;

public class RawPcodeAnalyzer extends GhidraScript {
    @Override
    public void run() throws Exception {
        // currentProgram.getFunctionManager()를 통해 현재 커서 주소(currentAddress)를 포함하는 함수 취득
        FunctionManager fm = currentProgram.getFunctionManager();
        Function func = fm.getFunctionContaining(currentAddress);

        if (func == null) {
            println("커서를 함수 내부에 위치시켜주세요.");
            return;
        }

        Listing listing = currentProgram.getListing();

        // 1. 현재 함수의 모든 어셈블리 명령어 순회
        for (Instruction instr : listing.getInstructions(func.getBody(), true)) {
            PcodeOp[] ops = instr.getPcode();
            if (ops == null) continue;

            for (PcodeOp op : ops) {
                // 2. 관심 있는 Opcode 필터링 (간접 호출 및 일반 호출)
                int opcode = op.getOpcode();
                if (opcode == PcodeOp.CALL || opcode == PcodeOp.CALLIND) {

                    Varnode target = op.getInput(0);
                    String targetName = getVarnodeName(target);

                    println(String.format("[0x%s] %s -> 타겟: %s",
                            instr.getAddress(), op.getMnemonic(), targetName));
                }
            }
        }
    }

    // 3. Raw Varnode를 사람이 읽을 수 있는 레지스터/상수로 변환하는 헬퍼 함수
    private String getVarnodeName(Varnode vn) {
        if (vn == null) return "null";

        if (vn.isConstant()) {
            return String.format("0x%x", vn.getOffset());
        }
        else if (vn.isRegister()) {
            Register reg = currentProgram.getRegister(vn);
            return (reg != null) ? reg.getName() : "Reg(0x" + Long.toHexString(vn.getOffset()) + ")";
        }
        return vn.toString();
    }
}

이번 시간에 분석해볼 코드다. 해당 코드를 실행해보면
제목
Listing 창에 커서를 올린 함수 구역 내 Raw P-Code에서 Call 및 Callind를 호출한 모든 P-Code를 잡아낸 것을 볼 수 있다. 이 코드를 알아보자.

일반적 자바 프로그래밍과의 차이점

 해당 스크립트가 자바로 작성되긴 하지만, 일반적인 자바 프로그래밍이 아닌 프레임워크 위에서 동작하는 플러그인의 형태를 띄는 만큼 약간 다른 점들이 존재한다.
먼저 자바 프로그램은 main 함수에서 시작되지만, 기드라 스크립트는 독립적인 프로그램이 아니므로 GhidraScript를 상속받아 run() 메서드를 오버라이드 해야한다. 또한 System.out.println()을 호출해 디버깅하지 않고 GhidraScript()에서 자체 제공하는 println(), printf() 함수 등을 사용하여 기드라 화면에 출력하게 된다.

분석

FunctionManager fm = currentProgram.getFunctionManager();
Function func = fm.getFunctionContaining(currentAddress);

if (func == null) {
    println("커서를 함수 내부에 위치시켜주세요.");
    return;
}
Listing listing = currentProgram.getListing();

 먼저 실행하는 순간 currentProgram.getFunctionManager()를 통해 FunctionManager형 변수를 하나 만든 것을 볼 수 있다. 이는 현재 분석중인 프로그램 내 모든 함수 명부(?)를 관리하는 객체인 FunctionManager를 불러오는 역할을 수행한다. 쉽게 생각하면 내가 해당 스크립트에서 함수 단위로 무언가를 하고 싶으면 일단 이 함수를 호출하면 된다. 이 객체를 통해 특정 함수를 찾거나, 바이너리 내 전체 함수를 순회하거나, 또는 새로운 함수를 만들어낼 수도 있다. 이 객체에서 getFunctionContaning(currentAddress)를 통해 무언갈 가져온 것을 볼 수 있는데, currentAddress를 통해 Listing 창에서 내가 커서를 올린 현재 메모리 지점 함수 주소를 가지고 있는 함수를 가져오라는 명령으로 이해하면 된다. 그리고 가져온 함수가 없을 때 예외처리를 수행했다.
 그리고 동일하게 현재 프로그램에서 getListing()을 통해서 Listing을 가져오는 것을 볼 수 있다. 이는 가져온 함수는 이 함수가 0x@@부터 0x##까지다 라는 값만 가지고 있어 어셈블리 및 p-code를 가져오고 싶다면 Listing이란 객체를 가져와서 해당 영역에 내에 있는 Instruction들을 가져오기 위함이다.

        // 1. 현재 함수의 모든 어셈블리 명령어 순회
        for (Instruction instr : listing.getInstructions(func.getBody(), true)) {
            PcodeOp[] ops = instr.getPcode();
            if (ops == null) continue;

            for (PcodeOp op : ops) {
                // 2. 관심 있는 Opcode 필터링 (간접 호출 및 일반 호출)
                int opcode = op.getOpcode();
                if (opcode == PcodeOp.CALL || opcode == PcodeOp.CALLIND) {

                    Varnode target = op.getInput(0);
                    String targetName = getVarnodeName(target);

                    println(String.format("[0x%s] %s -> 타겟: %s",
                            instr.getAddress(), op.getMnemonic(), targetName));
                }
            }
        }

  바로 for문을 통해 어떻게 순회하는지를 보여준다. listing.getInstruction()을 통해 InstructionIterator라는 이터레이터가 반환이 된다. 괄호 안에는 처음에는 어떤 범위 내의 instruction을 가져올건지를, 두번쨰로는 순방향, 역방향 탐색 여부를 넣는다. true일 경우 순반향 순회이다. 여기선 커서가 가리키는 함수 범위 내의 Instruction을 가져온다 정도로 인지하면 된다.
  이후 getPcode()를 통해 해당 instruction이 어떤 P-Code들로 이뤄져있는지 배열을 담는다. 이는 하나의 Instruction이 여러 P-Code로 분해될 수 있기 떄문이다. 이후 해당 P-Code들을 대상으로 순회를 다시 수행한다. 먼저 해당 P-Code의 opcode를 getOpcode()를 통해 가져온다. 이 때 가져온 opcode가 정수형인 것을 알 수 있다. 이는 빠른 처리가 필요할 시 사용되며, String 형태로 반환을 원한다면 getMnemonic()을 사용하면 된다.
  이제 if문을 보면 받은 opcode가 CALL이거나 CALLIND인 경우 첫 번째 인자 Varnode를 가져오고, getVarnodeName()이란 함수를 통해 타겟 Varnode를 가져온 것을 볼 수 있다.

    private String getVarnodeName(Varnode vn) {
        if (vn == null) return "null";

        if (vn.isConstant()) {
            return String.format("0x%x", vn.getOffset());
        }
        else if (vn.isRegister()) {
            Register reg = currentProgram.getRegister(vn);
            return (reg != null) ? reg.getName() : "Reg(0x" + Long.toHexString(vn.getOffset()) + ")";
        }
        return vn.toString();
    }

이는 따로 만든 헬퍼 함수로, 해당 Varnode의 타입이 상수인지, 레지스터에 들어가는지, 메모리에 들어가는지 등에 따라 사람이 읽기 쉽도록 처리해준 함수다. Varnode에서 값을 가져올 땐 getOffset()을 통해 가져올 수 있으며, 지난 시간에 이야기했듯 레지스터가 한 줄로 길게 매핑이 되어있으므로 레지스터의 경우 currentProgram 내 getRegister 함수를 통해 이 Varnode가 매핑된 실제 레지스터 객체를 가져온 뒤 해당 레지스터명을 getName()을 통해 가져오고, 예쁜 형태로 반환해주었다. 아무것도 해당되지 않을 경우 기드라 자체 제공 기본 문자열 형태열로 반환한다.

그렇게 CALL 또는 CALLIND에 해당하는 Raw P-Code를 문자열 형태로 변환한 뒤 출력해줌으로써 스크립트를 마무리한다.