zer0pts ctf - shredder

shredder

#hard #reversing
I’ve accidentally removed an important file using a strong shredder. Can you restore the file from its core dump?
author: ptr-yudai

문제 파일 (core)

문제에서 주어진 것은 core 파일 하나 입니다.

$ file core
core: ELF 64-bit LSB core file x86-64, version 1 (SYSV), SVR4-style, from './shredder document.pdf'

우선 binwalk 툴을 사용하여 elf를 추출가능합니다.

$ binwalk -e core

IDA로 열어보니 symbol이 전혀 없어서 symbol을 직접 복구하는 것부터 시작했습니다.

참고. 해당 elf파일을 core파일과 함께 gdb를 이용했으면 훨씬 분석이 쉬웠지만 실제 문제를 풀이할때 그렇게 하지 못했기에 다른 풀이로 적었습니다.

core 파일 분석

readelf -l core 명령어를 통해 core파일의 어느 부분이 실행 당시의 어느 메모리 부분인지 알 수 있습니다.

$ readelf -l core

Elf file type is CORE (Core file)
Entry point 0x0
There are 19 program headers, starting at offset 64

Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
NOTE 0x0000000000000468 0x0000000000000000 0x0000000000000000
0x0000000000000cfc 0x0000000000000000 0
LOAD 0x0000000000002000 0x00005635b10a3000 0x0000000000000000
0x0000000000001000 0x0000000000001000 R E 1000
LOAD 0x0000000000003000 0x00005635b12a4000 0x0000000000000000
0x0000000000001000 0x0000000000001000 R 1000
LOAD 0x0000000000004000 0x00005635b12a5000 0x0000000000000000
0x0000000000001000 0x0000000000001000 RW 1000
LOAD 0x0000000000005000 0x00005635b211a000 0x0000000000000000
0x0000000000021000 0x0000000000021000 RW 1000
LOAD 0x0000000000026000 0x00007ffbf0a7b000 0x0000000000000000
0x0000000000001000 0x00000000001e7000 R E 1000
LOAD 0x0000000000027000 0x00007ffbf0c62000 0x0000000000000000
0x0000000000000000 0x0000000000200000 1000
LOAD 0x0000000000027000 0x00007ffbf0e62000 0x0000000000000000
0x0000000000004000 0x0000000000004000 R 1000
LOAD 0x000000000002b000 0x00007ffbf0e66000 0x0000000000000000
0x0000000000002000 0x0000000000002000 RW 1000
LOAD 0x000000000002d000 0x00007ffbf0e68000 0x0000000000000000
0x0000000000004000 0x0000000000004000 RW 1000
LOAD 0x0000000000031000 0x00007ffbf0e6c000 0x0000000000000000
0x0000000000001000 0x0000000000027000 R E 1000
LOAD 0x0000000000032000 0x00007ffbf1059000 0x0000000000000000
0x0000000000002000 0x0000000000002000 RW 1000
LOAD 0x0000000000034000 0x00007ffbf1093000 0x0000000000000000
0x0000000000001000 0x0000000000001000 R 1000
LOAD 0x0000000000035000 0x00007ffbf1094000 0x0000000000000000
0x0000000000001000 0x0000000000001000 RW 1000
LOAD 0x0000000000036000 0x00007ffbf1095000 0x0000000000000000
0x0000000000001000 0x0000000000001000 RW 1000
LOAD 0x0000000000037000 0x00007ffd8fd8e000 0x0000000000000000
0x0000000000021000 0x0000000000021000 RW 1000
LOAD 0x0000000000058000 0x00007ffd8fdb8000 0x0000000000000000
0x0000000000003000 0x0000000000003000 R 1000
LOAD 0x000000000005b000 0x00007ffd8fdbb000 0x0000000000000000
0x0000000000001000 0x0000000000001000 R E 1000
LOAD 0x000000000005c000 0xffffffffff600000 0x0000000000000000
0x0000000000001000 0x0000000000001000 R E 1000

pwntools에는 core파일을 분석하기 위한 라이브러리가 포함되어있습니다. 이것을 이용하면 다양한 정보를 얻을 수 있습니다.

1
2
3
4
5
6
7
8
9
10
> >>> from pwn import *
> >>> core = Corefile('core')
[x] Parsing corefile...
[*] '/zer0pts/shredder_a3aae70d492b9f2f5dfa4322f3a82d2a/core'
Arch: amd64-64-little
RIP: 0x7ffbf0ab9e97
RSP: 0x7ffd8fdad5b0
Exe: '/home/ptr/colony/zer0ptsCTF/shredder/challenge/shredder' (0x5635b10a3000)
Fault: 0x3e800003fd1
[+] Parsing corefile...: Done

우선 위에서 symbol을 복구해야 했으므로 libc의 버전이 무엇인지, base address가 무엇인지 알아내야 했으므로 memory mapping정보를 알아내기로 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
>>> mappings = core.mappings
>>> for mapping in mappings:
... print mapping
...
5635b10a3000-5635b10a4000 r-xp 1000 /home/ptr/colony/zer0ptsCTF/shredder/challenge/shredder
5635b12a4000-5635b12a5000 r--p 1000 /home/ptr/colony/zer0ptsCTF/shredder/challenge/shredder
5635b12a5000-5635b12a6000 rw-p 1000 /home/ptr/colony/zer0ptsCTF/shredder/challenge/shredder
5635b211a000-5635b213b000 rw-p 21000
7ffbf0a7b000-7ffbf0c62000 r-xp 1e7000 /lib/x86_64-linux-gnu/libc-2.27.so
7ffbf0c62000-7ffbf0e62000 ---p 200000 /lib/x86_64-linux-gnu/libc-2.27.so
7ffbf0e62000-7ffbf0e66000 r--p 4000 /lib/x86_64-linux-gnu/libc-2.27.so
7ffbf0e66000-7ffbf0e68000 rw-p 2000 /lib/x86_64-linux-gnu/libc-2.27.so
7ffbf0e68000-7ffbf0e6c000 rw-p 4000
7ffbf0e6c000-7ffbf0e93000 r-xp 27000 /lib/x86_64-linux-gnu/ld-2.27.so
7ffbf1059000-7ffbf105b000 rw-p 2000
7ffbf1093000-7ffbf1094000 r--p 1000 /lib/x86_64-linux-gnu/ld-2.27.so
7ffbf1094000-7ffbf1095000 rw-p 1000 /lib/x86_64-linux-gnu/ld-2.27.so
7ffbf1095000-7ffbf1096000 rw-p 1000
7ffd8fd8e000-7ffd8fdaf000 rw-p 21000 [stack]
7ffd8fdb8000-7ffd8fdbb000 r--p 3000
7ffd8fdbb000-7ffd8fdbc000 r-xp 1000 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 1000 [vsyscall]

libc버전은 2.27이며 base address7ffbf0a7b000라는것을 알 수 있습니다.

다시 IDA로 돌아가서 symbol을 복구해보기로 합니다.

7FFBF0B12950에 위치한 함수의 이름을 알아내는걸 예시로 하나 해보겠습니다.

LOAD:0000000000201F58 qword_201F58    dq 7FFBF0B12950h        ; DATA XREF: LOAD:00000000000006D0↑o

7FFBF0B12950에서 base address를 빼면 libc파일에서의 offset이 나오게 됩니다.

>>> hex(0x7FFBF0B12950 - 0x7ffbf0a7b000)
'0x97950L'

해당 libc를 불러와서 해당 offset에 있는 symbol이 뭔지 알아낼 수 있습니다.

1
2
3
4
5
>>> from pwn import *
>>> libc = ELF('/lib/x86_64-linux-gnu/libc-2.27.so')
>>> symbols = dict(zip(libc.symbols.values(),libc.symbols.keys()))
>>> p[0x97950]
u'cfree'

그렇게 어느정도 symbol을 복구해주었습니다.

복구후에 분석을 진행하면 대충 이런 코드 인것을 알 수 있었습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
signed __int64 __fastcall sub_A88(__int64 filename)
{
unsigned int v2; // eax
int i; // [rsp+18h] [rbp-B8h]
unsigned int v4; // [rsp+1Ch] [rbp-B4h]
_BYTE *v5; // [rsp+20h] [rbp-B0h]
char null; // [rsp+28h] [rbp-A8h]
struct timeval time; // [rsp+30h] [rbp-A0h]
char v8; // [rsp+40h] [rbp-90h]
__int64 v9; // [rsp+70h] [rbp-60h]

if ( (unsigned int)sub_D50(filename, (__int64)&v8) )
return 1LL;
v4 = j_open64(filename, 2LL);
if ( (v4 & 0x80000000) != 0 )
return 1LL;
j___vdso_gettimeofday((__int64)&time, (__int64)&null);
v2 = sqadd(time.tv_sec, time.tv_usec);
j_srandom(v2);
v5 = (_BYTE *)j_malloc(v9);
j___lseek(v4, 0LL, 0LL);
j___read(v4, v5, v9);
for ( i = 0; i < v9; ++i )
v5[i] ^= j_rand();
j___lseek(v4, 0LL, 0LL);
j_write(v4, v5, v9);
j_free(v5);
j_close(v4);
if ( (unsigned int)j_unlink(filename) )
return 1LL;
j_write(1LL, "Press ENTER to quit...", 22LL);
j___read(0LL, 0LL, 1LL);
return 0LL;
}

이제 해야하는 것은 크게 두 가지 입니다. srand에 무슨 값이 쓰였는지와 암호화된 파일 shredder document.pdf의 추출입니다.

우선 쉬워보이는 pdf추출 부터 해보기로 했습니다.
코드상으로 파일을 읽어온 뒤, malloc으로 heap에 할당했기에 heap을 뒤져보면..

1
5635b211a000-5635b213b000 rw-p 21000

heap영역은 5635b211a000부터 시작하는걸 알고 있고, readelf를 통해 얻은 정보로는

1
2
LOAD           0x0000000000005000 0x00005635b211a000 0x0000000000000000
0x0000000000021000 0x0000000000021000 RW 1000

core파일의 0x5000번째부터 heap에 대한 정보인 것을 알 수 있습니다.

0x5260 번째부터 데이터가 시작되는걸 확인할 수 있었고, 여유롭게 0x8f0f 까지 추출하여 encrypted.pdf로 저장합니다.

이제 srand에 무슨 값이 들어갔는지 알아보기 위해 stack에 무슨 값이 있었는지 전부 저장합니다.

1
2
3
>>> f = open('stack.dat', 'w')
>>> f.write(core.stack.data)
>>> f.close()

sub_A88함수의 rbpstack에서 어디에 있는지 알아내기 위해서 앞서 구했던 heap주소를 이용해봅니다.

1
2
>>> hex(0x00005635b211a000 + 0x260)
'0x5635b211f260'
1
2
_BYTE *v5; // [rsp+20h] [rbp-B0h]
v5 = (_BYTE *)j_malloc(v9);

stack에서 \x60\xa2\x11\xb2\x35\x56값을 들고 있는게 v5이고 rbp-0xb0인것을 이용합니다.

0x1fed0rbp-0xb0인 것을 알았으니 struct timeval time; // [rsp+30h] [rbp-A0h] 값이 F6 F5 23 5E 00 00 00 00 D8 17 0F 00 00 00 00 00 인것을 알 수 있습니다.

이제 srand값도 다 구했습니다.
srand((unsigned int)(0x5e23f5f6 * 0x5e23f5f6 + 0x0f17d8 * 0x0f17d8));

복호화를 진행합니다.

solve.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from ctypes import CDLL

libc = CDLL('/lib/x86_64-linux-gnu/libc-2.27.so')
libc.srand(0x5e23f5f6 * 0x5e23f5f6 + 0x0f17d8 * 0x0f17d8)

with open('encrypted.pdf', 'rb') as f:
enc = f.read()

dec = b''

for ch in enc:
dec += bytes([ch ^ (libc.rand() & 0xff)])

with open('decrypted.pdf', 'wb') as f:
f.write(dec)

정상적으로 복호화가 완료되었습니다.

Share