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

[열심히 만든 리버싱 문제를 날먹으로부터 지키는 방법](https://dreamhack.io/forum/community/free-lounge/posts/1436-%EC%97%B4%EC%8B%AC%ED%9E%88-%EB%A7%8C%EB%93%A0-%EB%A6%AC%EB%B2%84%EC%8B%B1-%EB%AC%B8%EC%A0%9C%EB%A5%BC-%EB%82%A0%EB%A8%B9%EC%9C%BC%EB%A1%9C%EB%B6%80%ED%84%B0-%EC%A7%80%ED%82%A4%EB%8A%94-%EB%B0%A9%EB%B2%95/)

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

## 서론

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

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

## 문제 풀이

해결한 문제는 [Rev Level-3 Call more functions](https://dreamhack.io/wargame/challenges/1422) 이다.

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

```c
__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()`을 호출하여 입력된 값이 플래그와 일치하는지 검증 과정을 수행한다.

```c
__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_1311`은 `dword_5088`을 리턴하고 있다. 다시 위로 올라가 main 함수를 살펴보면 `v3` 이 1이면 검증 과정을 실패한다. 즉, `dword_5088`이 0이 되는 값을 찾는 것이 우리의 목표이다.

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

```c
_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에 히트했을 때 실행하는 간단한 스크립트를 만들 수 있다.

```bash
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의 뒤부터 값을 변경해나간다.

```bash
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번째 히트한 부분까지 모두 출력된다.

```bash
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바이트의 값을 날먹으로 모두 구할 수 있다.

```python
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을 사용하여 자동화 가능하다.

## 결론

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