从负开始学汇编0
第一天
一句一句看一下昨天的pwn1,超简单,几行代码的
main
push ebp
mov ebp, esp
and esp, 0FFFFFFF0h ; 这里是对齐esp到16字节
sub esp, 10h ; 抬高栈顶,相当于分配了16字节空间
mov dword ptr [esp], 1 ; 像栈里推入4字节 0x1,相当于push 1
call _alarm ; 调用函数alarm(1), 1是上一步分的
我拿gdb调试的时候
我们暂停一下,这个时候的栈应该是
到了and这句之前
EBP: 0xffffcbe8
ESP: 0xffffcbe8
and之后
EBP: 0xffffcbe8
ESP: 0xffffcbe0
sub esp, 0xfffffff0
之后
EBP: 0xffffcbe8
ESP: 0xffffcbd0
mov DWORD PTR [esp],0x1
后,esp没变化
相当于是在栈顶esp=0xffffcbd4
的时候执行了push 0x1
,使得esp = esp - 4
push xxx
和mov DWORD PTR [esp], xxx
作用和区别:
一个会使esp变化一个不会
并且此时x/x $esp
得到栈顶上的值是0x01
好了,啰嗦了好多,像小学生一样
call 0x8048400
(IDA里看到的call _alarm
)的时候,我们跟进看一下
_alarm proc near
jmp ds:off_804A014
_alarm endp
再跟着跳过去,发现这个ds:off_804A014
是属于.got.plt
.got.plt:0804A014 offset_804A014 dd offset alarm
然后我们不跟着他了,我们直接到alarm
跑完到下一句
这个时候我们发现栈里的数据还没清理
我们alarm的形参0x1
还在栈顶丢着
C里函数调用默认是cdel
,从右到左压参数,caller清理现场,可没见到清理现场啊
接着看下面的代码
0x80485a4: call 0x8048400
=> 0x80485a9: mov eax,ds:0x804a044 ;这句的0x804a044在IDa里是STDIN
0x80485ae: mov DWORD PTR [esp+0x4],0xxxxxx0h ;0
0x80485b6: mov DWORD PTR [esp],eax ;STDIN
0x80485b9: call 0x80483e0 ; setbuf(STDIN, 0)
0x80485be: mov eax,ds:0x804a060
我们跑过压参数的这两句,然后stack里是这样子
0000| 0xffffcbd0 --> 0xf7f9a5a0
0004| 0xffffcbd4 --> 0x0
我装的是peda插件,最左边是相对于esp偏移(十进制的),偏移越小肯定是离栈顶越近咯
可以看到原来的栈顶0xffffcbd0
这里存的是0x1
来着
诶?看这个样子被调用者自己清理了栈???这个形参算是caller的还是calle的啊
跟一下setbuf
=> 0xf7e4dff0 : sub esp,0x10
0xf7e4dff3 : push 0x2000
0xf7e4dff8 : push DWORD PTR [esp+0x1c]
0xf7e4dffc : push DWORD PTR [esp+0x1c]
0xf7e4e000 : call 0xf7e48210
0xf7e4e005 : add esp,0x1c
0xf7e4e008 : ret
有点迷…..
喝了口水,反应过来是这样子的,前面看到的几个函数都是glibc
里提供的…
这里有一个延迟加载的措施,其实比较复杂,我们进入到的这个setbuf
,只是glibc
里的一个跳台,这个帮真正的setbuf
把参数压进去,然后才调到真正起作用的setbuffer
这里的setbuf
调用setbuffer
的地方倒是像是cdel
先从坐到右参数压了栈
0000| 0xffffcbb0 --> 0xf7f9a5a0 ;STDIN
0004| 0xffffcbb4 --> 0x0 ; 0
0008| 0xffffcbb8 --> 0x2000 ; 不知道是啥
然后调setbuffer
最后调用之后add esp, 0x1c
对应原来的sub esp, 0x10
很明显这多出来的0x0c
是callersetbuf
对堆栈的清理
把给calleesetbuffer
传的三个参数给清理掉
再跟一下stebuffer
,发现和我们想的是一样的了
0000| 0xffffcbac --> 0xf7e4e005 ;ret_addr
0004| 0xffffcbb0 --> 0xf7f9a5a0 ;arg1
0008| 0xffffcbb4 --> 0x0 ;arg2
0012| 0xffffcbb8 --> 0x2000 ('') ;arg3
下面是0xf7e4e005
的具体逻辑就不看了
我们直接跳出setbuffer
, 跳出setbuf
回来main
里
关于
别看到返回地址就打断点,那个返回地址是栈里的,你没法打断
打断点好像只能在.text段
callee清理栈方式
ret n
caller清理栈方式
add esp, 0x0c
我们回到main,下面又是两个一毛一样的stebuf
=> 0x80485be: mov eax,ds:0x804a060
0x80485c3: mov DWORD PTR [esp+0x4],0x0
0x80485cb: mov DWORD PTR [esp],eax
0x80485ce: call 0x80483e0 ; setbuf(sdout, 0)
0x80485d3: mov eax,ds:0x804a040
0x80485d8: mov DWORD PTR [esp+0x4],0x0
0x80485e0: mov DWORD PTR [esp],eax
0x80485e3: call 0x80483e0 ; setbuf(stderror, 0)
0x80485e8: mov DWORD PTR [esp],0x8048690 ; "puts your name:"
0x80485ef: call 0x8048410 ;puts("puts your name")
这里也看清楚了call的是<setbuf@plt>
其实读代码大概是这样子的
诶,我有个疑问,这个代码怎么感觉和我调试的不太一样啊,我看蠢了等下
先看下main这里的代码
gdb-peda$ x/10i 0x8048594
0x8048594: push ebp
0x8048595: mov ebp,esp
0x8048597: and esp,0xfffffff0
0x804859a: sub esp,0x10
0x804859d: mov DWORD PTR [esp],0x1
0x80485a4: call 0x8048400
0x80485a9: mov eax,ds:0x804a044
0x80485ae: mov DWORD PTR [esp+0x4],0x0
0x80485b6: mov DWORD PTR [esp],eax
0x80485b9: call 0x80483e0
然后跟着过去看下call 0x80483e0 <setbuf@plt>
gdb-peda$ x/10i 0x80483e0
0x80483e0 : jmp DWORD PTR ds:0x804a00c
0x80483e6 : push 0x0
0x80483eb : jmp 0x80483d0
大概是这样子的
+----------------+-------------------+-----------
ELF PLT表 GOT表
+----------------+-------------------+----------
call => jmp *0x804a00c =>
这个plt是不允许写的,但这个GOT很明显是可以写的,但如果动态链接的话,在你编译的时候没法确定这个你要调用的库函数的地址,所以很明显这个GOT很明显开始放的不是真正的setbuf的地址
准确的说是,在setbuf第一次调用的时候,setbuf的got表项<setbuf@got.plt>
里存的是<setbuf@plt+6>
的地址,现在看看我们的plt表
,是不是发现有三条
0x80483e0 : jmp DWORD PTR ds:0x804a00c
0x80483e6 : push 0x0
0x80483eb : jmp 0x80483d0
这个我们会从<setbuf@got.plt>
调到这个<setbuf@plt+6>
,然后走到<setbuf@plt+11>
,这里会jmp一下,jmp到哪里了呢,我们看下0x80483d0
这个地址
gdb-peda$ x/10i 0x80483d0
0x80483d0: push DWORD PTR ds:0x804a004
0x80483d6: jmp DWORD PTR ds:0x804a008 ;jmp *0x804a008 不是 jmp 0x0804a008
0x80483dc: add BYTE PTR [eax],al
0x80483de: add BYTE PTR [eax],al
0x80483e0 : jmp DWORD PTR ds:0x804a00c
0x80483e6 : push 0x0
0x80483eb : jmp 0x80483d0
0x80483f0 : jmp DWORD PTR ds:0x804a010
0x80483f6 : push 0x8
0x80483fb : jmp 0x80483d0
跳到了这个其实是plt
表的开头,你可以发现
这个plt
除了最开始的两句算是公共的表项,剩下的每个函数对应三句话,第三局都是跳转到plt表的开头,之后应该是又跳到什么地方然后把对应的GOT标表项改过来了就完结了吧
大概是这样子
setbuf第一次调用
main => => => => => plt表首 => 然后不知道经过了啥 => 里填上了真的setbuf的地址
拿出来我的小书书查了下这个不知道经过啥其实是在plt
表首jmp到了一下_dl_runtime_resolve()
,函数完成了地址绑定
gdb-peda$ x/10i *0x804a008 ;注意是取出jmp * ,别把星号丢了
0xf7fedf00: push eax
0xf7fedf01: push ecx
0xf7fedf02: push edx
0xf7fedf03: mov edx,DWORD PTR [esp+0x10]
0xf7fedf07: mov eax,DWORD PTR [esp+0xc]
上网查了下这一段确实是_dl_runtime_resolve()
的源码
还有补充说是.got.plt
的前三项(每一项4字节)代表的分别是
0x804a000: 0x08049f14 ; .dynamic段地址
0x804a004: 0xf7ffd918 ; module_id
0x804a008: 0xf7fedf00 ; dl_runtime_resolve地址
setbuf第n次调用
main => => => 成功调用
好吧,动态绑定这一块好像都搞清楚了
刚说到哪里了,对.我们走走走,回到了main里,我们刚刚是调用了puts
=> 0x80485ef: call 0x8048410
0x80485f4: call 0x804854d
0x80485f9: mov eax,0x0
0x80485fe: leave
0x80485ff: ret
我们看0x0804854d
这里调用了什么函数
=> 0x804854d: push ebp
0x804854e: mov ebp,esp
0x8048550: sub esp,0x98 ; 栈抬高0x98 => old_esp = esp + 0x98
0x8048556: mov DWORD PTR [esp+0x8],0x100 ; 这个是后面要调用的函数的arg3
0x804855e: lea eax,[ebp-0x88] ; 这个时候[ebp-0x88]相当于[old_esp - 0x88] => [esp + 0x10] ; 其实这个换算了也没啥意思,因为是lea,传地址,用谁算都一样
; 我的$esp = 0xffffcb30 , 所以这句之后$eax = 0xffffcb30 + 0x10 = 0xffffcb40
mov ebp, esp
后,栈里的数据
0000 0xffffcbc8 --> 0xffffcbe8 ;ebp
0004 0xffffcbcc --> 0x80485f9 ;ret_addr
再接着看
0x804855e: lea eax,[ebp-0x88] ; $eax = 0xffffcb40
=> 0x8048564: mov DWORD PTR [esp+0x4],eax ; arg2 = $eax
0x8048568: mov DWORD PTR [esp],0x0 ; arg1 = 0x0
0x804856f: call 0x80483f0 ; read(0x0, 0xffffcb40, 0x100)
0x8048574: mov DWORD PTR [esp+0x8],0x100
0x804857c: lea eax,[ebp-0x88]
看到这里,发现是给调用了read
,其实这里的arg2
的值是什么不重要,重要的是他是[ebp-0x88]
,就是说他离ebp
有0x88
的距离,大概是这样子
压完3个参数,但还没call之前栈里大概是这样子的
0xffffcb30 +----------------------+ [esp] = arg1
arg1 [0x0]
0xffffcb34 +----------------------+ [esp+0x4] = arg2
arg2 [0xffffcb40] -------------------------------+
0xffffcb38 +----------------------+ [esp+0x8] = arg3 |
arg3 [0x100] |
0xffffcb40 +----------------------+ [esp+0x10] |
0x88个字节的缓冲区 <-----------------------------+
0xffffcbc8 +----------------------+ [esp+0x98] [ebp]; 开始抬起来0x98
EBP
0xffffcbcc +----------------------+ [esp+0x9c]
ret_addr
0xffffcbd0 +----------------------+ [esp+0x100]
这样子写入地址距离ebp是0x88,但read可写入0x100的数据,这样子我们就能覆盖到返回地址ret_addr
理论上我们只要构造
python -c "print 0x88 * 'A' + 'ABCD' + our_ret_addr"
就能填满0x88大小的缓冲区,用ABCD
覆盖4字节的EBP,然后更改返回地址
然后我们查了一下checksec
gdb-peda$ checksec
CANARY : disabled
FORTIFY : disabled
NX : ENABLED
PIE : disabled
RELRO : Partial
发现开了NX
,堆栈不可执行,所以我们,可以使用ret2libc
的方法
不知道为什么这题到了read
这里没有提示我输入…
所以我是start < in.data
这样子调试的,确认了一下,这个字节数是正确的
然后写完poc发现不能用,这个时候上网查找了一下,pwntools有启gdb
的功能,让我试试
p = process('./pwn1')
context.terminal = ['gnome-terminal','-x','sh','-c']
gdb.attach(p)
简直不能太赞,但似乎,有一点问题,我怎么让停在第一句呢,去翻了翻官网
Attaching to processes with attach() is useful, but the state the process is in may vary. If you need to attach to a process very early, and debug it from the very first instruction (or even the start of main), you instead should use debug()
哦,我不应该用attach,应该是用gdb.debug()
差不多,是这样子的
if debug == 1:
p = gdb.debug('/home/haibin/nsctf/pwn/pwn1','''
start
continue
''')
else:
p = remote('127.0.0.1', 6666)
试了好多次,好像这里必须写成start continue
才会刚好停在main
的入口处…别的要不报错,要不停的太远…吐血
不过这个半路就GG了,连接被重置,气死了…
迷之.因为我payload没写完,这个时候我在最后加了一句
p.interactive()
突然发现都好了…醉了
大概是明白了,脚本执行完之后就会断开,这里加了这句,拖住了他???
这样子好像行不通….这个p.interactive()
会拖住脚本的运行
把他加在最后面的话,前面的脚本还是相当于都执行完了,而如果把他加在最前面那相当于这句话之后的脚本都不执行了…
心碎,要睡一觉
代码里有一个leave指令其实相当于
mov esp, ebp
pop ebp
当然这个pop ebp
之后,esp
又会esp+4
对吧
关于调用规约
__cdecl caller清理参数,右向左入栈
__stdcall callee清理参数,右向左入栈
__fastcall callee清理参数,前两个参数寄存器传参数,其余右向左压入栈
fastcall寄存器传参,所以叫fast,使用高性能场合
可变长参数函数只能使用__cdecl
,网上的解释是说只有调用者才知道传入了多少参数,以用来清理参数,回收栈空间,这个理由没法说服我,也许是我没有完全理解
另外
#define WINAPI __stdcall