BYTECTF2021-byteview

Posted on Dec 20, 2021

漏洞分析

比赛的时候分析了很久这道题,结果也没做出来,现在想想主要 C++ 逆向不熟悉。

在 new content 时,如果之前已经 new 过了,就会进 case1。

      case 1u:                                  // new content
        v17 = *(_QWORD *)v5;
        if ( *(_QWORD *)v5 )
        {
          v21 = *(_QWORD *)v5;
          v7[6].m128i_i64[0] = (unsigned __int64)menu ^ v17 & 0xFFFFFFFF0000LL;
          v7[6].m128i_i32[2] = *(_QWORD *)(v17 + 32);
          v18 = (struct uniq_ptr_task_req *)operator new(0x18uLL);
          v18->choice = 1;                      // 另外一种 add 的情况
          ref = v18;
          *(__m128i *)&v18->old_content = _mm_unpacklo_epi64((__m128i)v21, (__m128i)(unsigned __int64)v7);// old content
        }

这里面有个 v18->old_content,引用了上一个 content。

LABEL_5:
        if ( ref )
          std::default_delete<page_hander>::operator()((struct uniq_ptr_task_req *)ref);
        break;

在每个 switch 结束时,会跳到这里,如果 std::move 过去的指针在 task_handle 函数中没有被析构,就会在这里析构,比赛的时候没把析构函数点开来,感觉非常可惜。

void __fastcall std::default_delete<page_hander>::operator()(struct uniq_ptr_task_req *a1)
{
  unsigned int *v2; // rbp
  __int64 **v3; // rbx
  _QWORD *v4; // rbx
  void *v5; // rdi
  unsigned int *v6; // rdi
  _QWORD *v7; // rbx
  void *v8; // rdi
  unsigned int *v9; // rdi

  if ( a1 )
  {
    v2 = a1->old_content;
    if ( a1->old_content )
    {
      v3 = (__int64 **)*((_QWORD *)v2 + 3);
      for ( *(_QWORD *)v2 = content::add_func_ptr; v3; v3 = (__int64 **)*v3 )
        free(v3[2]);
      v4 = (_QWORD *)*((_QWORD *)v2 + 10);
      while ( v4 )
      {
        v5 = v4;
        v4 = (_QWORD *)*v4;
        operator delete(v5);
      }
      memset(*((void **)v2 + 8), 0, 8LL * *((_QWORD *)v2 + 9));
      v6 = (unsigned int *)*((_QWORD *)v2 + 8);
      *((_QWORD *)v2 + 11) = 0LL;
      *((_QWORD *)v2 + 10) = 0LL;
      if ( v6 != v2 + 28 )
        operator delete(v6);
      v7 = (_QWORD *)*((_QWORD *)v2 + 3);
      while ( v7 )
      {
        v8 = v7;
        v7 = (_QWORD *)*v7;
        operator delete(v8);
      }
      memset(*((void **)v2 + 1), 0, 8LL * *((_QWORD *)v2 + 2));
      v9 = (unsigned int *)*((_QWORD *)v2 + 1);
      *((_QWORD *)v2 + 4) = 0LL;
      *((_QWORD *)v2 + 3) = 0LL;
      if ( v9 != v2 + 14 )
        operator delete(v9);
      operator delete(v2, 0x80uLL);
    }
    operator delete(a1, 0x18uLL);
  }
}

old_content 一起析构掉。显然析构函数是优化过的,该智能指针应当是由 std::move 传递给 task_handle 的,在 task_handle 后指针生命周期结束,应当被析构。这里优化过后就变成了 task_handle 获取资源后就自动销毁该指针了。

当 add content 的时候,申请了超过 9 的 data 个数,就会直接结束

  else                                          // request more than 10, fuck
  {
LABEL_34:
    std::__ostream_insert<char,std::char_traits<char>>(std::cout, v6);// v6 = "too many!"
    v30 = *(_QWORD *)(std::cout[0] - 24LL);
    v31 = *(_BYTE **)((char *)&std::cout[30] + v30);
    if ( !v31 )
      goto throw_bad_cast_lable;
    if ( v31[56] )
    {
      v32 = v31[67];
    }
    else
    {
      std::ctype<char>::_M_widen_init(*(_QWORD *)((char *)&std::cout[30] + v30));
      v32 = '\n';
      v35 = *(__int64 (__fastcall **)(__int64, unsigned int))(*(_QWORD *)v31 + 48LL);
      if ( v35 != std::ctype<char>::do_widen )
        v32 = v35((__int64)v31, '\n');
    }
    v33 = (std::ostream *)std::ostream::put((std::ostream *)std::cout, v32);
    std::ostream::flush(v33);                   // <=> puts("too many!\n")
    result = 0LL;
  }
  return result;
}

注意这里没有获得指针的所有权,也就是没有做正常流程下的 deque 的 emplace_back 操作。task_handle 返回后,原指针被析构,存储的 old_content 也被析构。但是 old_content 实际上不应该被析构因为下一个 add 的 content 也会把这个 old_content 当作它的 old_content,此时就可以做到 UAF,修改函数指针 xchg 栈迁移后 rop 即可 getshell。

这道题套了一大堆壳,实际上漏洞就是一个单纯的 UAF,伪代码应类似于

void foo(unique_ptr<task>)
{
	if (fail)
    {
        return;
    }
    else
    {
        deque<task>.emplace_back(std::move(unique_ptr<task>));
    }
}

class task
{
    ...
    content* old_content;
    ~task()
    {
        ...
        delete *old_content;
    }
}

int main()
{
    while(1)
    {
            unique_ptr<task> a;
        	if (old_content)
            {
            	a->old_content = old_content;
            }
    		foo(std::move(a));
        	// a 生命周期结束,析构
    }

}

官方 WP 是这样说的

  • 在task_handle中涉及到了page_handle的所有权传递,首先page_handle通过std::move的形式传递到了task_handle中,当page已经拥有一个content时,再次创建新的content时,在task_handle中会创建一个新的content,并且开始调用content的add函数来创建堆块,如果在这里执行失败就会直接return,没有完成page_handle的所有权传递,之后page_handle将会随着函数的结束被释放。

看起来挺炫酷,似乎是 std::move 使用不当、没有接受 move 来的资源之类的 C++ 陷阱,其实完全就是他代码的逻辑错误。事实上这里在 foo 里面不处理有什么关系吗?没有任何关系!只要 task 做好了析构,就不会内存泄露。本身 a 是希望被 move 给 deque 的,但是 foo 中由于错误,deque 不接受 a,所以不接受 move 的资源,这有问题吗,没有任何问题,foo 返回后 a 是会被正常析构的,那就没事,不会内存泄露。这里真正的问题,是 task 结构体中存储了 old_content,用来维护上一个 content,这当然也没问题,但是他在最开始的时候就把 old_content 给了 a,丝毫不管新的 content 是否是合法的,这才是问题所在。在发现新的 content 不合法时,他也没有拿走 a 对 old_content 的引用,那 a 被析构后自然就 UAF 了。正确的做法应该是 add 成功后,再把 old_content 给 a 进行引用,不过这里 old content 存在外层,所以只能提前给,那么 add 失败的时候自然要把这个引用置 0,这才是真正引发漏洞的地方。

修改一下官方 WP 可能会更明确

当page已经拥有一个content时,再次创建新的content时,会在 page_handle 里面保留对 old_content 的引用(has-a 的关系)在task_handle中会创建一个新的content,并且开始调用content的add函数来创建堆块,如果在这里执行失败就会直接return,没有将 old_content 引用置 0,之后page_handle将会随着函数的结束被释放,然后把这个 old_content free 掉。

和那些乱七八糟的 stl 其实没有任何关系。

不过对于这样的代码,直接分析可能难以有成效,主要是因为各个类各个字段的含义难以分析,也许可以考虑进行一下 fuzz。

结构化 fuzz

最开始的时候,我使用的是 libprotobuf-mutator 和 AFLplusplus 结合的方式,不过最后我发现根本没有必要,这里我还是都总结一下。

首先,可以用 protobuf 来描述我们的输入,为了描述堆菜单题的输入,不需要特别复杂的东西,参考一下网络上的教程就可以写了

// out.proto
syntax = "proto2";

package menuctf;

message New_content
{
    required int32 choice_id = 1 [default = 1];
    required int32 sum_of_datas = 2;
    message data
    {
        required int32 key = 1;
        required int32 size = 2;
    }
    repeated data datas = 3;
}

message Edit_message
{
    required int32 choice_id = 1 [default = 2];
    required int32 key = 2;
    required string content = 3;
}

message Show_message
{
    required int32 choice_id = 1 [default = 3];
    required int32 key = 2;
}

message Old_content
{
    required int32 choice_id = 1 [default = 4];
}

message Content_info
{
    required int32 choice_id = 1 [default = 5];
}

message Exit
{
    required int32 choice_id = 1 [default = 6];
}

message Choices
{
    message Choice
    {
        oneof all_choices
        {
            New_content new_content = 1;
            Edit_message edit_message = 2;
            Show_message show_message = 3;
            Old_content old_content = 4;
            Content_info content_info = 5; 
            Exit exit = 6;
        }
    }

    repeated Choice choice = 1;
}

为了和 AFL 整合,可以在 afl-libprotobuf-mutator 的框架上进行修改,需要替换 gen 下的 out.proto 为我们为目标编写的 protobuf,mutator.cc 中需要实现 AFLplusplus 需要的 API,由于 AFLplusplus 的 API 有变更,所以需要重新实现,这里参考了这篇文章

按照 AFLplusplus 文档所要求的,我们需要一个实现了这些 API 的 *.so 文件

void *afl_custom_init(afl_state_t *afl, unsigned int seed);
unsigned int afl_custom_fuzz_count(void *data, const unsigned char *buf, size_t buf_size);
size_t afl_custom_fuzz(void *data, unsigned char *buf, size_t buf_size, unsigned char **out_buf, unsigned char *add_buf, size_t add_buf_size, size_t max_size);
const char *afl_custom_describe(void *data, size_t max_description_len);
size_t afl_custom_post_process(void *data, unsigned char *buf, size_t buf_size, unsigned char **out_buf);
int afl_custom_init_trim(void *data, unsigned char *buf, size_t buf_size);
size_t afl_custom_trim(void *data, unsigned char **out_buf);
int afl_custom_post_trim(void *data, unsigned char success);
size_t afl_custom_havoc_mutation(void *data, unsigned char *buf, size_t buf_size, unsigned char **out_buf, size_t max_size);
unsigned char afl_custom_havoc_mutation_probability(void *data);
unsigned char afl_custom_queue_get(void *data, const unsigned char *filename);
u8 afl_custom_queue_new_entry(void *data, const unsigned char *filename_new_queue, const unsigned int *filename_orig_queue);
const char* afl_custom_introspection(my_mutator_t *data);
void afl_custom_deinit(void *data);

这里只有 afl_custom_fuzz afl_custom_post_process 两个 API 需要进行特殊的实现,这里我是这样实现的

// mutator.cc
...
static int sum_of_datas = 0;
static int size_of_data[11];
static char content_buf[0xE0];

void new_content(menuctf::Choices& msg)
{
	std::random_device rd;
	auto choice = new menuctf::New_content();
	int sum = rd() % 11;
	choice->set_sum_of_datas(sum);

	if (sum_of_datas < 10)
	{
		sum_of_datas = sum;
		for (int i = 0; i < sum_of_datas; i++)
		{
			auto data = choice->add_datas();
			data->set_key(i);
			int size = rd() % 0xCF + 1;
			size_of_data[i] = size;
			data->set_size(size);
		}
	}

	msg.add_choice()->set_allocated_new_content(choice);
}

void edit_message(menuctf::Choices& msg)
{
	if (!sum_of_datas) 
	{
		return;
	}
	auto choice = new menuctf::Edit_message();
	std::random_device rd;
	int idx = rd() % sum_of_datas;
	choice->set_key(idx);
	for (int i = 0; i < size_of_data[idx]; i++)
	{
		content_buf[i] = rd() % 127 + 1;
        while (content_buf[i] == '\n')
		{
			content_buf[i] = rd() % 127 + 1; // 换行可能会造成读取出错
		}
	}
	content_buf[size_of_data[idx]] = 0;
	choice->set_content(content_buf);

	msg.add_choice()->set_allocated_edit_message(choice);
}

void show_message(menuctf::Choices& msg)
{
	if (!sum_of_datas)
	{
		return;
	}
	auto choice = new menuctf::Show_message();
	std::random_device rd;
	int idx = rd() % sum_of_datas;
	choice->set_key(idx);

	msg.add_choice()->set_allocated_show_message(choice);
}

void old_content(menuctf::Choices& msg)
{
	auto choice = new menuctf::Old_content();

	msg.add_choice()->set_allocated_old_content(choice);
}

void content_info(menuctf::Choices& msg)
{
	auto choice = new menuctf::Content_info();

	msg.add_choice()->set_allocated_content_info(choice);
}

void exit_choice(menuctf::Choices& msg)
{
	auto choice = new menuctf::Exit();

	msg.add_choice()->set_allocated_exit(choice);
}

// AFLPlusPlus interface
extern "C"
{
	static std::default_random_engine engine_pro;
	static std::uniform_int_distribution<unsigned int> dis(0, UINT32_MAX);

	void *afl_custom_init(void *afl, unsigned int seed)
	{
#pragma unused(afl)
		engine_pro.seed(seed);
		return nullptr;
	}

	void afl_custom_deinit(void *data)
	{
		assert(!data);
	}

	/**
	 * Perform custom mutations on a given input
	 *
	 * (Optional for now. Required in the future)
	 *
	 * @param[in] data pointer returned in afl_custom_init for this fuzz case
	 * @param[in] buf Pointer to input data to be mutated
	 * @param[in] buf_size Size of input data
	 * @param[out] out_buf the buffer we will work on. we can reuse *buf. NULL on
	 * error.
	 * @param[in] add_buf Buffer containing the additional test case
	 * @param[in] add_buf_size Size of the additional test case
	 * @param[in] max_size Maximum size of the mutated output. The mutation must not
	 *     produce data larger than max_size.
	 * @return Size of the mutated output.
	 */
	// afl_custom_fuzz
	size_t afl_custom_fuzz(void *data, unsigned char *buf, size_t buf_size, unsigned char **out_buf,
						   unsigned char *add_buf, size_t add_buf_size, size_t max_size)
	{
#pragma unused(data)
#pragma unused(buf)
#pragma unused(buf_size)
#pragma unused(add_buf)
#pragma unused(add_buf_size)

		menuctf::Choices msg;
		std::random_device rd;

		int i = 0;
		while (i++ < 100)
		{
			if (rd() % 100 > 80)
			{
				exit_choice(msg);
				break;
			}
			switch (rd() % 5)
			{
			case 0:
				new_content(msg);
				break;
			case 1:
				edit_message(msg);
				break;
			case 2:
				show_message(msg);
				break;
			case 3:
				old_content(msg);
				break;
			case 4:
				content_info(msg);
				break;
			default:
				break;
			}
		}

		static uint8_t *saved_buf = nullptr;

		assert(buf_size <= max_size);

		uint8_t *new_buf = (uint8_t *)realloc((void *)saved_buf, max_size);
		saved_buf = new_buf;

		std::string serialize_data;
		msg.SerializePartialToString(&serialize_data);
		memcpy(new_buf, serialize_data.c_str(), msg.ByteSizeLong());
		*out_buf = new_buf;

		return msg.ByteSizeLong();
	}

	size_t afl_custom_post_process(void *data, uint8_t *buf, size_t buf_size, uint8_t **out_buf)
	{
#pragma unused(data)
		// new_data is never free'd by pre_save_handler
		// I prefer a slow but clearer implementation for now

		static uint8_t *saved_buf = NULL;

		menuctf::Choices msg;
		std::stringstream stream;
		// 如果加载成功
		if (protobuf_mutator::libfuzzer::LoadProtoInput(true, buf, buf_size, &msg))
		{
			ProtoToDataHelper(stream, msg);
		}
		else
		{
			// printf("[afl_custom_post_process] LoadProtoInput Error\n");
			// std::ofstream err_bin("err.bin");
			// err_bin.write((char*)buf, buf_size);

			// abort();

			// 如果加载失败,则返回 Exit Choice
			/// NOTE: 错误的变异 + 错误的 trim 将会导致 post process 加载失败,尤其是 trim 逻辑。
			/// TODO: 由于默认的 trim 会破坏样例,因此需要手动实现一个 trim,这里实现了一个空 trim,不进行任何操作
			ProtoToDataHelper(stream, menuctf::Exit());
		}
		const std::string str = stream.str();

		uint8_t *new_buf = (uint8_t *)realloc((void *)saved_buf, str.size());
		if (!new_buf)
		{
			*out_buf = buf;
			return buf_size;
		}
		*out_buf = saved_buf = new_buf;

		memcpy((void *)new_buf, str.c_str(), str.size());

		return str.size();
	}

	int32_t afl_custom_init_trim(void *data, uint8_t *buf, size_t buf_size)
	{
		/// NOTE: disable trim
		return 0;
	}

	size_t afl_custom_trim(void *data, uint8_t **out_buf)
	{
		/// NOTE: unreachable
		return 0;
	}
}

这里的 afl_custom_fuzz 就是实现自己的变异逻辑了,参考的文章中使用 libprotobuf-mutator 来进行变异,这个的变异原理似乎是对序列化的 protobuf 直接进行变异,我认为这样变异的效率不甚理想,所以干脆直接完全随机了,有点类似于远古时期的 fuzzing。这样做的理由是,对于 ctf 中大多数的无聊堆题,输入的字符串一般不会影响到结果,最容易出现漏洞的还是堆块的申请和释放,以及堆块的大小,所以这里直接完全随机反而可以更高效率的找到洞。

然后实现一个把 protobuf 转为格式化的输出的函数

// mutator.cc
...
void ProtoToDataHelper(std::stringstream &out, const google::protobuf::Message &msg)
{
	const google::protobuf::Descriptor *desc = msg.GetDescriptor();
	const google::protobuf::Reflection *refl = msg.GetReflection();

	const unsigned fields = desc->field_count();
	for (unsigned i = 0; i < fields; ++i)
	{
		const google::protobuf::FieldDescriptor *field = desc->field(i);

		if (field->cpp_type() == google::protobuf::FieldDescriptor::CPPTYPE_MESSAGE)
		{
			if (field->is_repeated())
			{
				// maybe choice list or data list
				const google::protobuf::RepeatedFieldRef<google::protobuf::Message> &ptr = refl->GetRepeatedFieldRef<google::protobuf::Message>(msg, field);
				for (const auto &child : ptr)
				{
					ProtoToDataHelper(out, child);
				}
			}
			else if (refl->HasField(msg, field))
			{
				// maybe one choice or one data
				const google::protobuf::Message &child = refl->GetMessage(msg, field);
				ProtoToDataHelper(out, child);
			}
		}
		else if (field->cpp_type() ==
				 google::protobuf::FieldDescriptor::CPPTYPE_INT32)
		{
			out.width(8);
			out << refl->GetInt32(msg, field);
		}
		else if (field->cpp_type() ==
				 google::protobuf::FieldDescriptor::CPPTYPE_STRING)
		{
			out << refl->GetString(msg, field) << "\n";
		}
		else
		{
			abort();
		}
	}
}
...

由于目标程序使用 read 读入,所以最好填充满 read,所以这里在对 int32 进行输出的时候,设置了 8 的场宽,填充空格即可,因为程序使用 strtol 这类的函数,所以仍然可以正常输入。

为了方便调试,还可以实现一个 dumper.cc,参照 protobuf_ctf_fuzz,针对我的 afl_custom_fuzz 我稍微修改了一下

int main(int argc, char *argv[])
{
	menuctf::Choices msg;

	// 测试变异逻辑
	int rand_fd;
	if ((rand_fd = open("/dev/random", O_RDONLY)) < 0)
		abort();
	unsigned int seed;
	read(rand_fd, &seed, sizeof(seed));
	close(rand_fd);

	void *init_data = afl_custom_init(nullptr, seed);
	if (argc > 1)
	{
		if (!strcmp(argv[1], "gen"))
		{
			uint8_t *out_buf = nullptr;
			size_t new_size = afl_custom_fuzz(nullptr, nullptr, 0,
												&out_buf, nullptr, 0, 0x2000);
			uint8_t *new_str = nullptr;
			size_t new_str_size = afl_custom_post_process(init_data, out_buf, new_size, &new_str);
			std::string new_str_str((char *)new_str, new_str_size);
			std::cout << new_str_str << std::endl
						<< std::endl;
		}
        if (!strcmp(argv[1], "-d") && argc > 2)
		{
			struct stat statbuf;
			stat(argv[2], &statbuf);
			int proto_size = statbuf.st_size;

			char* proto_buf = new char [proto_size + 1];

			FILE* proto_fp = fopen(argv[2], "r");
			if (!proto_fp) { return -1; }
			fread(proto_buf, proto_size, 1, proto_fp);

			menuctf::Choices msg;
			std::stringstream stream;
			// 如果加载成功
			if (protobuf_mutator::libfuzzer::LoadProtoInput(true, (const unsigned char *) proto_buf, proto_size, &msg))
			{
				ProtoToDataHelper(stream, msg);
			}
			std::cout << stream.str();
		}
	}
	else
	{
		for (int i = 0; i < 5; i++)
		{
			uint8_t *out_buf = nullptr;
			size_t new_size = afl_custom_fuzz(nullptr, nullptr, 0,
												&out_buf, nullptr, 0, 0x2000);
			uint8_t *new_str = nullptr;
			size_t new_str_size = afl_custom_post_process(init_data, out_buf, new_size, &new_str);
			std::string new_str_str((char *)new_str, new_str_size);
			std::cout << i << " =============== " << std::endl;
			std::cout << new_str_str << std::endl
						<< std::endl;
		}
	}
	afl_custom_deinit(init_data);

	// std::cout << "msg DebugString: " << msg.DebugString() << std::endl;
//	std::stringstream stream;
//	ProtoToDataHelper(stream, msg);
//	std::cout << stream.str() << std::endl;

	return 0;
}

执行 make 后会编译出来 dumpermutatorlibmutator.so,直接执行 dumper 可以看到变异情况,执行 ./dumper gen > rand 可以获得一个语料。然后就可以开始 fuzz 了,首先设置环境变量

# setup_env.sh
#!/bin/bash
export AFL_CUSTOM_MUTATOR_ONLY=1
export AFL_CUSTOM_MUTATOR_LIBRARY=`pwd`/libmutator.so
export AFL_USE_QASAN=1

如果是写成脚本设置,注意要用 source ./setup_env.sh 来设置。

然后执行

AFLplusplus/afl-fuzz -i input -o output -Q -- ./byteview

input 文件夹需要自己新建,input 里面用 dumper 随便生成一个语料即可,只是为了通过 AFL 的 check。

QQ截图20211219232018.png

运行一下,瞬间就可以出 crash。crash 文件中是序列化后的数据,为了获得 poc,需要反序列化,我在 dumper 中实现了这个功能,来看一看

$ ./dumper -d ./output/default/crashes/id:000001,sig:06,src:000000+000012,time:366,execs:153,op:libmutator.so,pos:0 > poc

$ xxd poc
00000000: 2020 2020 2020 2031 2020 2020 2020 2039         1       9
00000010: 2020 2020 2020 2030 2020 2020 2020 3732         0      72
00000020: 2020 2020 2020 2031 2020 2020 2031 3732         1     172
00000030: 2020 2020 2020 2032 2020 2020 2031 3037         2     107
00000040: 2020 2020 2020 2033 2020 2020 2020 3735         3      75
00000050: 2020 2020 2020 2034 2020 2020 2031 3336         4     136
00000060: 2020 2020 2020 2035 2020 2020 2020 3434         5      44
00000070: 2020 2020 2020 2036 2020 2020 2031 3930         6     190
00000080: 2020 2020 2020 2037 2020 2020 2031 3233         7     123
00000090: 2020 2020 2020 2038 2020 2020 2020 3531         8      51
000000a0: 2020 2020 2020 2034 2020 2020 2020 2031         4       1
000000b0: 2020 2020 2020 3130 2020 2020 2020 2033        10       3
000000c0: 2020 2020 2020 2037 2020 2020 2020 2031         7       1
000000d0: 2020 2020 2020 2033 2020 2020 2020 2030         3       0
000000e0: 2020 2020 2020 3936 2020 2020 2020 2031        96       1
000000f0: 2020 2020 2031 3138 2020 2020 2020 2032       118       2
00000100: 2020 2020 2031 3139 2020 2020 2020 2033       119       3
00000110: 2020 2020 2020 2031 2020 2020 2020 2035         1       5
00000120: 2020 2020 2020 2034 2020 2020 2020 2035         4       5
00000130: 2020 2020 2020 2033 2020 2020 2020 2032         3       2
00000140: 2020 2020 2020 2032 2020 2020 2020 2030         2       0
00000150: 7442 0304 2114 2658 3a46 227d 466d 484b  tB..!.&X:F"}FmHK
00000160: 7903 3530 5061 2601 176a 6c35 793d 4c12  y.50Pa&..jl5y=L.
00000170: 390f 0c4e 5601 0e19 6247 416e 3433 7a07  9..NV...bGAn43z.
00000180: 420f 431c 2f5d 682c 5b55 5a55 2401 1d44  B.C./]h,[UZU$..D
00000190: 1902 2a70 0741 342d 6418 0329 2b04 7435  ..*p.A4-d..)+.t5
000001a0: 0e64 5c3a 6e7e 6525 1564 4e2f 203f 2d07  .d\:n~e%.dN/ ?-.
000001b0: 0a20 2020 2020 2020 3220 2020 2020 2020  .       2   
000001c0: 322d 6f15 582e 3726 3c10 6111 4c4a 0848  2-o.X.7&<.a.LJ.H
000001d0: 3965 1162 7530 2361 0834 3026 2618 121e  9e.bu0#a.40&&...
000001e0: 7f2d 3b74 4453 4343 1747 7275 011f 7c60  .-;tDSCC.Gru..|`
000001f0: 0831 0742 321e 6f46 7611 6f70 3174 1037  .1.B2.oFv.op1t.7
00000200: 7f47 070b 1924 0f4e 1d7b 662f 2151 3a6f  .G...$.N.{f/!Q:o
00000210: 662f 1a3d 3d48 6357 2d48 441f 025e 4943  f/.==HcW-HD..^IC
00000220: 2923 1472 5b17 1706 7032 423c 6102 3948  )#.r[...p2B<a.9H
00000230: 0111 237b 3365 6e6b 0a20 2020 2020 2020  ..#{3enk.   
00000240: 3520 2020 2020 2020 3320 2020 2020 2020  5       3   
00000250: 3220 2020 2020 2020 3120 2020 2020 2020  2       1   
00000260: 3420 2020 2020 2020 3020 2020 2020 3134  4       0     14
00000270: 3420 2020 2020 2020 3120 2020 2020 3230  4       1     20
00000280: 3020 2020 2020 2020 3220 2020 2020 3132  0       2     12
00000290: 3120 2020 2020 2020 3320 2020 2020 3230  1       3     20
000002a0: 3220 2020 2020 2020 3420 2020 2020 2020  2       4   
000002b0: 3120 2020 2020 2020 3920 2020 2020 2020  1       9   
000002c0: 3020 2020 2020 2039 3620 2020 2020 2020  0      96   
000002d0: 3120 2020 2020 2036 3420 2020 2020 2020  1      64   
000002e0: 3220 2020 2020 3131 3020 2020 2020 2020  2     110   
000002f0: 3320 2020 2020 2037 3920 2020 2020 2020  3      79   
00000300: 3420 2020 2020 3133 3920 2020 2020 2020  4     139   
00000310: 3520 2020 2020 3133 3820 2020 2020 2020  5     138   
00000320: 3620 2020 2020 2032 3220 2020 2020 2020  6      22   
00000330: 3720 2020 2020 3230 3620 2020 2020 2020  7     206   
00000340: 3820 2020 2020 2020 3220 2020 2020 2020  8       2   
00000350: 3320 2020 2020 2020 3720 2020 2020 2020  3       7   
00000360: 3120 2020 2020 2020 3320 2020 2020 2020  1       3   
00000370: 3020 2020 2020 2036 3420 2020 2020 2020  0      64   
00000380: 3120 2020 2020 2034 3420 2020 2020 2020  1      44   
00000390: 3220 2020 2020 2020 3620 2020 2020 2020  2       6   
000003a0: 3220 2020 2020 2020 3138 1d6b 3a43 695b  2       18.k:Ci[
000003b0: 0538 2211 5232 3e26 4051 0860 3625 0b01  .8".R2>&@Q.`6%..
000003c0: 4129 0b12 0352 616d 283d 3227 7e29 2302  A)...Ram(=2'~)#.
000003d0: 546d 7b67 6c0a 2020 2020 2020 2031 2020  Tm{gl.       1  
000003e0: 2020 2020 2030 2020 2020 2020 2034 2020       0       4  
000003f0: 2020 2020 2034 2020 2020 2020 2034 2020       4       4  
00000400: 2020 2020 2031 2020 2020 2020 2035 2020       1       5  
00000410: 2020 2020 2030 2020 2020 2020 3138 2020       0      18  
00000420: 2020 2020 2031 2020 2020 2031 3034 2020       1     104  
00000430: 2020 2020 2032 2020 2020 2032 3037 2020       2     207  
00000440: 2020 2020 2033 2020 2020 2020 3536 2020       3      56  
00000450: 2020 2020 2034 2020 2020 2031 3731 2020       4     171  
00000460: 2020 2020 2031 2020 2020 2020 3130 2020       1      10  
00000470: 2020 2020 2036                                6

喂给目标程序跑一下,能看到在 too many 后面 crash 了,每个 crash 都试试,应该是能分析出来是什么原因的。

真的有必要这么做吗

跑完 AFL 之后,我才想到,我既没有用 libprotobuf-mutator 来帮我变异,又没有用到 AFLplusplus 的任何变异策略,那么其实这两个都没必要用,只需要一个能随机生成语料的引擎,然后一直把语料喂给目标就可以了,所以只要这个简单的 shell 脚本就可以了

#!/bin/bash
while ((1))
do 
    ./dumper gen > poc
    cat poc | ./byteview
    if [ $? -ne 0 ]; then
        break
    fi
done

语料生成其实随便写个脚本就可以了,不需要用什么 protobuf。

不过这个没有用 qasan,找不出 UAF 这样可能不会 crash 的洞,可以使用 qasan 可能会更快的发现。不过对于此题,这样已经足够,直接跑就可以出 crash 了。分析一下 poc

$ xxd poc 
00000000: 2020 2020 2020 2031 2020 2020 2020 2034         1       4
00000010: 2020 2020 2020 2030 2020 2020 2020 2031         0       1
00000020: 2020 2020 2020 2031 2020 2020 2031 3832         1     182
00000030: 2020 2020 2020 2032 2020 2020 2031 3330         2     130
00000040: 2020 2020 2020 2033 2020 2020 2020 3134         3      14
00000050: 2020 2020 2020 2032 2020 2020 2020 2033         2       3
00000060: 4f48 0724 2536 6528 2119 062c 6901 0a20  OH.$%6e(!..,i.. 
00000070: 2020 2020 2020 3120 2020 2020 2020 3420        1       4 
00000080: 2020 2020 2020 3020 2020 2020 3134 3120        0     141 
00000090: 2020 2020 2020 3120 2020 2020 3132 3820        1     128 
000000a0: 2020 2020 2020 3220 2020 2020 3230 3520        2     205 
000000b0: 2020 2020 2020 3320 2020 2020 2033 3220        3      32 
000000c0: 2020 2020 2020 3120 2020 2020 2020 3020        1       0 
000000d0: 2020 2020 2020 3120 2020 2020 2020 3920        1       9 
000000e0: 2020 2020 2020 3020 2020 2020 2037 3720        0      77 
000000f0: 2020 2020 2020 3120 2020 2020 2035 3020        1      50 
00000100: 2020 2020 2020 3220 2020 2020 3137 3320        2     173 
00000110: 2020 2020 2020 3320 2020 2020 3136 3420        3     164 
00000120: 2020 2020 2020 3420 2020 2020 3135 3020        4     150 
00000130: 2020 2020 2020 3520 2020 2020 2036 3920        5      69 
00000140: 2020 2020 2020 3620 2020 2020 2031 3820        6      18 
00000150: 2020 2020 2020 3720 2020 2020 2033 3020        7      30 
00000160: 2020 2020 2020 3820 2020 2020 2032 3720        8      27 
00000170: 2020 2020 2020 3320 2020 2020 2020 3420        3       4 
00000180: 2020 2020 2020 3320 2020 2020 2020 3720        3       7 
00000190: 2020 2020 2020 3520 2020 2020 2020 3120        5       1 
000001a0: 2020 2020 2020 3520 2020 2020 2020 3020        5       0 
000001b0: 2020 2020 3139 3220 2020 2020 2020 3120      192       1 
000001c0: 2020 2020 3136 3620 2020 2020 2020 3220      166       2 
000001d0: 2020 2020 2031 3820 2020 2020 2020 3320       18       3 
000001e0: 2020 2020 3137 3120 2020 2020 2020 3420      171       4 
000001f0: 2020 2020 3131 3420 2020 2020 2020 360a      114       6.

可以看到是由先成功申请了 content 再失败申请 content 造成的 crash。

总结

  • 如果要用 qasan,直接用 AFLplusplus 的 qemu-mode 起可能会更方便
  • 如果要使用真正的 AFLplusplus + protobuf 实现基于覆盖率的结构化感知 fuzz,那么在实现 afl_custom_fuzz 时可以用 libprotobuf-mutator 提供的变异器对输入的数据(序列化的 protobuf)进行变异,然后通过 afl_custom_post_process 筛选变异的数据,对于可以反序列化的数据反序列化并格式化为目标程序可以接受的输入。对于 byteview 这题,由于 new_content 中的 datas 的个数与 sum_of_datas 有关,其实也可以在这里筛选正确的变异结果。注意这种情况下需要提供正确的语料,可以用 dumper 输出序列化的 protobuf 来构造语料。
    • 对于这种方法,我个人认为并不高效,我个人不太信任 libprotobuf-mutator 的变异效果。不过如果尝试对 byteview 这题进行这样的测试,有可能会发现效率非常高,也可能会发现效率非常低。为什么会出现这种结果?因为这种情况下是基于语料库进行变异的,语料库的质量决定了 fuzz 的速度,而实际上随机生成一个语料,还是有比较高的概率产生可以让目标程序直接 crash,或者稍微变异一下就 crash 的语料的(也就是很容易就产出超高质量的语料)。
  • 关于 AFL_CUSTOM_MUTATOR_ONLY 的设置与否,如果不设置该变量,用户提供的变异器只会在 havoc 阶段(undetermined mutate 的第一个阶段)前进行一次变异,其余情况会由 AFL 进行变异,我觉得由 AFL 直接对序列化的数据进行变异,可能会变异出无意义的数据,应当全权交给 libprotobuf-mutator 来变异,所以感觉还是需要设置的。

不足与改进

如前文所述,这种 fuzz 方式(也就是用 qasan + 结构化随机输入)非常适合找出 CTF 传统堆题由申请释放触发的 UAF 等漏洞的 poc。不足主要在于

  • 如果目标程序使用了 read 等对输入要求较高的读取函数时(即对非法输入的鲁棒性较差),需要多花点时间对输入进行格式化,避免非法输入导致程序爆炸
  • 对于触发条件比较微妙的漏洞可能会比较难以发现。
  • 适配工作比较多,会浪费很多时间

所以其实,这种 fuzz 方法最适合的就是漏洞本身很简单,但是程序比较难以分析的题目。这种题没做出来会在赛后给选手很大的心理伤害,在赛时无路可走了,也不妨尝试乱 fuzz 一下,也可以少点遗憾。

如果要改进,我个人的想法有如下:

  • 使用 libprotobuf-mutator 进行变异,并且自己重写 mutator 方法,实现对变异更精确的控制
  • 真正与 AFL 整合,也就是提供给 AFL 进行变异的语料应当是反序列化的数据,这样可以利用 AFL 提供的变异算法,可能可以达到更高的覆盖率,也许可以发现一些比较“subtle”的洞。然后我们在 havoc 阶段前进行结构化的变异,指导 AFL 生成高度结构化的输入。不过如果让 AFL 进行变异,可能需要目标程序有较高的鲁棒性(毕竟 ctf 题不是工业程序,乱输一气程序会炸并不影响选手拿不到 flag)。
  • 关于溢出类型的洞,可以对变异器的 size 要进行变异,生成可能会超过 buf 的 string,从修改的角度来看这其实没啥难度,这里没这么做,主要原因是目标程序鲁棒性较差,并且分析的时候也看得出来是不会有溢出的。换句话说,如果有溢出的洞,ctf 中一般是能直接看出来的,那也没必要去搞什么 fuzz,花里胡哨的没必要,有这个适配的时间估计硬看也能看出来了。不过对于 realworld 的东西,感觉不妨一试。

之后有空的话,我会尝试对别的无聊菜单题进行 fuzz,看看有无奇效。

相关的代码在 github

reference

https://github.com/thebabush/afl-libprotobuf-mutator

https://kiprey.github.io/2021/09/protobuf_ctf_fuzz/