열심히 만든 리버싱 문제를 날먹하는 방법
이 글을 읽기 전에 위의 드림핵 커뮤니티에 올라온 글을 보면 이해하기 수월하다.
서론
리버싱 문제는 출제자가 의도한 대로 정석적인 방법으로 역연산을 하여 플래그를 구할 수도 있지만, 편법을 사용하여 날먹하는 것이 가능하다.
편법에는 여러가지 방법이 있지만 위 드림핵 커뮤니티 글을 읽고 아이디어를 얻어 리버싱 문제를 해결했기 때문에 그 방법을 소개하고자 한다.
문제 풀이
해결한 문제는 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_1311은 dword_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을 사용하여 자동화 가능하다.
결론
이 방법은 일종의 편법이라 할 수 있다. 리버싱 문제 해결의 중간 과정을 완전히 생략해버리기 때문에 분석 능력 향상에 큰 도움이 되지 않는다고 생각한다. 모든 문제 날먹을 권장하는 것이 아닌 여러가지 리버싱 문제 접근 방법 중 한 가지로 생각해 주었으면 한다.