【WP】tqlctf2022-Pwn
比赛的时候只做出来一道(😭)
unbelievable_write
控制tcache struct以后改chunk size造large bin,然后做large bin attack改target
exp
1 | from pwn import* |
看到比较简单的非预期解是直接改掉free的got表,确实简单很多
另一种思路
出题人给了另一种思路,利用stdio未初始化,第一次执行puts会申请0x1000的缓冲区输入内容,从而完成对target的覆盖
具体来说是先申请0x1000的chunk,内容全部填上target-8
然后控制tcache结构体,个数位置都写上1,申请地址到mp_上,把mp_.tcache_bins改成一个大数,从而当申请0x1000的chunk时会从tcache上拿。再配合上上面说的在堆上布置一堆target-8,就会从这里拿出chunk了
puts->_IO_file_xsputn->_IO_file_overflow,发现没有缓冲区(f->_IO_write_base == NULL),于是调用_IO_doallocbuf申请(实际上是调用_IO_file_jumps+0x68处的_IO_file_doallocate)_IO_file_doallocate首先调用_IO_file_stat(_IO_file_jumps+0x90),然后malloc(0x1000),最后_IO_setb设置fp->_flags和写入_IO_buf_base和_IO_buf_end,后续会设置好stdout结构体
最后ch==EOF退出
之后还是在_IO_file_xsputn中往下走,进入_IO_default_xsputn,内部是一个循环,会调用_IO_file_overflow实现逐字节向缓冲区输入内容
而后回到puts,调用_IO_file_overflow,有意思的是知道puts输出字符串会自带回车,发现就是在这里,在进入_IO_do_write之前会先向缓冲区补一个回车,_IO_do_write函数会调用函数指针_IO_file_write(_IO_file_jump+0x78),最终将使用write将缓冲区的内容输出到标准输出
_IO_file_overflow源码
1 | int |
ezvm
之前没有接触过unicorn
在james詹爹的陪伴下稍微了解了unicorn相关的知识赛后做出了这题(不过花了挺长时间的=-=)
汇编也写的挺拉的
不过感觉还挺有意思的
题目
题目是用unicorn引擎模拟了x86架构,输入的指令被执行
uc_hook_add注册hook事件回调,当hook事件被触发时进行回调1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17uc_err uc_hook_add(uc_engine *uc, uc_hook *hh, int type, void *callback,
void *user_data, uint64_t begin, uint64_t end, ...);
/*
@uc: uc_open() 返回的句柄
@hh: 注册hook得到的句柄. uc_hook_del() 中使用
@type: hook 类型
@callback: 当指令被命中时要运行的回调
@user_data: 用户自定义数据. 将被传递给回调函数的最后一个参数 @user_data
@begin: 回调生效区域的起始地址(包括)
@end: 回调生效区域的结束地址(包括)
注意 1: 只有回调的地址在[@begin, @end]中才会调用回调
注意 2: 如果 @begin > @end, 每当触发此hook类型时都会调用回调
@...: 变量参数 (取决于 @type)
注意: 如果 @type = UC_HOOK_INSN, 这里是指令ID (如: UC_X86_INS_OUT)
@return 成功则返回UC_ERR_OK , 否则返回 uc_err 枚举的其他错误类型
*/
hook类型参数定义如下1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33typedef enum uc_hook_type {
// Hook 所有中断/syscall 事件
UC_HOOK_INTR = 1 << 0,
// Hook 一条特定的指令 - 只支持非常小的指令子集
UC_HOOK_INSN = 1 << 1,
// Hook 一段代码
UC_HOOK_CODE = 1 << 2,
// Hook 基本块
UC_HOOK_BLOCK = 1 << 3,
// 用于在未映射的内存上读取内存的Hook
UC_HOOK_MEM_READ_UNMAPPED = 1 << 4,
// Hook 无效的内存写事件
UC_HOOK_MEM_WRITE_UNMAPPED = 1 << 5,
// Hook 执行事件的无效内存
UC_HOOK_MEM_FETCH_UNMAPPED = 1 << 6,
// Hook 读保护的内存
UC_HOOK_MEM_READ_PROT = 1 << 7,
// Hook 写保护的内存
UC_HOOK_MEM_WRITE_PROT = 1 << 8,
// Hook 不可执行内存上的内存
UC_HOOK_MEM_FETCH_PROT = 1 << 9,
// Hook 内存读取事件
UC_HOOK_MEM_READ = 1 << 10,
// Hook 内存写入事件
UC_HOOK_MEM_WRITE = 1 << 11,
// Hook 内存获取执行事件
UC_HOOK_MEM_FETCH = 1 << 12,
// Hook 内存读取事件,只允许能成功访问的地址
// 成功读取后将触发回调
UC_HOOK_MEM_READ_AFTER = 1 << 13,
// Hook 无效指令异常
UC_HOOK_INSN_INVALID = 1 << 14,
} uc_hook_type;
题目中注册回调1
uc_hook_add(v8, v9, 2LL, menu, 0LL, 1LL, 0LL, 699LL);
对应UC_HOOK_INSN,从函数定义可以看到699即对应指令ID(这里想了半天是啥。。因为一开始用的是IDA7.5没识别到这个参数还懵逼了好久……)
查看[定义][https://github.com/unicorn-engine/unicorn/blob/master/include/unicorn/x86.h] 发现对应指令syscall
进入回调函数uc_reg_read是从模拟的寄存器取值uc_reg_write是向模拟的寄存器写值,本程序内用来保存返回值uc_mem_read是从模拟空间读取数据到内部uc_mem_write是从内部往模拟空间写数据uc_mem_read和uc_mem_write进行读写操作时都会使用calloc申请chunk作为缓冲区
内部有点像文件系统,开头有三个文件stdin,stdout,stderr
类似菜单题实现了四个功能
- 0对应read,将内部虚拟文件指定字节数的内容读到模拟空间,如果选0,1,2会执行
read - 1对应write,将模拟空间内的数据读到内部虚拟的文件中(其实就是个chunk),选前三个执行
write - 2对应open,新建一个虚拟的文件,限制最多15个(包括012),使用
malloc申请一个chunk,将name文件名写上,布置上三个函数指针,写上size。size不超过0x400,如果满了或者虚拟文件名重复了则直接返回。 - 3对应close,释放新建虚拟文件时申请的chunk,选前三个会执行close()
一个虚拟的文件对应的结构体大致如下1
2
3
4
5
6
7
8
9struct VFile{
__int64 fd;
char name[0x18];
char *file_space;
__int64 size;
void *read;
void *write;
void *close;
}
思路
观察open操作中1
2
3
4
5
6
7
8
9v6 = &stru_5020 + j;
v6->file_space = malloc(size);
strcpy(v6->name, a1);
v6->read = choice_1;
v6->write = choice_0;
v6->close = choice_3;
v6->id = j;
++time_limit;
v6->size = size;
在写入虚拟文件名时使用的是strcpy函数,会发生null byte的溢出,而后面正好是chunk指针
所以可以利用这个漏洞修改tcache的fd
我的思路大概是
- 改掉tcache的key,造出tcache double free,然后修改指针指到unosrted bin的fd
- 申请一个较大的chunk(从unsorted bin切割),拿到libc地址,然后改fd指向
__free_hook - 再进行
open即可拿到__free_hook指针 - 接着向
__free_hook写入gadget栈迁移到堆上,因为每次写入都会先calloc然后出来再free掉缓冲区,所以rop链会在作为缓冲区的chunk上执行 - rop链布置在虚拟空间中,通过
mov指令加加减减布置好的 - 本题开了沙箱禁用
execve,所以最后在堆上先mprotect改为rwx,然后调用read读入orw的shellcode,执行完read时rsi指向shellcode,所以再call rsi即可完成orw读出flag(也可以写入的时候直接就把shellcode写进去然后ret就行了,但这里因为之前堆风水没做好chunk大小不太够所以就用read读了)
exp
(确实丑陋…)
1 | from pwn import* |
后记
- 观摩了大佬的wp,自己确实写的复杂了点,过程上很多可以简化有些步骤是不必要的(但也懒得改了),比如tcache直接改fd就彳亍了,也不知道为什么当时写的时候还绕了路,南辕北辙的意思了
- 另外可以利用标准输出和输入计算和布置gadget,把信息读到指定地址,用标准输出输出指定地址的信息,布置gadget的时候可以使用标准输入读到指定地址然后再用程序的write功能写到chunk内,而自己是在汇编里嗯算的就不是那么快速属于…
- 再后来稍稍优化了一下exp,发现ubuntu20的libc版本从9.2变成9.7了,所以exp中偏移对应的是9.7小版本的libc
nemu
题目
1 | [*] '/home/ayoung/pwn/tqlctf/nemu' |
题目给了源码
可以看到set功能能够在pmem后任意写1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43static int cmd_set(char *args){
paddr_t dest_addr;
uint32_t data;
bool success = false;
if(args == NULL) {
printf("Please input argument\n");
return 0;
}
else{
//split string
char *dest_addr_str = strtok(args, " ");
char *data_str = strtok(NULL, " ");
if( (dest_addr_str==NULL) || (data_str == NULL)){
printf("wrong argument\n");
return 0;
}
dest_addr = expr(dest_addr_str, &success);
if(!success) {
printf("Wrong express!\n");
return 0;
}
data = expr(data_str, &success);
if(!success) {
printf("Wrong express!\n");
return 0;
}
vaddr_write(dest_addr, 4, data);
return 0;
}
}
void vaddr_write(vaddr_t addr, int len, uint32_t data) {
paddr_write(addr, len, data);
}
void paddr_write(paddr_t addr, int len, uint32_t data) {
memcpy(guest_to_host (addr), &data, len);
}
/* convert the guest physical address in the guest program to host virtual address in NEMU */
x能够在peme后任意地址读1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41static int cmd_x(char *args){
if(args == NULL){printf("Please input argument\n"); return 0;}
else{
printf("%-10s\t%-10s\t%-10s\n","Address","DwordBlock","DwordBlock");
char *n_str = strtok(args, " ");
if(!memcmp(n_str,"0x",2)){
long addr = strtol(n_str,NULL,16);
printf("%#010x\t",(uint32_t)addr);
printf("%#010x\n",vaddr_read(addr,4));
}
else{
int n = atoi(n_str);
n_str = strtok(NULL, " ");
long addr = strtol(n_str,NULL, 16);
while(n){
printf("%#010x\t",(uint32_t)addr);
for(int i=1; i<=2; i++){
printf("%#010x\t",vaddr_read(addr,4));
addr += 4;
n--;
if(n == 0) break;
}
printf("\n");
}
}
}
return 0;
}
uint32_t vaddr_read(vaddr_t addr, int len) {
return paddr_read(addr, len);
}
uint32_t paddr_read(paddr_t addr, int len) {
return pmem_rw(addr, uint32_t) & (~0u >> ((4 - len) << 3));
}
命令的读入使用的是readline1
line_read = readline("(nemu) ");
该库函数会动态分配内存存储字符串
WP结构体1
2
3
4
5
6
7
8
9
10typedef struct watchpoint {
int NO;
struct watchpoint *next;
/* TODO: Add more members if necessary */
char exp[30];
uint32_t old_val;
uint32_t new_val;
} WP;
w设置watchpoint时1
2
3
4
5...
WP *wp = new_wp();
wp->old_val = val;
memcpy(wp->exp, args, 30);
...
其中args对应刚才readline读入的字符串
可以看到这里memcpy了30字节的内容到exp,而上面提到了readline是动态分配的内存,实际上第一次分配时会从unsorted bin中切割,于是bk指针会被复制到wp->exp中
通过任意地址读即可泄露基址
然后考虑如何控制执行流
注意到删除watchpoint时1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22bool delete_watchpoint(int NO){
if (head == NULL) {
printf("There is no watchpoint to delete!");
return false;
}
WP *wp;
if (head->NO == NO) {
wp = head;
head = head->next;
free_wp(wp);
}
...
...
void free_wp(WP* wp){//addr
wp->exp[0] = '\0';
wp->new_val = -1;
wp->next = free_;
free_ = wp;
return;
}
检测到head->NO匹配时,进入free_wp,其中涉及链表操作,将头节点加入空闲链表,执行head->next=free__,所以可以利用这里进行真正意义的任意地址写
可以修改strmcmp的got表为system
再次输入/bin/sh即可getshell
另一种思路
看到nu1l大佬的wp,泄露基址是利用写操作造出一个unsorted bin,然后写入line_read,每次输入命令时会进入rl_gets执行free(line_read)释放之前动态分配的存储命令内容的空间
之后还是利用上面方法将system写入__free_hook,再次输入/bin/sh即可
exp
1 | from pwn import * |




