Skip to main content

Command Palette

Search for a command to run...

열심히 만든 리버싱 문제를 날먹하는 방법

Updated

열심히 만든 리버싱 문제를 날먹으로부터 지키는 방법

이 글을 읽기 전에 위의 드림핵 커뮤니티에 올라온 글을 보면 이해하기 수월하다.

서론

리버싱 문제는 출제자가 의도한 대로 정석적인 방법으로 역연산을 하여 플래그를 구할 수도 있지만, 편법을 사용하여 날먹하는 것이 가능하다.

편법에는 여러가지 방법이 있지만 위 드림핵 커뮤니티 글을 읽고 아이디어를 얻어 리버싱 문제를 해결했기 때문에 그 방법을 소개하고자 한다.

문제 풀이

해결한 문제는 Rev Level-3 Call more functions 이다.

아래에는 Writeup이 존재하기 때문에 문제를 온전하게 즐기고 싶은 사람은 아쉽지만 뒤로 가기를 눌러주길 바란다.

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  unsigned int v3; // ebx

  sub_11E9(a1, a2, a3);
  __printf_chk(1LL, "Input: ");
  __isoc99_scanf("%65s", s);
  v3 = 1;
  if ( strlen(s) == 64 )
  {
    v3 = sub_1311();
    if ( v3 )
    {
      puts(":(");
      return 0;
    }
    else
    {
      __printf_chk(1LL, "Correct! The flag is DH{%s}\n", s);
    }
  }
  return v3;
}

먼저 Input 값의 길이가 64바이트인지 확인한다. 그 후, sub_1311()을 호출하여 입력된 값이 플래그와 일치하는지 검증 과정을 수행한다.

__int64 sub_1311()
{
  sub_1236(63LL);
  sub_1257();
  sub_1236(51LL);
  sub_12CB();
  sub_1236(62LL);
  sub_1257();
  sub_1290();
  sub_1236(4LL);
  sub_12CB();
  sub_1236(61LL);
  sub_1257();
  sub_1236(101LL);
  sub_12CB();
  sub_1236(60LL);
  sub_1257();
  sub_1290();
  sub_1290();
  sub_1236(80LL);
  sub_12CB();
  sub_1236(59LL);
  sub_1257();
  sub_1236(102LL);
  ...
  return (unsigned int)dword_5088;
}

sub_1311을 열어보면 많은 양의 함수가 호출되는 것을 볼 수 있다. 여기서 호출되는 모든 함수를 분석하는 것은 매우 복잡한 일이다. 따라서 우리는 마지막 return 값에 주목할 필요가 있다.

sub_1311dword_5088을 리턴하고 있다. 다시 위로 올라가 main 함수를 살펴보면 v3 이 1이면 검증 과정을 실패한다. 즉, dword_5088이 0이 되는 값을 찾는 것이 우리의 목표이다.

이제 sub_1311에서 호출하는 함수들을 살펴볼 차례이다. 우리는 날먹을 하는 것이 목표이기 때문에 모든 함수를 자세히 살펴볼 필요가 없다. dword_5088에 직접적인 연관을 주는 함수만 찾아본다.

_BOOL8 sub_12CB()
{
  _BOOL8 result; // rax

  if ( (unsigned int)dword_508C <= 1 )
    exit(1);
  result = byte_50A0[dword_508C - 2] != byte_50A0[dword_508C - 1];
  dword_5088 |= result;
  --dword_508C;
  return result;
}

sub_12CB에서 result를 계산하고 dword_5088에 or 연산을 통해 값을 업데이트 해주고 있다. result를 계산하는 부분이 앞에서 입력 값을 전처리한 다음, 저장된 값과 비교하는 핵심 부분일 것으로 추정된다.

여기서 주목할 것은 or 연산을 하기 때문에 한번 1로 고정되면 그 값이 변하지 않는 점이다.

dword_5088 |= result; 에 bp를 걸어주고 결과 값이 1인지 0인지 비교하는 스크립트를 사용하도록 하자. gdb에서 bp에 히트했을 때 실행하는 간단한 스크립트를 만들 수 있다.

gdb-peda$ b *0x00005555555552f6
Breakpoint 1 at 0x5555555552f6
gdb-peda$ command 1
Type commands for breakpoint(s) 1, one per line.
End with a line saying just "end".
>if $eax == 1
 >info b
 >continue
 >end
>if $eax == 0
 >continue
 >end
>end

연산한 결과 값은 eax 레지스터에 저장되기 때문에 eax의 값을 비교하는 것으로 dword_5088의 값이 변화하는 것을 추적 가능하다. 이 문제에서 값의 비교는 마지막 문자부터 일어나기 때문에 payload의 뒤부터 값을 변경해나간다.

gdb-peda$ ru
Starting program: /home/skybridge/workspace/dreamhack/rev/Level-3/Call_more_functions/main 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Input: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x00005555555552f6 
 breakpoint already hit 1 time
        if $eax == 1
          info b
          continue
        end
        if $eax == 0
          continue
        end

Breakpoint 1, 0x00005555555552f6 in ?? ()
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x00005555555552f6 
 breakpoint already hit 2 times
        if $eax == 1
          info b
          continue
        end
        if $eax == 0
          continue
        end

...

eax가 1인 경우에만 info b를 실행하기 때문에 첫 번째 문자가 틀렸다면 1번째 히트했을 때부터 64번째 히트한 부분까지 모두 출력된다.

gdb-peda$ ru
Starting program: /home/skybridge/workspace/dreamhack/rev/Level-3/Call_more_functions/main 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Input: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa3

Breakpoint 1, 0x00005555555552f6 in ?? ()
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x00005555555552f6 
 breakpoint already hit 2 times
        if $eax == 1
          info b
          continue
        end
        if $eax == 0
          continue
        end

Breakpoint 1, 0x00005555555552f6 in ?? ()
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x00005555555552f6 
 breakpoint already hit 3 times
        if $eax == 1
          info b
          continue
        end
        if $eax == 0
          continue
        end
...

이번에는 브루트포스를 통해 뒤의 1바이트를 맞춘 상태이다. 이번에는 두 번째 bp에 히트한 부분부터 출력되었다. 그 이유는 첫번째 bp에서는 dword_5088 값이 0이었기 때문에 gdb 스크립트에 의해 출력되지 않은 것이다.

이 방식으로 64바이트의 값을 날먹으로 모두 구할 수 있다.

from pwn import *

p = process(['gdb', './main'])

p.sendline('r')
p.sendline('12345')
p.sendline('b *0x00005555555552f6')

p.sendline('command 1')
p.sendline('if $eax == 1')
p.sendline('info b')
p.sendline('continue')
p.sendline('end')
p.sendline('if $eax == 0')
p.sendline('continue')
p.sendline('end')
p.sendline('end')


flag = b'a' * 64
flag = bytearray(flag)

letter = b'abcdef0123456789'

for i in range(len(flag)):
    idx = 63 - i
    for c in letter:
        flag[idx] = c # 뒤에서부터 한글자씩 바꾸기
        print(flag)
        p.sendline('run')
        p.sendlineafter(b'Input: ', flag)
        p.recvuntil(b'breakpoint already hit ')
        attempt = int(p.recvuntil(b' ')[:-1])
        if attempt == (i + 1):  #아직 안맞는다면
            continue
        else: #정답을 찾음
            break

print(flag)

p.interactive()

물론 이 모든 과정을 pwntools을 사용하여 자동화 가능하다.

결론

이 방법은 일종의 편법이라 할 수 있다. 리버싱 문제 해결의 중간 과정을 완전히 생략해버리기 때문에 분석 능력 향상에 큰 도움이 되지 않는다고 생각한다. 모든 문제 날먹을 권장하는 것이 아닌 여러가지 리버싱 문제 접근 방법 중 한 가지로 생각해 주었으면 한다.

More from this blog

Android 악성 앱, 왜 여전히 뚫릴까? Louvain 그래프 탐지법이 그 해답일지도!

“바이러스는 진화한다. 그럼 탐지법도 진화해야 하지 않을까?” 악성 앱은 더 똑똑해졌습니다. 단순히 특정 API를 호출하는지 감지하는 방식은 더 이상 통하지 않죠. API가 어떻게 엮이는지, 함께 등장할 때 어떤 ‘행위 패턴’을 만드는지까지 보는 시대입니다. 그리고 바로 여기서, “동시 출현 그래프”와 “커뮤니티 탐지”라는 키워드가 등장합니다. 개념 드리프트, 악성 탐지의 맹점 악성 앱 탐지 모델이 시간이 지나면 무력해지는 현상, 알고 계셨나...

Jul 28, 2025

[WriteUp] / [CryptoHack] Resisting Bruteforce

해석 블록 암호가 안전하려면, 공격자가 AES의 출력과 무작위 비트 배열을 구분할 수 없어야 한다. 또한, 키를 무작위로 전부 시도하는(브루트포싱) 것보다 더 효율적인 복호화 방법이 있어서는 안 된다. 그래서 이론적으로라도 브루트포싱보다 적은 연산으로 복호화가 가능하다면, 그 암호는 '깨졌다고(broken)' 간주된다. 128비트 키를 브루트포싱하는 건 얼마나 어려울까? 누군가 계산했는데, 비트코인 전체 채굴 연산력을 동원해도 우주의 나이보다 ...

Jul 28, 2025
A

Aegis

5 posts