케이스1 (ret) 케이스2 (br) 케이스3 (b) 케이스4 (b) 케이스5 (br) 케이스6 (ret)
IDA 7.7 분석 안됨 분석 안됨 분석 안됨 정상 분석 됨 정상 분석 됨 분석 꼬임
Binary Ninja 3.3 분석 안됨 정상 분석 됨 정상 분석 됨 정상 분석 됨 정상 분석 됨 분석 꼬임

 

케이스 1 (ret)

int main(int argc, char const *argv[])
{
    printf("hello world!\n");
    __asm__ volatile(
        "adr x30,0x0 \n\t"
        "add x30,x30,0xc \n\t"
        "ret \n\t"
        );
    printf("test 1\n");
    printf("test 2\n");

    return 0;
}

IDA 7.7 분석 결과

 
방해 asm 이후 함수가 끝난 것으로 표시됨

분석이 asm 이후 되지 않음
분석이 asm 이후 되지 않음

Binary Ninja 3.3.3996 분석 결과

방해 asm 이후 함수가 끝난 것으로 표시됨
분석이 asm 이후 되지 않음

 

케이스 2 (br)

int main(int argc, char const *argv[])
{
    printf("hello world!\n");
    __asm__ volatile(
        "adr x30,0x0 \n\t"
        "add x30,x30,0xc \n\t"
        "br x30 \n\t"
        );
    printf("test 1\n");
    printf("test 2\n");

    return 0;
}

IDA 7.7 분석 결과

분석이 asm 이후 되지 않음

Binary Ninja 3.3.3996 분석 결과

정상적으로 분석 됨

케이스 3 (b)

#include <ctime>
int main(int argc, char const *argv[])
{
    printf("hello world!\n");
   
    // 항상 false가 되어야 실행되지 않는다.
    // 최적화 되지 않기 위해 불투명 술어로 코딩한다.
    if (rand() < 0) {
        __asm__(
            "b 0x4\n"
            ".long 12345678\n"
        );
    }
    printf("test 1\n");
    printf("test 2\n");

    return 0;
}

IDA 7.7 분석 결과

함수 전체가 분석 안됨

Binary Ninja 3.3.3996 분석 결과

정상적으로 분석 됨

 

케이스 4 (b)

#include <ctime>
int main(int argc, char const *argv[])
{
    printf("hello world!\n");
   
    // 항상 false가 되어야 실행되지 않는다.
    // 최적화 되지 않기 위해 불투명 술어로 코딩한다.
    if (rand() < 0) {
        __asm__(
            "b 0x4\n"
            "add sp,sp,#0x100\n"
            "add sp,sp,#0x100\n"
        );
    }
    printf("test 1\n");
    printf("test 2\n");

    return 0;
}

IDA 7.7 분석 결과

정상적으로 분석 됨

Binary Ninja 3.3.3996 분석 결과

정상적으로 분석 됨

케이스 5 (br)

int main(int argc, char const *argv[])
{
    printf("hello world!\n");

    if (rand() < 0) {
        __asm__(
            "mov x8,#0x1\n"
            "adr x9, #0x10\n"
            "mul x8, x9, x8\n"
            ".long 0x12345678\n"
            "br x8\n"
        );
    }
    printf("test 1\n");
    printf("test 2\n");

    return 0;
}

IDA 7.7 분석 결과

정상적으로 분석 됨

Binary Ninja 3.3.3996 분석 결과

정상적으로 분석 됨

케이스 6 (ret)

int main(int argc, char const *argv[])
{
    printf("hello world!\n");

    if (rand() < 0) {
        __asm__(
            "adr x8,#0xc\n"
            "mov x30,x8\n"
            "ret\n"
        );
    }
    printf("test 1\n");
    printf("test 2\n");

    return 0;
}

IDA 7.7 분석 결과

분석 꼬임

Binary Ninja 3.3.3996 분석 결과

분석 꼬임

 

 

제어 흐름 난독화란? if, switch와 같은 제어/분기 구문에 난독화를 하는 것

아래는 예제이다.

package obfuscate.test;

import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import java.util.Random;
import bam.boo.zoo;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Random을 넣은 이유는, 상수를 넣으면 최적화 되어 버린다.
        Random rand = new Random();
        int num = rand.nextInt(100000);
        System.out.println("hohoh!oho " + num);
        zoo.func1(num);

        System.out.println("end!");
    } 
}

난독화 테스트를 위해 제어 흐름을 if 구문을 사용하여 코딩하였다.

package bam.boo;

public class zoo {
    public static void func1(int num)
    {
        if (num > 1)
        {
            System.out.println("if");
        }
        else
        {
            System.out.println("else");
        }
    }
}

zoo class를 decompile하면 아래와 같다.

package a.a;

import java.io.PrintStream;

public class a {
   public static void a(int var0) {
      PrintStream var1;
      String var2;
      if (var0 > 1) {
         var1 = System.out;
         var2 = "if";
      } else {
         var1 = System.out;
         var2 = "else";
      }

      var1.println(var2);
   }
}

아래는 smali 코드이다.

smali 코드로 변환하면서 goto 명령어가 생성되었다.

goto와 같은 분기 명령어에 pass를 적용한 후 dummy 코드를 포함하여 ifne/goto 명령어를 적절히 추가하면 분기가 복잡하게 보이도록 만들 수 있다.

@Override
public void visitProgramClass(ProgramClass programClass)
{
    if ((programClass.getAccessFlags() & AccessConstants.INTERFACE) != 0)
    {
        return;
    }
    // 수를 참조할때 static 변수가 아니면 최적화되어 버리므로 번거롭지만 생성하여야 한다.
    ConstantPoolEditor constantPoolEditor = new ConstantPoolEditor(programClass);

    ClassEditor classEditor = new ClassEditor(programClass);
    int nameIndex = constantPoolEditor.addUtf8Constant("valueX");
    int descriptorIndex = constantPoolEditor.addUtf8Constant("I");
    
    ProgramField opaqueField = new ProgramField(
        AccessConstants.PRIVATE | AccessConstants.STATIC, nameIndex, descriptorIndex, null);
    classEditor.addField(opaqueField);

    new InitializerEditor(programClass).addStaticInitializerInstructions(/*mergeIntoExistingInitializer=*/true,
            // static 변수에 저장
            ____ -> {
                ____.ldc(rand.nextInt(10000)) // 양수 생성
                    .putstatic(programClass, opaqueField);
            });
    programClass.accept(new AllMethodVisitor(new AllAttributeVisitor(this)));
}

@Override
public void visitBranchInstruction(Clazz             clazz,
                                   Method            method,
                                   CodeAttribute     codeAttribute,
                                   int               offset,
                                   BranchInstruction branch)
{
    // goto 명령어에만 적용한다.
    if (branch.opcode != Instruction.OP_GOTO) {
        return;
    }
    InstructionSequenceBuilder ____ = new InstructionSequenceBuilder((ProgramClass)clazz);
    FrameFinder finder = new FrameFinder(this.partialEvaluator, offset);
    codeAttribute.instructionsAccept(clazz, method, finder);
    if (!finder.targets.isEmpty())
    {
            // x + 1 != 0
            // x 값으로 양수를 생성했으므로 위의 수식은 무조건 성립한다.
            ____.getstatic(clazz.getName(), "valueX", "I") // 위에서 생성한 x값
                .iconst_1() // 상수 1
                .iadd() // 덧셈
                .ifne(branch.branchOffset) // 0이 아니면 분기
                // 여기서부터 절대 실행되면 안됨(dummy)
                .getstatic("java/lang/System", "out", "Ljava/io/PrintStream;")
                .ldc("never seen")
                .invokevirtual("java/io/PrintStream", "println", "(Ljava/lang/String;)V")
                .goto_(finder.targets.get(rand.nextInt(finder.targets.size())) - offset);
                // 끝
                
                // 명령어 교체
                codeAttributeEditor.replaceInstruction(offset, ____.instructions());
    }
}

위 pass를 적용하면 goto 명령어가 getstatic ~ goto로 대체된다.

apk 빌드 후 smali 코드를 확인해 보면 아래와 같다.

위에서는 ifne가 goto 명령어를 대체하므로, 무조건 참이 되는 수식을 작성하여 원래 가고자 했던 곳으로 점프시킨다.

그 밑에는 실행되면 안되는 fake코드를 넣고, 마지막에는 goto 명령어를 넣어서 임의의 위치를 가리키게 한다.

 

java 코드로 변환하면 아래와 같이 난독화 된 것을 볼 수 있다.

x64 Native Tools Command Prompt for VS 2022 프롬프트 실행

git clone https://github.com/curl/curl.git
cd curl
buildconf.bat
Set RTLIBCFG=static
cd winbuild
nmake /f MakeFile.vc mode=static vc=17

빌드는 아래 경로에 생성 됨

curl\builds\libcurl-vc17-x64-release-static-ipv6-sspi-schannel

curl.zip
1.25MB

프로젝트 설정

#include <curl.h>
int main()
{
    CURL* curl = curl_easy_init();
    CURLcode res;
}

 

'Windows > Dev' 카테고리의 다른 글

detours 빌드 및 적용  (0) 2024.04.11
[VC++] string deallocate  (0) 2022.05.23
get EIP (gcc / vc)  (0) 2019.04.11
C용 초경량 XML 파서 : Mini-XML 소개 및 사용법  (0) 2017.06.29
x86 __usercall 함수 후킹하기  (0) 2017.05.11

+ Recent posts