从负开始学汇编0

Author Avatar
Aryb1n 7月 22, 2017

第一天

一句一句看一下昨天的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 xxxmov 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],就是说他离ebp0x88的距离,大概是这样子
压完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